跳过正文

9. 函数、方法和闭包:function/method/closure

·
目录
rust-lang - 这篇文章属于一个选集。
§ 9: 本文

函数用于执行一个任务或计算一个值。

函数使用 fn 声明, 使用 -> 来指定返回值类型,没有指定返回值时默认为 unit type 类型和值 ()

函数签名的每个参数都要标注类型,但 lifetime 可以使用 '_ 来让编译器自动推导(如根据 lifetime elision rule),如: fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>>{}

函数最多只能有一个返回值,可以用 tuple 等类型来封装多返回值的情况。

函数体最后一个表达式(不以分号结尾)作为函数的返回值, 也可以使用 return 语句提前返回。

// 函数名称、变量名称都使用 `snake_case` 命名风格。

// 无参数、无返回值的函数, 无返回值等效于返回 ()
fn greet_world() {
    println!("Hello, world!");
}

// 函数只能有一个返回值类型
fn function_name(parameter1: Type1, parameter2: Type2) -> ReturnType {
    // ...
}

// 接受一个参数,但无返回值的函数,等效于返回 -> ()
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

// 接受两个参数、有返回值的函数
fn add(a: i32, b: i32) -> i32 {
    // 表达式的值作为函数的返回值
    a + b
}

fn main() {
    // 函数定义的顺序是无关的,可以先使用,再定义。
    // 类似的,可以先使用再定义的情况还有:
    // 1. struct、enum 等类型定义,可以先使用再定义;
    // 2. 外部 mod 声明,如 mod auth,可以先使用 auth,再声明 mod auth;
    fizzbuzz_to(100);
}

fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
    if rhs == 0 {
        return false;
    }
    // 返回表达式值
    lhs % rhs == 0
}

fn fizzbuzz(n: u32) -> () {
    if is_divisible_by(n, 15) {
        println!("fizzbuzz");
    } else if is_divisible_by(n, 3) {
        println!("fizz");
    } else if is_divisible_by(n, 5) {
        println!("buzz");
    } else {
        println!("{}", n);
    }
}

函数支持嵌套定义,在同一个作用域内,函数定义的位置、顺序是无关的,可以先使用再定义:

fn main() {
    test();

    fn test() {
        println!("just fortest");
    }
}

函数传参是 Pattern Match 的过程,使用 _ 忽略不使用的参数:

/*
FunctionParam :
   OuterAttribute* ( FunctionParamPattern | ... | Type)

FunctionParamPattern :
  PatternNoTopAlt : ( Type | ... )
 */

fn first((value, _): (i32, i32)) -> i32 { value }

Rust 对象的的可见性是 block scope但是对象的生命周期不是 block scope,所以函数内创建的对象可以通过所有权转移的方式返回到调用方作用域,从而继续有效。

函数 lifetime
#

如果函数返回值包含借用,则需要和输入参数的 lifetime 有关系,而不能返回函数内部创建的对象的借用,否则会导致悬垂借用而编译失败。

  • 例外情况是返回 lifetime 是 ‘static 的借用值。
// 错误:返回借用时,它的 lifetime 必须和输入参数的 lifetime 有关系
fn test_func<'a, 'b>(a: &'a u32, b: &'b u32) -> &u32 {
    let _c = a + b;
    return &32;
}
/*
error[E0106]: missing lifetime specifier
  --> src/lib.rs:27:49
   |
27 | fn test_func<'a, 'b>(a: &'a u32, b: &'b u32) -> &u32 {
   |                         -------     -------     ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value with an elided lifetime, but the lifetime cannot be derived from the arguments
note: these named lifetimes are available to use
*/

// 另一典型的情况:函数返回值是 impl Trait,它可以具有&捕获 lifetime,从而导致自身不是 'static 的:
trait FooTrait<'a> {}

struct FooStruct;

impl<'a> FooTrait<'a> for FooStruct {}

// 错误:FooTrait 包含 'lifetime, 但返回 impl FooTrait 时未指定 lifetime,故报错。
// 另外,由于输入参数不是方法,而且有多个 lifetime,所以输出的 lifetime 不能被推导,impl FooTrait<'_> 也是错误的。
// OK: -> impl FooTrait<'a> 或 -> impl FooTrait<'b>
fn test_impl<'a, 'b>(a: &'a i32, b: &'b i32) -> impl FooTrait {
    let _c = *a + *b;
    return FooStruct;
}

/*
error[E0106]: missing lifetime specifier
  --> src/lib.rs:52:54
   |
52 | fn test_impl<'a, 'b>(a: &'a i32, b: &'b i32) -> impl FooTrait {
   |                         -------     -------          ^^^^^^^^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value with an elided lifetime, but the lifetime cannot be derived from the arguments
note: these named lifetimes are available to use
  --> src/lib.rs:52:14
   |
52 | fn test_impl<'a, 'b>(a: &'a i32, b: &'b i32) -> impl FooTrait {
   |              ^^  ^^
help: consider using one of the available lifetimes here
   |
52 | fn test_impl<'a, 'b>(a: &'a i32, b: &'b i32) -> impl FooTrait<'lifetime> {
   |                                                              +++++++++++
*/

另一典型的情况:函数返回值是 impl Trait,它可以具有&捕获函数参数的 lifetime,从而导致自身不是 ‘static 的:


pub trait FooTrait {}

pub struct FooStruct<'a> {
    name: &'a i32,
}

impl<'a> FooTrait for FooStruct<'a> {}

pub fn test_impl<'a, 'b>(a: &'a i32, b: &'b i32) -> impl FooTrait {
    let _c = *a + *b;
    let r = FooStruct { name: a };
    return r;
}

pub fn test_impl_func2<'a, 'b>(a: &'a i32, b: &'b i32) -> bool {
    let _c = test_impl(a, b);
    // _c 对应的 impl FooTrait 对象的 liftime 是 'a, 
    // 不满足 Rust 要求实现 Any 的对象必须是 'static
    /*
    pub trait Any: 'static {
        // Required method
        fn type_id(&self) -> TypeId;
    }
    */
    
    let c: &dyn std::any::Any = &_c;
    /*
    error: lifetime may not live long enough
      --> src/lib.rs:73:33
       |
    70 | pub fn test_impl_func2<'a, 'b>(a: &'a i32, b: &'b i32) -> bool {
       |                        -- lifetime `'a` defined here
    ...
    73 |     let c: &dyn std::any::Any = &_c;
       |                                 ^^^ coercion requires that `'a` must outlive `'static`
    */
    let i = c.is::<FooStruct<'_>>(); 
    return i;
}

函数签名(定义)包含 lifetime 类型参数,这意味着函数对象本身的 lifetime 必须要比这些 lifetime 短,也就是在执行函数期间,这些引用类型的参数引用的对象必须一致有效。

fn 函数指针类型
#

fn 是一个指针类型,类型为函数签名,占用一个 usize 内存,可以用作变量类型,如 let myfn: fn(&City) -> i64 = my_city_fn,可以像其它类型一样来使用,如保存到变量,作为函数的参数和返回值等。

例子:

let my_key_fn: fn(&City) -> i64 = // 变量类型为函数指针
if user.prefs.by_population {
    city_population_descending // 函数名实际为函数指针
} else {
    city_monster_attack_risk_descending
};
cities.sort_by_key(my_key_fn);

// 另一个例子:
// 声明 fn 函数指针类型别名
type Binop = fn(i32, i32) -> i32;

//  下面这些函数签名均和 Binop 一致,故均是 Binop 类型
fn add(a: i32, b: i32) -> i32 { a + b }
fn subtract(a: i32, b: i32) -> i32 { a - b }
fn multiply(a: i32, b: i32) -> i32 { a * b }

// 使用函数指针类型作为参数类型
fn apply_operation(operation: Binop, x: i32, y: i32) -> i32 {
    // 调用函数指针对应的函数
    operation(x, y)
}

fn main() {
    // 保存函数指针类型的变量
    let mut operation_to_perform: Binop;

    operation_to_perform = add;
    println!("Result of add: {}", apply_operation(operation_to_perform, 10, 5));

    operation_to_perform = subtract;
    println!("Result of subtract: {}", apply_operation(operation_to_perform, 10, 5));

    println!("Directly passing multiply: {}", apply_operation(multiply, 10, 5));
}

fn 不是闭包,不能捕获上下文中的对象或借用。但对于 fn 类型参数,可以传入没有捕获上下文的闭包,Rust 通过 type coercion 将闭包类型自动隐式转换为 fn 函数指针类型。

Rust 也提供了函数定义(函数 item)到 fn 指针类型的 type coercion,故可以将函数名赋值给函数指针类型变量。

fn process_data(processor: fn(i32) -> i32) {
    println("{}", processor(42));
}

fn main() {
    // OK:闭包没有捕获上下文对象
    process_data(|x| x+1);

    // ERROR:捕获了上下文对象的闭包不能转换为 fn 函数指针
    let multiplier = 2;
    process_data(|x| x*multiplier);
}

Rust fn 函数实现了 Fn/FnMut/FnOnce trait, 也实现了 Clone/Copy/Send/Sync/Unpin trait。

  • 函数的输入参数、输出参数类型都不影响这些 trait 的实现。

使用 Fn/FnMut/FnOnce 限界的闭包类型参数,也可以使用 fn 函数;

// 闭包类型参数也可以传入 fn 函数指针
let v: Vec<&str> = "1abc2abc3".matches(char::is_numeric).collect();

fn 函数指针类型支持 lifetime 定义和 HRTB lifetime,常用于不支持 lifetime 的闭包赋值(闭包可以通过 type coercion 隐式转换为 fn 函数指针):

let test_fn: for<'a> fn(&'a _) -> &'a _ = |p: &String| p;
println!("Results:{}", test_fn(&"asdfab".to_string()));

fn 函数语法
#

https://doc.rust-lang.org/reference/items/functions.html

  • FunctionQualifiers :fn 前可以加限定符:const,async,unsafe,extern ABI;

self 参数支持两种格式:

  1. ShorthandSelf : (& | & Lifetime)? mut? self, 如 self, mut self, &self, &mut self, &'a mut self;
  2. TypedSelf : mut? self : Type, 如 self: Type, mut self: Type, 传入的是 self 或 mut self,会消耗 self;
Function :
   FunctionQualifiers fn IDENTIFIER GenericParams? ( FunctionParameters? ) FunctionReturnType? WhereClause? ( BlockExpression | ; )

FunctionQualifiers :
   const? async? unsafe? (extern Abi?)?

Abi :
   STRING_LITERAL | RAW_STRING_LITERAL

FunctionParameters :
      SelfParam ,? | (SelfParam ,)? FunctionParam (, FunctionParam)* ,?

SelfParam :
   OuterAttribute* ( ShorthandSelf | TypedSelf )

ShorthandSelf :
   (& | & Lifetime)? mut? self

TypedSelf :
   mut? self : Type

FunctionParam :
   OuterAttribute* ( FunctionParamPattern | ... | Type2 )

FunctionParamPattern :
   PatternNoTopAlt : ( Type | ... )

FunctionReturnType :
   -> Type

其中的泛型参数 GenericParamswhere 语法参考:10-rust-lang-generic-trait.md

可以给函数整体,或函数参数指定 attribute macro 来实现条件编译或特殊处理语义:

fn len(
    #[cfg(windows)] slice: &[u16],
    #[cfg(not(windows))] slice: &[u8],
) -> usize {
    slice.len()
}

const fn
#

用来初始化全局的 const/static 常量,需要在编译时执行,所以实现上有些限制:

  1. 内部只能调用其它 const 函数;
  2. 不能动态分配内存,操作原始指针(即使在 unsafe block 中也不行);
  3. 除了 lifetime 外, 不能使用其他类型作为泛型参数;

extern fn
#

使用指定的 ABI 来定义函数,用于将 Rust 函数导出给其它 ABI 的程序(如 C/C++)调用:

  • 未指定 extern 时,默认为 extern "Rust";
  • 指定 extern 但是未指定 ABI 时,默认为 extern "C"
fn foo() {}
// 等效于
extern "Rust" fn foo() {}

extern fn new_i32() -> i32 { 0 }
let fptr: extern fn() -> i32 = new_i32;
// 等效于
extern "C" fn new_i32() -> i32 { 0 }
let fptr: extern "C" fn() -> i32 = new_i32;

在使用 extern "C" 将声明函数使用 C ABI 导出时,通常还加 #[no_mangle] 属性,它用于指示 Rust 编译器不对该函数改名,从而让其它语言能正确链接到导出的函数名实 现上。

#[no_mangle]
extern "C" fn new_i32() -> i32 { 0 }

类似的还有 extern block,用于声明 Rust 中可以调用的外部函数库中的函数或静态(static)变量&常量。

#[cfg(target_family = "unix")]
#[link(name = "readline", kind = "dylib")]
// extern block 中只能使用 static 变量和函数签名。
extern "C" {
    // 静态常量
    static rl_readline_version: libc::c_int;

    // 静态变量
    static mut rl_prompt: *const libc::c_char;

    // 函数签名
    fn with_name(format: *const u8, args: ...);

    // 变长参数函数签名( 只有 extern block 中的函数签名的最后一个参数支持变参)
    fn foo(x: i32, ...);
}

// 需要在 unsafe 中使用 extern block 中声明的函数或全局对象
unsafe { foo(23) }

关联函数和方法
#

trait 和类型的 impl XX {} 都支持定义关联函数(Associated functions)和方法(Method):

  • 第一个参数名为 self 时(如 &self,&mut self, self: XX, mut self: XX)为方法,否则为关联函数。

  • &self 等效为 self: &Self&mut self 等效为 self: &mut Self

方法中可以使用 Self 类型,等效为 impl XX 中的 XX 类型, 当 XX 类型比较复杂(如泛型)时,使用 Self 更简洁。

也可以为方法的 self 参数指定其它类型,如 Box<T>,这时只能使用该类型的对象来调用方法:

pub fn into_vec<A>(self: Box<[T], A>) -> Vec<T, A> where A: Allocator

let s: Box<[i32]> = Box::new([10, 40, 30]);
let x = s.into_vec();
// s 已经被 move,不能再访问。

函数变量赋值时,输入参数是逆变,返回值是协变:

// 'middle is used in both a co- and contravariant position.
fn takes_fn_ptr<'short, 'middle: 'short>(f: fn(&'middle ()) -> &'middle (), ) {

    // As the variance at these positions is computed separately, we can freely shrink 'middle in
    // the covariant position and extend it in the contravariant position.

    // 函数输入参数是逆变,返回结果是协变。
    let _: fn(&'static ()) -> &'short () = f;
}

方法查找
#

t.method() 方法调用时,根据 method 的 self 参数要求,自动对 t 值类型进行转换,如自动解引用和自动借用,以及 type coercion 类型转换等,直到类型匹配该方法的 self 参数类型。

  • 更一般的,. 操作符支持自动解引用和自动借用,如 sb.field 等效于 (*sb).field 即自动解引用后返回 field 值。

示例:调用 T 类型对对象 value 的 foo() 方法(value.foo()) 时,编译器:

  1. 检查是否可以直接调用 T::foo(value),即先看 T 是否直接实现了方法 foo();
  2. 再尝试调用 <&T>::foo(value)<&mut T>::foo(value) ,即看 &T, &mut T 类型是否实现了方法 foo();
  3. 如果 T 本身不是引用类型(但 value 可以是引用类型)且实现了 Deref<Target=U>, 则执行 *T 获得 U 类型值, 然后对 U 重新执行 1-2 步骤;
  4. 最后尝试 unsized coercion 到类型 U,然后重新执行 1-2 步骤。

unsized coercion 是 Rust 内置的的隐式转换,目前支持如下三种情况(不能自定义和扩展):11-rust-lang-type-coercion.md

  1. [T; n] 到 [T]. : 如 array 对象可以调用 slice 的方法。
  2. T 到 dyn U: 如果 T 实现了 U + Sized,而且 U 是对象安全的(object safe 或更新的术语 dyn compatiable)的 trait;
  3. 实现了 CoerceUnsized<Foo<U>>&T,&mut T 和智能指针类型;
let array: Rc<Box<[T; 3]>> = ...;

// val[i] 自动解引用 index() 方法的返回值,等效为 *array.index(0)
let first_entry = array[0];
  1. 编译器先检查 Rc<Box<[T; 3]>> 类型是否实现了 Index trait,结果没有。同时 &Rc<Box<[T; 3]>>&mut Rc<Box<[T; 3]>> 也都没有;
  2. 编译器使用 Deref traitRc<Box<[T; 3]>>Box<[T; 3]> ,继续尝试;
  3. Box<[T; 3]>, &Box<[T; 3]>, 和 &mut Box<[T; 3]> 都没有实现 Index,所以继续 Deref[T; 3];
  4. [T; 3] , &[T; 3], &mut [T;3] 没有实现 Index;
  5. 编译器尝试 unsized coercion,结果为 [T], 而它实现了 Index trait,所以可以调用 index() 函数;

注意:上面的 method lookup 过程不会考虑可变性,lifetime 和 unsafe。

闭包
#

闭包是编译器生成的匿名类型,编译器为它自动实现了 Fn/FnMut/FnOnce trait

闭包特性:

  • 使用 || 而非 () 来指定输入参数列表;
  • 如果是单行表达式,可以忽略大括号,否则需要使用大括号;
  • 如果指定返回值类型, 则必须使用大括号;
  • 可以省略返回值声明,默认根据表达式自动推导;
  • 闭包的输入和输出参数一旦被自动推导后就不能变化(第一次调用时被推导&实例化),后续多次调用时,传的或返回的参数值类型必须匹配第一次调用时的值类型
// fn 函数:必须指定输入、输出参数类型
fn  add_one_v1 (x: u32) -> u32 { x + 1 }

// 闭包:输入、输出参数类型可以自动推导。
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

// 闭包:返回值类型根据后续对 one 的使用方式来定。
let one = || 1;

// block 中 return 或最后一个表达式值作为返回
let closure_annotated = |i: i32| -> i32 { i + outer_var };

// error:指定返回值类型时必须使用大括号
let is_even = |x: u64| -> bool x % 2 == 0;
// ok
let is_even = |x: u64| -> bool { x % 2 == 0 };

// 单行表达式的结果作为值返回,不需要指定大括号
let closure_inferred  = |i | i + outer_var;

let color = String::from("green");
// 闭包对应的匿名对象内部共享借用了 color(定义闭包时,借用已经发生)
let print = || println!("`color`: {}", color);
// 使用 move,闭包将 color 捕获到生成的匿名对象内部(定义闭包时,转移已经发生)
let print = move || println!("`color`: {}", color);

// 闭包的输入、输出值类型一旦被推导出来后,就不能再变化。
//
// 两次函数调用的推导类型不一致,编译失败。
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);

// 传给闭包的参数列表也是 pattern 赋值语法。
// 如果闭包不使用传入的参数,可以将其设置为 _。
fn main() {
    foobar(32, 64, |_, _| panic!("Comparing is futile!"));
}

编译器根据闭包使用对象方式来自动确定如何捕获该对象(struct/tuple/enum 一般被作为一个整体来捕获,但也可以使用临时变量来捕获某个 field):

  1. 共享借用上下文中的对象 &T: 优先选择该类型,如闭包中以只读方式使用上下文对象;
  2. 可变借用上下文中的对象 &mut T: 如闭包中修改了上下文对象;
  3. 将上下文中的对象所有权移动到闭包中(move): 如闭包中 drop 对象,或返回 non-copy 对象 (转移所有权);
fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
    // 闭包捕获了函数参数 stat,它位于函数栈上,使用共享借用 &T 捕获。
    cities.sort_by_key(|city| -city.get_statistic(stat));
}

move 捕获:一般用于多线程环境中,如后续在另一个线程中运行改闭包,编译器不能推断闭包对象内部借用的对象生命周期是否有效,所以需要确保生成的匿名闭包对象是 'static 的。也即,如果闭包内部 借用了上下文中的对象,需要确保该借用的 lifetime 是 'static 的。一般情况下很难满足这个约束,所以需要使用 move 关键字将上下文对象所有权转移到闭包中。

let s = String::from("coolshell");

// s 作为闭包返回值,所以是 move 语义。
// 在定义闭包时,s 已经被 move 捕获到闭包中,外部不能再访问。
let take_str = || s;

// s 已经被 move 进闭包,不能再访问。(除非该闭包后续不再使用)
//println!("{}", s);
println!("{}",  take_str()); // OK

let movable = Box::new(3);
let consume = || {
    println!("`movable`: {:?}", movable);
    // drop() 方法需要拥有对象所有权,所以 movable 对象被转移到闭包中
    std::mem::drop(movable);
};
consume(); // 该闭包只能调用一次
// consume(); // 报错

这种捕获是在定义闭包时已经发生(闭包定义时编译器即生成一个匿名类型对象,并将捕获的对象所有权转移到该对象中),但如果闭包后续不再使用,则捕获失效,被捕获的对象还可以继续在原上下文中使用(类似于这种的情况还有 &T/&mutT 借用):

// 在定义闭包时捕获环境中对象,但如果闭包后续不再使用,则捕获失效,原对象可以继续访问。
let mut count = 0;
let mut inc = || {
    count += 1;  // &mut 捕获 count 对象
    println!("`count`: {}", count);
};

// 错误:count 已经被闭包 &mut 捕获,所以不能再访问和修改。
// count += 2;
inc();
// OK:闭包后续不再使用,故还可以继续访问 count。
assert_eq!(count, 1);


// 另一个例子
let color = String::from("green");
// print 有效时(后续还有调用)会保有 color 的共享引用
let print = || println!("`color`: {}", color);
print();
// 由于闭包是共享引用,所以原对象还可以有其它共享引用
let _reborrow = &color;
print();
// print 后续不再使用,所以可以 move color
let _color_moved = color;

move 会把捕获的变量转移进闭包,但如果捕获的是 &T 本身,则闭包内部获得的还是 &T闭包整体并不满足 ‘static

use std::thread;

let people = vec![
    "Alice".to_string(),
    "Bob".to_string(),
    "Carol".to_string(),
];

let mut threads = Vec::new();

for person in &people {
    threads.push(thread::spawn(move || {
        // person 是 &String 类型,所以 move 捕获的是 &String 而非 String
        println!("Hello, {}!", person);
    }));
}

for thread in threads {
    thread.join().unwrap();
}

// 报错:
/*
error[E0597]: `people` does not live long enough
  --> src/main.rs:12:20
   |
12 |     for person in &people {
   |                    ^^^^^^ borrowed value does not live long enough
...
21 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...
*/

闭包在第一次被调用时推导输入和输出值类型,后续不能再变化,也即后续再次调用该闭包时传入的参数和 liftime 需要符合第一次实例化推导的 lifetime 和参数值类型

// https://rust-lang.github.io/rfcs/3216-closure-lifetime-binder.html
use std::cell::Cell;

fn main() {
    let static_cell: Cell<&'static u8> = Cell::new(&25);
    let closure = |s| {};
    closure(static_cell);
    let val = 30;
    let short_cell: Cell<&u8> = Cell::new(&val);
    closure(short_cell);
}
/*
error[E0597]: `val` does not live long enough
  --> src/main.rs:8:43
   |
4  |     let static_cell: Cell<&'static u8> = Cell::new(&25);
   |                      ----------------- type annotation requires that `val` is borrowed for `'static`
...
8  |     let short_cell: Cell<&u8> = Cell::new(&val);
   |                                           ^^^^ borrowed value does not live long enough
9  |     closure(short_cell);
10 | }
   | - `val` dropped here while still borrowed
*/

这里的问题是:第一次调用 closure(static_cell) 时将它的类型推导为 |s: Cell<&'static u8>|, 所以后续它不能再传入 Cell<&'0 u8> 的方式传 入一个更短的 lifetime &'0

闭包实现 Copy/Clone
#

Rust 自动判断闭包对象是否实现 Copy、Clone 的规则和 struct 类似:

  1. 只持有 &T 的闭包同时实现了 Copy 和 Clone;
  2. 持有 &mut T 的闭包没有实现 Copy 和 Clone;
  3. 通过 move 转移所有权的闭包,如果转移的所有对象实现了 Copy/Clone 则该闭包实现了 Copy/Clone;

在要对闭包对象传给多个函数时,是否实现 Copy/Clone 会影响对象转移:

let y = 10;
let add_y = |x| x + y; // add_y 闭包对象实现了 Copy
let copy_of_add_y = add_y; // Copy 赋值
assert_eq!(add_y(copy_of_add_y(22)), 42); // 调用多次


let mut x = 0;
let mut add_to_x = |n| { x += n; x }; // add_to_x 内部使用了 &mut 借用捕获 x,所以没有实现 Copy 和 Clone
let copy_of_add_to_x = add_to_x; // 复制时转移了所有权
assert_eq!(add_to_x(copy_of_add_to_x(1)), 2); // 再使用 add_to_x 时报错


let mut greeting = String::from("Hello, ");
let greet = move |name| {
    greeting.push_str(name); // move 捕获了 greeting 所有权,而 String 实现了 Clone,所以闭包实现了 Clone
    println!("{}", greeting);
};
greet.clone()("Alfred");  // OK
greet.clone()("Bruce");   // OK

闭包对象是否实现 AutoTrait,如 Send、Sync、Unpin 等,也是看捕获的对象是否都实现了它们。

上面判断闭包是否实现 Copy/Clone/Send 等 trait 时,考虑的都是闭包捕获的上下文对象,而不用考虑闭包的输入参数和内部创建的对象(因为它们是栈对象,在同步多线程场景下,一旦创建就不会跨线程)。

  • 如果是异步闭包,则还需要考虑输入参数和内部创建的对象。

闭包的编译器实现
#

在定义闭包时,编译器为其自动生成匿名类型,并为该为该匿名类型实现 Fn/FnMut/FnOnce trait,具体取决于闭包捕获上下对象的方式:

  • 共享捕获:实现了 Fn trait,可以被调用多次;
  • 可变捕获:实现了 FnMut trait,可以被调用多次;
  • 转移对象所有权:实现了 FnOnce trait,只能被调用一次(消耗了闭包对象自身)

FnOnce trait 的定义如下:

// Rust 为闭包实现的 FnOnce trait 定义:
pub trait FnOnce<Args> where Args: Tuple,
{
    type Output; // 为闭包的返回值类型

    // Required method
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

// F 是 FnOnce() 类型,所以只能被调用一次;
fn f<F: FnOnce() -> String> (g: F) {
    println!("{}", g());
}

let mut s = String::from("foo");
let t = String::from("bar");
// 闭包修改和返回了 s,所以该闭包转移了 s 的所有权,编译器为其实现了 FnOnce trait, 满足函数 f 的参数限界要求
f(|| { s += &t; s});
// Prints "foobar".

如果闭包通过 &T/&mutT 借用方式捕获了上下文对象,则生成的匿名类型包含 lifetime 泛型参数,在实现 FnXX trait 时也包含这些 lifetime 泛型参数,也即生成的匿名闭包对象是有特定 lifetime 的(和借用的上下文对象 lifetime 有关),而这一般很难满足 'static 要求,所以一般需要使用 move 来将对象所有权转移到闭包中。

比例如,对于含有闭包的表达式 f(|| { s += &t; s});, 由于闭包通过 &T 借用捕获了上下文对象,所以生成的匿名类型包含 lifetime:

// 编译器为闭包生成的匿名对象类型示例:
struct Closure<'a> {
    s : String,     // 转移 s 所有权,不是借用:因为闭包返回 s,所以需要转移 s 所有权。
    t : &'a String, // 对于 t 是共享借用
}

// 由于闭包返回 s 即转移了捕获对象的所有权,所以闭包只能调用一次,编译器为其实现 FnOnce trait:
impl<'a> FnOnce<()> for Closure<'a> {
    type Output = String; // 闭包的返回值类型

    fn call_once(self) -> String {
        self.s += &*self.t;
        self.s
    }
}

f(|| { s += &t; s});
// 等效于:
let c = Closure{s: s, t: &t} // Closure 的 lifetime 和 t 一致,也即 c 匿名类型是有特定 lifetime 的对象(非 'static)
f(c);

类似的还有返回值为 impl Trait 匿名类型(如显式返回该类型对象,或者通过 async fn 函数返回的值),也可能包含 lifetime:

// 编译器为 impl Sized 生成的匿名类型同时捕获了 ‘a 和 T(Rust 2024 版本后的默认行为,也即 impl XX 作为函数返回值时默认捕获所有泛型参数,可以通过 use<> 来定义精确捕获)
fn foo<'a, T>(x: &'a T) -> impl Sized {
    (x,)
}

// 另一个例子:
async fn foo(x: &mut i32) -> i32
// 等价于:
fn foo<'a>(x: &'a mut i32) -> impl Future<Output = i32> + 'a  // 返回的匿名对象包含 lifetime,且和 x 一致

// 更精确的,返回值的类型是:impl Future<Output = i32> + '_
// 这里的 '_ 表示 编译器会为其捕获函数的范型参数和 lifetime,具体参考前面的 impl Trait 一节。
//
// 后续调用 foo 返回的 fut 类型为:impl Future<Output = i32> + '_,推导的生命周期和 v 一致。
let fut = foo(&mut v);
// 错误:fut 不是 'static 类型。
tokio::spawn(fut).await.unwrap();

编译器为闭包实现 FnOnce trait 的情况:闭包内部 Drop 捕获的对象,或将捕获对象所有权转移出来或转移给其它函数,则该闭包函数只能调用一次。

// 闭包前加 move,无条件转移所引用的对象所有权到闭包中
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);
    // list 所有权被转移到闭包
    thread::spawn(move ||
        println!("From thread: {:?}", list)
    ).join().unwrap();
}

let color = String::from("green");
let print = move || println!("`color`: {}", color);
print();
// 报错:color 已被转移到闭包,不能再访问。
// let _reborrow = &color;
// println!("{}", _reborrow);

// Error
fn main() {
    let movable = Box::new(3);

    // consume 只能调用一次,因为它内部将 movable 变量 move 走了。
    let consume = || {
        println!("`movable`: {:?}", movable);
        take(movable);
    };
    consume();

    // 错误:闭包捕获的 movable 所有权被转移到 take() 函数,所以闭包只能调用一次。
    // consume();
}
fn take<T>(_v: T) {}

对于转移到闭包中的对象,闭包外不能再使用(借用)该变量,外围对象被借用闭包捕获后也不能再修改它的值。(实际上,一旦对象被借用,不管是共享还是可变借用,只要该借用还有效,对象都不能被修改或 move。《== 借用冻结)

fn main() {
    let mut a = 123;
    let ar = &a;
    // a = 456; // Error:cannot assign to `a` because it is borrowed
    println!("{ar}")
}

fn main() {
    let mut x = 4;
    // x 被共享借用
    let add_to_x = |y| y + x;

    let result = add_to_x(3);
    // 输出:The result is 7
    println!("The result is {}", result);

    // 错误:在被共享借用的有效情况下(后续会调用执行该闭包), 不能修改其值
    // x = x + 3;

    let result2 = add_to_x(3);
    println!("The result2 is {}", result2);
}

Fn* 函数限界
#

Rust 的泛型参数支持使用函数 Fn/FnMut/FnOnce 进行限界,后续可以传入闭包:

// TypePathSegment 典型的情况是 :: 分割的标识符,如 ::std::ops::Index,但也支持更复杂的情况
TypePathSegment :
  PathIdentSegment (::? (GenericArgs | TypePathFn))?

PathIdentSegment  // IDENTIFIER 为函数类型关键字如 Fn/FnMut/FnOnce 等
    IDENTIFIER | super | self | Self | crate | $crate

// 函数闭包限界,如 fn myfunc(F: Fn(i32, &str) -> bool) {}
// 或 fn myfunc(S, F: Fn(i32, <S as Trait>::Assoc) -> bool)
TypePathFn :
( TypePathFnInputs? ) (-> Type)?

Fn/FnMut/FnOnce 作为 trait 限界时:

  1. 表达了后续使用改闭包的方式:如限界是 Fn 时,可以可能同时调用多次,限界是 FnOnce 时只会调用一次;
  2. 也限制了后续传入的闭包类型:如限界是 FnOnce 时,可以传入实现 Fn/FnMut/FnOnce 类型的闭包。如果限界是 Fn,则只能传入 Fn 类型闭包;
// 例如 std::thread::spawn() 函数的参数参数是一个闭包限界类型
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    // F 是一个 FnOnce 类型的闭包,整体实现了 Send,具有 'static 生命周期
    F: FnOnce() -> T + Send + 'static, 
    // 闭包的返回值同样需要实现 Send 和 具有 'static 生命周期
    T: Send + 'static, 

// 闭包整体是否实现 Send,是否满足 'static,取决于捕获的对象是否实现了 Send 和满足 'static,而与闭包的参数和内部创建的对象无关。
// 典型的情况是闭包捕获了 Rc 时,没有实现 Send。通过 &T/&mut T 捕获对象时,一般未实现 'static。

fn foobar<F>(mut f: F)  where F: FnMut(i32) -> i32
{
    let tmp = f(2);
    println!("{}", f(tmp));
}

fn main() {
    let mut acc = 2;
    // 传入的闭包实现了 FnMut trait,因为它修改了上下文对象 acc。
    foobar(|x| {
        acc += 1;
        x * acc
    });
}
// output: 24


fn foobar<F>(f: F) where F: Fn(i32) -> i32 // 可以对 f 调用多次,因为 F 不含上下文对象的 &mut 借用,也没有转移上下文对象
{
    println!("{}", f(f(2)));
}

fn main() {
    let mut acc = 2;

    // 错误:闭包修改了 acc,所以该闭包是 FnMut 类型,不满足函数限界的 Fn 类型要求。
    foobar(|x| {
        acc += 1;
        x * acc
    });
}


fn foobar<F>(f: F) // f 是 FnOnce 类型,所以可以传入 Fn/FnMut/FnOnce 类似的闭包
    where F: FnOnce() -> String
{
    println!("{}", f());
    // 错误:f 是 FnOnce 类型,所以只能被调用一次。
    // println!("{}", f());
}

Fn 是 FnMut 子类型, FnMut 是 FnOnce 子类型, 所以在使用它们进行限界时:

  1. Fn 最特殊,如果用 Fn 限界,则只能传入 Fn 类型闭包;
  2. FnOnce 最一般,可以传入所有闭包类型;

FnOnce/FnMut/Fn trait 的定义如下:

// std::ops::FnOnce
pub trait FnOnce<Args> where Args: Tuple,
 {
     type Output; // 输出参数类型

     // Required method
     // 传入 self
     extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
 }

// std::ops::FnMut,FnMut 是 FnOnce 子类型
pub trait FnMut<Args>: FnOnce<Args> where Args: Tuple,
 {
     // Required method
     // 传入 &mut self
     extern "rust-call" fn call_mut( &mut self, args: Args ) -> Self::Output;
 }

// std::ops::Fn, Fn 是 FnMut 子类型
pub trait Fn<Args>: FnMut<Args> where Args: Tuple,
  {
      // Required method
      // 传入 &self
      extern "rust-call" fn call(&self, args: Args) -> Self::Output;
  }

// F 是 Fn() 类型闭包
fn apply<F>(f: F) where F: Fn() {
    f();
}

fn main() {
    let x = 7;
    let print = || println!("{}", x);
    apply(print);
}


#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];
    // OK:编译器推断该闭包符合 FnMut 要求,虽然它没有捕获外围任何对象
    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];
    let mut sort_operations = vec![];
    let value = String::from("by key called");
    // 错误:编译器推断该闭包为 FnOnce 类型,不符合 sort_by_key() 的要求 FnMut
    list.sort_by_key(|r| {
        // 转移了捕获对象 value 的所有权,所以该闭包实现的是 FnOnce
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

传入的闭包需要满足 Fn/FnOnce/FnMut 的限界要求,包括输入参数、输出参数的参数类型和个数,如果闭包不使用某个参数,可以设置为 _:

fn countdown<F>(count: usize, tick: F)
    where F: Fn(usize)
{
    for i in (1..=count).rev() {
        tick(i);
    }
}

fn main() {
    countdown(3, |i| println!("tick {}...", i));
    countdown(3, |_| ());
}

fn 函数指针无条件的实现了 Fn/FnOnce/FnMut trait,所以对于用 Fn/FnOnce/FnMut trait 限界的参数,可以传入闭包函数或 fn 函数指针。

但对于 fn 函数指针类型的函数参数,只能传入函数指针(函数名),或传入没有捕获上下文对象的闭包:

// 闭包是一种匿名类型,可以赋值被变量
let add = |x, y| x + y;
// 调用闭包函数
let mut x = add(5,7);

// 没有捕获环境中值的闭包,可以转换为函数指针
type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);

Fn* 函数限界的 HRTB
#

使用 Fn/FnMut/FnOnce进行限界时,如果输入、输出参数包含借用,这时一般需要使用 HRTB 类型的 liftime 声明(但不绝对)。

这是因为 HRTB 类型的 lifetime 是任意短的,其它任意具体的 ‘special lifetime 都是它的子类型,可以赋值给 HRTB lifetime。

HRTB: Higher-Rank Trait Bounds

对于闭包限界,一般使用 HRTB lifetime 声明,原因是闭包输入参数的 lifetime 是在实际调用闭包时才确定的(根据传入的对象的具体 lifetime),但它们都必须是定义闭包限界时指定的 lifetime ‘a 的子类型,这样后续才能赋值给限界的变量,而 HRTB 类型的 ‘a lifetime 是任意短的,所以满足这个要求。

// 闭包限界包含引用参数时,可以不使用 HRTB lifetime,而是使用特定的 lifetime 标记:
fn apply_fnmut_simple<'a, 'b, F>( mut f: F, x: &'a i32, y: &'b str, ) -> Combined<'a, 'b>
where
    // FnMut 限界没有使用 HRTB,而是特定的 'a 和 'b, 故在调用时只能传入和参数 x、y 相同 lifetime 借用参数
    F: FnMut(&'a i32, &'b str) -> Combined<'a, 'b>,
{
    f(x, y)
}

// FnMut 限界使用了 HRTB,在调用时可以传入任意 lifetime 的借用参数
fn apply_fnmut_simple2<'a, 'b, F>( mut f: F, x: &'a i32, y: &'b str, ) -> Combined<'a, 'b>
where
    F: for<'x, 'y> FnMut(&'x i32, &'y str) -> Combined<'x, 'y>,
{
    f(x, y) // x 和 y 的 lifetime 都是 HRTB lifetime 的子类型,所以可以赋值。
}

fn call_on_ref_zero<F>(f: F) where for<'a> F: Fn(&'a i32) {
    let zero = 0;
    // f 的 'a 是 HRTB,所以可以传入任意具体 lifetime 的值,如 &zero。
    f(&zero);
}

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

// 1. 函数 Closure 泛型参数中没有 'a lifetitme
//
// 2. 在 F 的 Bound 中使用 for <'a> 来声明 'a lifetime,这里 for <'a> 表示对于任意 lifetime 'a, Fn 都满足, 也即 for <'a> 的
// 'a 可以看作无限短,其它任何具体的 lifetime 都是它的 subtype
impl<F> Closure<F> where F: for <'a> Fn(&'a (u8, u16)) -> &'a u8,
{
    fn call<'a>(&'a self) -> &'a u8 {
        // 'a 和上面的 for <'a> 没有关系,是 call() 方法自己的 lifetime。
        // 由于编译器会自动加 lifetime,所以可以不指定 'a 如:fn call(&self) -> &u8
        (self.func)(&self.data)
    }
}

fn do_it<'b>(data: &'b (u8, u16)) -> &'b u8 { &data.0 }

fn main() {
    'x: {
        let clo = Closure {
            data: (0, 1),
            // 由于 F 是 HRTB,任意其它 lifetime(如 'b) 都是它的子类型,可以赋值给 func。
            func: do_it
        };
        println!("{}", clo.call());
    }
}

闭包类型限界,如果输入或输出包含引用且没有使用 lifetime 声明,则默认使用 HRTB, 并根据 lifetime-elision 规则来将添加的输入和输出 lifetime 关联起来,所以闭包限界的输出 liftime 大部分情况可以省略不写。

  • lifetime elision rule 适用于普通函数、函数指针和作为限界的闭包 trait 签名,但不适用于闭包定义(因为闭包定义不支持 lifetime 参数)(这会带来一些列问题,参考 9-rust-lang-function-closure.md)
impl<T> [T] {
    pub fn sort_by_key<K, F>(&mut self, mut f: F)
    where
        // F 是 FnMut 类型,由于输入包含借用 &T 但没有声明 liftime,编译器自动转换为 HRTB 声明,等效为: F: for<'a> FnMut<&'a T> -> K,
        F: FnMut(&T) -> K,
        K: Ord,
    {
        stable_sort(self, |a, b| f(a).lt(&f(b)));
    }
}

// 另一个例子
#[derive(Debug)]
struct Combined2<'a> {
    num: &'a i32
}

fn apply_fn_mut_simple4<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    _y: &'b str,
) -> Combined2<'a>
where
    // 这里应该是使用了 HRTB,同时使用 lifetime-elision 将输出和输入的借用关联起来。等效为:F: for<'x> FnMut(&'x i32) -> Combined2<'x>
    F:  FnMut(&i32) -> Combined2,
{
    f(x)
}

fn main(){
    let x = 23;
    let y = "sfasd";
    let result = apply_fn_mut_simple4(|i| Combined2{num: i}, &x, &y);
    println!("{result:?}");
}

// 另一例子:使用 HRTB,这里由于输入、输出包含多个借用,则必须显式指定它的 lifetime(如果输入、输出只包含一个引用,则由于 lifetime-elision,可以忽略 lifetime)
fn apply_fnmut_simple2<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b>
where
    F: for<'x, 'y> FnMut(&'x i32, &'y str) -> Combined<'x, 'y>,
{
    f(x, y)
}

虽然闭包定义不支持声明 lifetime 参数,但是可以使用外围定义的 lifetime 参数:

fn testStr<'a> (input: &'a String) -> &'a String {
    let closure_test = |input: &'a String | -> &'a String {input};
    return closure_test(input);
}

闭包限界除了使用 HRTB 外,还可以将整个闭包标记为 'static,这意味着闭包内部借用捕获的对象以及返回的对象必须都是 'static,这样才能满足 Box 中对 T 是 ‘static 的要求:

  • 不考虑闭包的输入参数,因为编译器在定义闭包时就创建了对应的匿名类型,'static 是对该匿名类型的要求,而输入参数的 lifetime 是在调用时传入的。
type BoxedCallback = Box<dyn Fn(&i32) -> i32>;
struct BasicRouter{
    routers: std::collections::HashMap<String, Boxe dCallback>
}

//
impl BasicRouter {
    fn add_route<C>(&mut self, url: &str, callback: C)
        where C: Fn(&i32) -> i32 // 因为 Box<T> 等价于 Box<T + 'static >,所以这里必须添加 + 'static 约束
      {
          self.routers.insert(url.to_string(), Box::new(callback));
      }
}
/*
error[E0310]: the parameter type `C` may not live long enough
   --> src/main.rs:182:46
    |
182 |         self.routers.insert(url.to_string(), Box::new(callback));
    |                                              ^^^^^^^^^^^^^^^^^^
    |                                              |
    |                                              the parameter type `C` must be valid for the static lifetime...
    |                                              ...so that the type `C` will meet its required lifetime bounds
    |
help: consider adding an explicit lifetime bound
    |
180 |         C: Fn(&i32) -> i32 + 'static, // 要求闭包是 'static 的,即闭包的参数和捕获的借用都必须是 'static
    |                            +++++++++
*/

// 解决办法:
impl BasicRouter {
    // 要求闭包是 'static 的,即闭包返回借用类型值、捕获的借用都必须是 'static,否则将所有权转移到 Box 中是不安全的
    fn add_route<C>(&mut self, url: &str, callback: C) where C: Fn(&i32) -> i32  + 'static
      {
          self.routers.insert(url.to_string(), Box::new(callback));
      }
}

async fn 的 Fn* 限界不支持 HRTB
#

同步函数 fn 的 Fn* 限界支持 HRTB(而且是默认行为),但是 async fn 异步函数的 Fn* 限界不支持 HRTB:

async fn for_each_city<F, Fut>(mut f: F)
where
    // 错误:不支持 HRTB, 所以这里 Rust 为包含引用的限界自动添加的 HRTB 无效:F: for<'c> FnMut(&'c str) -> Fut,
    F: FnMut(&str) -> Fut,
    Fut: Future<Output = ()>,
{
    for x in ["New York", "London", "Tokyo"] {
        f(x).await;
    }
}

async fn do_something2(city_name: &str) { todo!() }

async fn main() {
    for_each_city(do_something2).await;
}

/*
error[E0308]: mismatched types
   --> src/main.rs:101:5
    |
101 |     for_each_city(do_something2).await;
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
    |
    = note: expected opaque type `impl for<'c> Future<Output = ()>`  // 期望的是一个 HRTB 的高阶 lifetime
               found opaque type `impl Future<Output = ()>` // 实际是编译器自动创建的一个具有特定 lifetime 的匿名类型
    = note: distinct uses of `impl Trait` result in different opaque types
*/

解决办法:使用 AsyncFn* 限界(Rust 1.75+ 开始支持),它支持 HRTB(具体参考:14-rust-lang-async.md):

async fn for_each_city<F>(mut f: F)
where
    F: AsyncFnMut(&str) -> (), // `AsyncFn*` 只适用于异步函数,它的返回值会被自动封装为 Future<T>,所以不需要像同步函数那样显式返回 Future
    // 等效于:
    //  F: for<'a> AsyncFnMut(&'a str) -> (),
{
    for x in ["New York", "London", "Tokyo"] {
        f(x).await;
    }
}

async fn do_something2(city_name: &str) { todo!() }

async fn main() {
    for_each_city(do_something2).await;
}

闭包定义不支持 lifetime 标记和 HTRB
#

定义闭包时,Rust 不支持定义 lifetime(如,不支持 |x: &'a i32| -> &'a i32 { x }),不支持 HRTB liftime 自动推导,不支持自动应用 lifetime-elision 规则,这会带来一系列问题:

  1. 如果闭包的输入、输出都包含借用类型参数,由于不能显式表达输出的 lifetime 不能比输入的 lifetime 长的约束关系,会导致编译报错;

  2. 如果闭包通过借用捕获上下文对象,同时闭包的输入或输出包含借用类型参数,由于不能显式表达闭包借用捕获的对象的 lifetime 不能比与输入、输出借用对象的 lifetime 短的约束关系,也会导致编译报错;

特殊情况:闭包只是输入包含借用,但输出不包含借用(或者为 'static) 且闭包没有通过借用捕获上下文对象,这种情况不存在 lifetime 间的潜在关系约束,故不会编译报错。

// 错误:同步闭包不支持 HRTB
// 返回的 Combined 也是有 lifetime 的,但是它的 lifetime 和 a、b 之间约束缺少定义,所以编译器报错。
// 如果只是纯输入有借用,但是输出没有借用(或为 'static) 且闭包内部没有借用方式捕获上下文,则是 OK 的。
let mut closure = |a: &i32, b: &str|  {Combined { num: a, text: b }}

解法:将闭包转换为 fn 函数,fn 函数的签名支持 HRTB 和 lifetime 标记约束(见后文) 。 而且对于 Fn* 限界的函数参数,也支持传入 fn 函数

闭包定义的原理和 lifetime 问题分析
#

闭包定义本质是编译器生成的一个匿名类型对象,如果通过借用捕获上下文对象,或则输入、输出包含借用,编译器都会给生成的匿名对象添加 lifetime 标记,但并没有定义它们之间的约束关系,从而可能导致编译失败。

闭包在 HIR 中被 desugar 成:

  • 一个匿名的 struct(用于保存捕获环境),可能包含捕获的环境借用的周期定义 ‘a;
  • 对应的 impl Fn/FnMut/FnOnce trait 实现,可能包含是输入、输出生命周期定义 ‘b,‘c;
  • 闭包体变成 call 方法的函数体。

背景:FnOnce/FnMut/Fn trait 的定义如下:

pub trait FnOnce<Args> where Args: Tuple,
{
    type Output; // trait 关联类型定义了闭包的返回值类型

    // Required method
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> where Args: Tuple,
{
    // Required method
    extern "rust-call" fn call_mut(&mut self, args: Args,) -> Self::Output;
}

pub trait Fn<Args>: FnMut<Args> where  Args: Tuple,
{
    // Required method
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

如果闭包的输入、输出包含借用类型参数,或通过借用(&T/&mut T)方式捕获了上下文对象,则生成的匿名类型会包含生命周期的定义,关联类型 Output 的生命周期和闭包输出值生命周期一致。

示例 1: 闭包通过借用捕获上下文对象时编译报错:

let mut fields: Vec<&str> = Vec::new();
let pusher = |a: &str| {
    // 闭包通过 &mut 可变借用补货了 fields
    fields.push(a);
};

/*
error[E0521]: borrowed data escapes outside of closure
   --> src/main.rs:110:9
    |
108 |     let mut fields: Vec<&str> = Vec::new();
    |         ---------- `fields` declared here, outside of the closure body
109 |     let pusher = |a: &str| {
    |                   - `a` is a reference that is only valid in the closure body
110 |         fields.push(a);
    |         ^^^^^^^^^^^^^^ `a` escapes the closure body here
    |
    = note: requirement occurs because of a mutable reference to `Vec<&str>`
    = note: mutable references are invariant over their type parameter
    = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
*/

HIR 实现展开:

  1. 编译器为闭包定义生成一个匿名闭包结构体,同时包含借用捕获的生命周期定义 ‘a:
  2. 编译器该该匿名结构体实现 FnMut,由于闭包输入包含借用,所以包含借用参数的生命周期定义 ‘b;
// ‘a 为捕获的上下文对象借用 fields 的生命周期
struct Closure<'a> {
    fields: &'a mut Vec<&'a str>, // 捕获的变量(推断出的借用方式)
}

// 闭包的实现:'a 为捕获的上下文对象借用 fields 的生命周期,'b 为传入的对象 str 的生命周期,但是并没有声明 'a 和 'b 之间的约束关系 'b: 'a。
impl<'a, 'b> FnMut<(&'b str,)> for Closure<'a> {
    type Output = ()

    extern "rust-call" fn call_mut(&mut self, (a,): (&'b str,)) -> () {
        self.fields.push(a);
    }
}

这里关于 lifetime 的矛盾点:

  1. 编译器自动标注的两个生命周期:‘a 是闭包捕获的 fields 借用对象的生命周期,‘b 是调用闭包时传入参数对象的生命周期;
  2. 由于将 str 存入 fields,所以 ‘b 的声明周期不能比 ‘a 短,也即两者需要满足约束 'b: 'a;
  3. 但是 'b: 'a 这个关系并没有显式明确定义,所以拒绝编译;

示例 2:闭包返回借用时的生命周期问题:

let mut fields: Vec<&str> = Vec::new();
let pusher = |a: &str| {fields.push(a); a};

// 报错:
/*
error: lifetime may not live long enough
   --> src/main.rs:111:9
    |
109 |     let pusher = |a: &str| {
    |                      -   - return type of closure is &'2 str
    |                      |
    |                      let's call the lifetime of this reference `'1`
110 |         fields.push(a);
111 |         a
    |         ^ returning this value requires that `'1` must outlive `'2`
*/

// 类似的例子:
fn fn_elision(x: &i32) -> &i32 { x } // 函数 OK,`lifetime-elision` 会自动将输出的 lifetime 设置为和输入一致

// 编译器将闭包函数 lifetime 标记为(注意:闭包函数不适用于普通函数的 lifetime elision 规则):|x: &'a i32| -> &'b i32 { x }
// 这里要求 'a: 'b, 但是这个关系并没有明确定义,所以拒绝编译;
let closure_elision = |x: &i32| -> &i32 { x };
// |     let closure = |x: &i32| -> &i32 { x };
// |                       -        -      ^ returning this value requires that `'1` must outlive `'2`
// |                       |        |
// |                       |        let's call the lifetime of this reference `'2`
// |                       let's call the lifetime of this reference `'1`

HIR 展开:编译器生成的闭包结构体,捕获 &mut fields, 编译器会把闭包 desugar 成一个匿名结构体 + FnMut 实现。伪代码大致如下:

struct Closure<'a> {
    fields: &'a mut Vec<&'a str>,
}

impl<'a, 'b> FnMut<(&'b str,)> for Closure<'a> {
    type Output = &'b str;  // Output 为闭包的返回值类型,返回值是传入的引用本身,所以它的生命周期推导为 'b

    extern "rust-call" fn call_mut(&mut self, (a,): (&'b str,)) -> Self::Output {
        self.fields.push(a);
        a
    }
}

这里有两个生命周期的问题:

  • ‘a → fields 绑定的生命周期,Vec<&‘a str> 里存的元素必须活得至少和 ‘a 一样久。
  • ‘b → 闭包参数 a: &‘b str 的生命周期(调用时局部)。
  • 返回值类型是 &‘b str,这是没问题的, 问题出在 fields.push(a):它要求 &‘a str,但 a 的类型是 &‘b str,编译器需要证明 ‘b: ‘a,却无法保证。

综上:

  1. 闭包定义不支持 lifetime 标记和 HRTB;
  2. 如果闭包通过借用捕获了上下文对象,则该闭包匿名类型对象也捕获和具有对应的生命周期(大概率不是 ‘static);
  3. 如果闭包的输入参数、返回值或捕获的对象包含借用,则它们的生命周期间约束,由于不能显式定义,所以编译器报错;

问题示例 3:闭包在第一次被调用时根据输入值类型情况被实例化,后续再次调用该闭包时传入的参数 liftime 需要是实例化推导的 lifetime 的子类型(这样才能赋值):

// https://rust-lang.github.io/rfcs/3216-closure-lifetime-binder.html
use std::cell::Cell;

fn main() {
    let static_cell: Cell<&'static u8> = Cell::new(&25);
    let closure = |s| {};
    // 第一次调用 closure(static_cell) 时将它的类型推导为 `|s: Cell<&'static u8>|`
    closure(static_cell);
    
    let val = 30;
    let short_cell: Cell<&u8> = Cell::new(&val);
    // 后续不能再传入 `Cell<&'0 u8>`, 因为 '0 的 lifetime 比 'static 短。
    closure(short_cell);
}
/*
error[E0597]: `val` does not live long enough
  --> src/main.rs:8:43
   |
4  |     let static_cell: Cell<&'static u8> = Cell::new(&25);
   |                      ----------------- type annotation requires that `val` is borrowed for `'static`
...
8  |     let short_cell: Cell<&u8> = Cell::new(&val);
   |                                           ^^^^ borrowed value does not live long enough
9  |     closure(short_cell);
10 | }
   | - `val` dropped here while still borrowed
*/

闭包定义的 lifetime 问题解法
#

  1. 使用 nightly toolchain 和开启 #![feature(closure_lifetime_binder)],可以为闭包函数指定 for <'a> 语法的 lifetime,https://github.com/rust-lang/rust/issues/97362
  2. 或者,定义一个 helper 函数,可以指定闭包输入、输出参数所需的 lifetime,内部定义闭包时使用该 lifetime 标记;
  3. 或者,将闭包转换为 fn 函数指针,函数指针支持使用 for<'a> HRTB 来定义高阶函数,https://stackoverflow.com/a/60906558

解决办法 1 : 使用 #![feature(closure_lifetime_binder)]

fn main() {
    let clouse_test = |input: &String| input;
    // 问题 1:由于输出是 input,也是借用,所以等效为:
    // let clouse_test = |input: &String| -> &String input;
    // 但是 lifetime-elide rule 不适用于闭包,所以输入和输出参数的借用的 lifetime 关系并没有被推导和定义,从而报错。
    /*
    error: lifetime may not live long enough
       --> src/main.rs:108:40
        |
    108 |     let clouse_test = |input: &String| input;
        |                               -      - ^^^^^ returning this value requires that `'1` must outlive `'2`
        |                               |      |
        |                               |      return type of closure is &'2 String
        |                               let's call the lifetime of this reference `'1`
    */

    let clouse_test = |input: &String| -> &String {input};
    /*
    error: lifetime may not live long enough
       --> src/main.rs:108:53
        |
    108 |     let clouse_test = |input: &String| -> &String { input };
        |                               -           -         ^^^^^ returning this value requires that `'1` must outlive `'2`
        |                               |           |
        |                               |           let's call the lifetime of this reference `'2`
        |                               let's call the lifetime of this reference `'1`
    */

    // 问题 2:闭包不支持定义 'lifetime
    let clouse_test = |input: &'a String| ->&'a String {input};
    /*
    error[E0261]: use of undeclared lifetime name `'a`
       --> src/main.rs:108:47
        |
    107 | async fn main() -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
        |              - help: consider introducing lifetime `'a` here: `<'a>`
    108 |     let clouse_test = |input: &'a String| -> &'a String { input };
        |                                               ^^ undeclared lifetime
    */

    // OK: 使用 nightly toolchain 和开启 #![feature(closure_lifetime_binder)] 后可以使用 HRTB 为闭包定义 lifetime
    let clouse_test = for <'a> |input: &'a String| ->&'a String {input};
}

解决办法 2 : 闭包使用外围 helper 函数定义的 lifetime,闭包定义可以使用外围定义的 lifetime 标记:

fn testStr<'a> (input: &'a String) -> &'a String {
    let closure_test = |input: &'a String | -> &'a String {input};
    return closure_test(input);
}

解决办法 3(建议!): 将闭包转换为 fn 函数指针,fn 函数指针支持 HRTB,也支持明确定义输入和输出的 lifetime 约束(但不能捕获上下文对象,需要将它们作为参数传入):

// fn 函数指针支持 HRTB
let test_fn: for<'a> fn(&'a _) -> &'a _ = |p: &String| p;
println!("Results:{}", test_fn(&"asdfab".to_string()));

// 更复杂的例子:
// 返回的结构体持有输入的两个借用
#[derive(Debug)]
struct Combined<'a, 'b> {
    num: &'a i32,
    text: &'b str,
}

// 下面的闭包定义会出错:因为返回的 Combined 的 lifetime 缺少和输入参数 a、b 的 lifetime 的约束关系
//  let mut closure = |a: &i32, b: &str|  {Combined { num: a, text: b }}

// 方案 1: 使用函数指针
fn apply_fn_ptr<'a, 'b>(
    f: fn(&'a i32, &'b str) -> Combined<'a, 'b>,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b> {
    f(x, y)
}

// 方案 2: 不使用 HRTB
fn apply_fnmut_simple<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b>
where
    F: FnMut(&'a i32, &'b str) -> Combined<'a, 'b>,
{
    f(x, y)
}

// 方案 3: 使用 HRTB
fn apply_fnmut_simple_hrtb<'a, 'b, F>(
    mut f: F,
    x: &'a i32,
    y: &'b str,
) -> Combined<'a, 'b>
where
    F: for<'x, 'y> FnMut(&'x i32, &'y str) -> Combined<'x, 'y>,
{
    f(x, y)
}

// 辅助函数,用于方案 1 - 显式指定返回值 Combined 和输入参数的生命周期
fn make_combined<'a, 'b>(num: &'a i32, text: &'b str) -> Combined<'a, 'b> {
    Combined { num, text }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
    let n = 42;
    let s = String::from("hello");

    println!("=== 方案 1: 使用函数指针 ===");
    let result1 = apply_fn_ptr(make_combined, &n, &s);
    println!("Result1: {:?}", result1);

    println!("\n=== 方案 2a: 使用局部函数(推荐) ===");
    // 在函数内部定义函数,也是显式的定义返回值 Combined 和输入参数的生命周期
    fn local_make_combined<'a, 'b>(a: &'a i32, b: &'b str) -> Combined<'a, 'b> {
        Combined { num: a, text: b }
    }
    let result2a = apply_fnmut_simple(local_make_combined, &n, &s); // 传入内部函数
    println!("Result2a: {:?}", result2a);
    let result2a = apply_fnmut_simple(make_combined, &n, &s);  // 传入外部函数
    println!("Result2a: {:?}", result2a);
    // 对于 HRTB 闭包限界,也可以传入 fn 函数
    let result2a = apply_fnmut_simple_hrtb(make_combined, &n, &s);
    println!("Result2a: {:?}", result2a);

    Ok(())
}

下面这些例子,闭包的输入参数 _x 都没有使用,所以即使为它指定 'lifetime 也没有影响:

// 其它例子:https://github.com/rust-lang/rust/pull/56746/files
fn willy_no_annot<'w>(p: &'w str, q: &str) -> &'w str {
    let free_dumb = |_x| { p }; // no type annotation at all

    let hello = format!("Hello");
    free_dumb(&hello)
}

fn willy_ret_type_annot<'w>(p: &'w str, q: &str) -> &'w str {
    // type annotation on the return type
    let free_dumb = |_x| -> &str { p };

    let hello = format!("Hello");
    free_dumb(&hello)
}

fn willy_ret_region_annot<'w>(p: &'w str, q: &str) -> &'w str {
    // type+region annotation on return type
    let free_dumb = |_x| -> &'w str { p };

    let hello = format!("Hello");
    free_dumb(&hello)
}

fn willy_arg_type_ret_type_annot<'w>(p: &'w str, q: &str) -> &'w str {
    // 如果闭包返回的是 _x, 则会报错。
    // type annotation on arg and return types
    let free_dumb = |_x: &str| -> &str { p };

    let hello = format!("Hello");
    free_dumb(&hello)
}

fn willy_arg_type_ret_region_annot<'w>(p: &'w str, q: &str) -> &'w str {
    // 如果闭包返回的是 _x, 则会报错。
    let free_dumb = |_x: &str| -> &'w str { p }; // fully annotated

    let hello = format!("Hello");
    free_dumb(&hello)
}

闭包的 Send+‘static 问题
#

前面讨论过闭包的编译器实现,即 Rust 编译器将闭包定义转换为匿名类型对象,同时为该匿名类型实现某个 Fn/FnOnce/FnMut trait。如果闭包通过 &T/&mut T 捕获了上下文对象,或者闭包的输入或输出包含借用,则该匿名类型还包含 lifetime 类型参数:

// 闭包定义示例:
let mut fields: Vec<&str> = Vec::new();
let pusher = |a: &str| {fields.push(a); a}; // pusher 匿名类型对象具有 lifetime(不是 'static)

// 对应的编译器生成的匿名对象示例:
// 1. 匿名类型包含 lifetime
struct Closure<'a> {
    fields: &'a mut Vec<&'a str>,
}

// 2. 在实现 FnMut 时,参数和返回值(关联类型 Output)都具有 lifetime
impl<'a, 'b> FnMut<(&'b str,)> for Closure<'a> {
    type Output = &'b str;  // Output 为闭包的返回值类型,返回值是传入的参数,所以它的生命周期推导为 'b

    extern "rust-call" fn call_mut(&mut self, (a,): (&'b str,)) -> Self::Output {
        self.fields.push(a);
        a
    }
}

在多线程场景中,提交的闭包类型对象必须实现 Send + 'static,闭包返回值也必须实现 Send+'static:

// std::thread::spawn() 函数的闭包和返回值都需要实现 Send 和 'static
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static, // 闭包整体 FnOnce() -> T 需要实现 Send + 'static
    T: Send + 'static, // 闭包返回值的要求

对于闭包整体限界 Send + 'static 的理解:

  1. Rust 在定义闭包时,即创建一个实现 Fn/FnMut/FnOnce trait匿名类型对象(如 impl Fn*) ,该对象可能通过借用捕获了上下文对象,所以该匿名类型对象本身是具有 lifetime 约束的(比如它的 lifetime 比所捕获的对象长 ),而上面的 'static 则要求该匿名类型对象是可以在程序运行过程中一直存在,所以一般不能通过借用捕获上下文(需要 move)。而 Send 则要求该闭包匿名对象可以在多个线程间转移,所以 move 捕获的对象也必须实现 Send,例如不能是 Rc。(但是线程内创建的 Rc 不受影响,因为它只能在所在线程运行 )。

  2. 对于异步闭包的限界场景,进一步要求闭包内跨 .await 的对象都需要实现 Send,所以异步闭包的输入参数(如 &T/&mut T)、内部创建的对象、捕获的对象都需要实现 Send,该闭包才从整体上实现 Send。

Send: 表示(闭包匿名类型)对象可以在多线程间安全转移,这要求:

  • 通过 move 捕获&转移的对象不能是未实现 Send 的 Rc、裸指针等类型;
    • &mut T 可变借用捕获的变量由于是独占性访问,所以实现了 Send;
    • &T 共享捕获的变量是否实现 Send,取决于 T 是否实现 Sync,所以 T 是 Rc 和内部可变性类型 Cell/RefCell 时,&T 没有实现 Send;

‘static: 表示(闭包匿名类型)对象在程序运行期间可以一直有效,这是因为该闭包对象可能被转移到其它线程中运行,而执行时机是不确定的,在执行时该闭包对象可能已经脱离了创建它的 block 上下文。

  • 这要求闭包通过 &T/&mut T 借用捕获的对象也必须是 'static 的,一般很难满足,故一般使用 move 闭包类型,将对象的所有权转移到闭包。
  • 另外,闭包的输出参数包含借用时,该闭包对象也是有 lifetime 的,一般在编译时会直接报错,也很难满足 'static 要求。

注意:

  1. 闭包的输入参数包含 &T/&mut T 借用时,不影响闭包匿名类型整体的 ‘static 和 Send 特性,因为闭包输入参数是在执行该闭包时传入的,而闭包匿名类型在此之前已经创建。
  2. 闭包内部创建的对象,如 Rc,不影响闭包整体是否实现 Send,因为闭包作为一个整体只会被一个线程执行,所以内部对象不会有跨现场的场景。(但是异步闭包内部创建的对象会影响整体是否实现 Send)。
// 错误的情况:
use std::rc::Rc;
use std::thread;

fn main() {
    let rc = Rc::new(42);
    thread::spawn(move || {
        println!("{}", rc);
    });
}
/*
error[E0277]: `Rc<i32>` cannot be sent between threads safely
   --> src/main.rs:6:5
    |
6   |     thread::spawn(move || {
    |     ^^^^^^^^^^^^^ `Rc<i32>` cannot be sent between threads safely
    |
    = help: within `impl FnOnce() + Send`, the trait `Send` is not implemented for `Rc<i32>`
*/


// OK 的情况:
thread::spawn(move || {
    let rc = Rc::new(42);
    println!("{}", rc);
});

注意:move 会把捕获的变量转移进闭包,但如果捕获的是 &T, 则闭包内部获得的还是一个非 'static 的借用,还是有生命周期问题:

use std::thread;

let people = vec![
    "Alice".to_string(),
    "Bob".to_string(),
    "Carol".to_string(),
];

let mut threads = Vec::new();

for person in &people {
    threads.push(thread::spawn(move || {
        // person 是 &String 类型,所以 move 捕获的是 &String 而非 String
        println!("Hello, {}!", person);
    }));
}

for thread in threads {
    thread.join().unwrap();
}

// 报错:
/*
error[E0597]: `people` does not live long enough
  --> src/main.rs:12:20
   |
12 |     for person in &people {
   |                    ^^^^^^ borrowed value does not live long enough
...
21 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...
*/

解决办法:

// 方法 1:直接转移所有权
for person in people {
    threads.push(thread::spawn(move || {
        println!("Hello, {}!", person);
    }));
}

// 方法 2:克隆每个元素
for person in &people {
    let person = person.clone();
    threads.push(thread::spawn(move || {
        println!("Hello, {}!", person);
    }));
}

Fn* 类型的 trait object
#

Fn/FnMut/FnOnce 都是 trait,当作为函数输入参数值或返回值时(而非泛型参数限界),需要使用 trait object 类型,如 &dyn Trait, Box<dyn Trait>impl Trait

// 错误:Fn(i32) -> i32:是 trait 类型,是 unsize 大小,所以不能直接返回
// fn returns_closure() -> Fn(i32) -> i32 {
//     |x| x + 1
// }

// 正确:返回用 Box 封装的 trait object,具有 2 usize 的固定大小
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

// 正确:返回 impl Trait 类型对象,它是编译器自动生成的匿名类型对象(编译时静态分发,而 trait object 是运行时动态分发)
fn create_fn() -> impl Fn() {
    let text = "Fn".to_owned(q);
    move || println!("This is a: {}", text)
}

fn create_fnmut() -> impl FnMut() {
    let text = "FnMut".to_owned();
    move || println!("This is a: {}", text)
}

fn create_fnonce() -> impl FnOnce() {
    let text = "FnOnce".to_owned();
    move || println!("This is a: {}", text)
}

// 问题:C 只能保存一种固定的闭包类型
struct BasicRouter<C> where C: Fn(&Request) -> Response {
    routers: HashMap<String, C>;
}

// 解决办法:使用 trait object
type BoxedCallback = Box<dyn Fn(&Request) -> Response>;
struct BasicRouter{
    routers: HashMap<String, BoxedCallback>;
}

trait object 默认没有实现 Send/Sync/Unpin,在多线程和异步场景,需要显式的标记:参考 10-rust-lang-generic-trait.md

发散函数
#

发散函数(Diverging functions) 指的是不返回的函数, 如 loop,panic!() 或退出的函数 os.exit()/os.abort() 等:

  fn foo() -> ! {
      panic!("This call never returns.");
  }

  fn loop_forever() -> ! {
      loop {
          println!("I will loop forever!");
      }
  }

! 也可以作为类型:

#![feature(never_type)]
fn main() {
    let x: ! = panic!("This call never returns.");
    println!("You will never see this line!");
}

对比: unit type 有唯一值和类型 ():

fn some_fn() {
    // 未指定返回值,等效为返回 ();
    ()
}

可变参数
#

Rust 函数不支持可变长度参数(variadic arguments),但通过函数宏可以模拟:

macro_rules! sum {
    ($($x:expr),*) => {
        {
            let mut total = 0;
            $(
                total += $x;
            )*
            total
        }
    };
}

fn main() {
    let result = sum!(1, 2, 3, 4);
    println!("{}", result);
}

Rust 仅在 extern "C" 中有限支持 C 风格的可变长度参数:

extern "C" {
    fn printf(format: *const i8, ...) -> i32;
}
rust-lang - 这篇文章属于一个选集。
§ 9: 本文

相关文章

1. 标识符和注释:identify/comment
·
Rust 标识符介绍
10. 泛型和特性:generic/trait
·
Rust 泛型和特性
11. 类型协变:type coercion
·
Rust 高级话题:子类型和类型协变
12. 迭代器:iterator
·
Rust 迭代器