这里有一段 Rust 的函数签名:

file

这是一段在 async_trait 下生成的代码,不得不吐槽 Rust 的这些特性太令人难懂了,新手学 Rust 肯定会被劝退。但这里不妨详细解释这段函数签名。

function

这是 Rust 里最简单的函数语法:

fn foo(id: Uuid) -> usize {
...
}

很明显上图里的东西要比这个复杂的多,我们一个一个来看。

self

在函数签名里有一个 &self 参数,它表示所在结构体的语法糖。如果你有其他面向对象的语言经验的话,应该很熟悉 class 和 class 下面定义方法的语法,然后在 new 出一个示例后可以通过 .(点)操作符来使用这个类下面的方法。

在 Rust 里类似。

struct Foo {}

impl Foo {
	pub fn get(&self) -> usize {
		...
	}
}

let foo = Foo{};
foo.get();

这个语法其实是通过 self 隐藏了一个自身的对象,我们把他展开就是这样的:

let foo = Foo{};

Foo::get(&foo);

从一个对象调用的形式变成了一个不同的函数调用的形式,函数签名中的 self,就表示一个函数所在的 struct 的对象,self 前面的 & 对这个对象的不可变借用。同理,self (获取了所有权的对象)还可以是 self&mut self(可变借用)。

...
fn get(&mut self) {}

let mut foo = Foo{};
Foo::get(&mut foo);

...
fn get(self {}

let foo = Foo {};
Foo::get(foo);
// get 获取了所有权,foo 失效

lifetime

在函数名后紧跟着的有两个泛型参数:

fn get_administractor_by_id<'life0, 'async_trait>(........

单引号开头的泛型是生命周期注解(生命周期也是泛型,但叫生命周期注解),生命周期用在借用上,用于告诉编译器这个借用会持续多长时间,编译器会保证借用在这个生命周期内有效,而不变成悬垂指针。

生命周期是复杂的概念,来慢慢解释。

借用

如果一个函数的入参有一个借用的参数:fn foo(input: &Foo) {},可以把 input 当作是一个指向 Foo 的指针,如果在 foo 执行中 Foo 被释放了,函数 foo 中的 input 参数就会变成一个悬垂指针,这是 Rust 所不允许的。幸好 Rust 提供了生命周期注解,我们给加上注解:

fn foo<'a>(input: &'a Foo) {}

单引号开头的泛型是生命周期,函数 foo 定义了一个生命周期 'a(你可以起任何名字),它表示函数 foo 的生命周期,而参数中 input: &'a Foo,借用参数被标记了 'a 生命周期,表示这个借用 input,必须在 'a 内有效,而 'a 表示函数 foo 的生命周期,所以也等于 input 必须在函数 foo 内有效。

让我们画张图。


let input = Foo;  // ------------------------
                  // ---                     |
foo(&input);      //    | foo 生命周期        | input 生命周期
                  //----                     |

上面是有效的,因为 input 存活的时间比 foo 长。

注解

好了我们来看图片里的生命周期 'life0, 'async_trait,表示函数定义了两个生命周期 'life0async_trait

先来看 'life0,标注在 &'life0 self 参数上,表示 &self 的存活时间必须比函数长。同时看 where 部分,where 部分用于泛型约束,它约束了生命周期 'life0'async_trait 的关系:'life0: 'async_trait,意思是生命周期 'life0 必须大于 'async_trait。然后看下一行约束:Self: 'async_trait,表示 Self 对象收到 'async_trait 生命周期约束,即是 Self 必须存活比 'async_trait 久。

最后一行的约束里看到 T 受到了 'static 的生命周期约束,'static 我们应该很熟悉,是表示全局生命周期,但是在这里不一定是表示该对象必须在整个应用程序间存活。

'static

'static 生命周期标注的对象表示在整个应用程序间存活。,如果你写过字面量字符串:let t: &'static str = "abc";,这个字符串受到 'static 约束,在整个应用程序间存活。但是当 'static 标注在泛型上时候却有另外的含义。

fn foo<T: 'static>(input: T) {}

上面的泛型 T 作为入参,同时受到 'static 约束,但是它不是表示 T 必须在整个应用程序间存活,我们可以在临时作用域中这样调用它:

fn main() {
    {
        let t = String::new();
        foo(t);
				// t drop here
    }
}

fn foo<T: 'static>(input: T) {}

很明显变量 t 并不能在整个应用程序间存活,但是受到 'static 约束的 T 仍然接收了这个 t。你可以认为 'static 标注的泛型表示获取了 T 的所有权。

稍微修改一下代码,向 'static 传递借用:

fn main() {
    // cli::run_cli(migration::Migrator).await;

    {
        let t = String::new();
        foo(&t);
    }
    println!("drop")
}

fn foo<T: 'static>(input: T) {}

编译器弹出了一个错误:

error[E0597]: `t` does not live long enough
  --> migration/src/main.rs:9:13
   |
8  |         let t = String::new();
   |             - binding `t` declared here
9  |         foo(&t);
   |         ----^^-
   |         |   |
   |         |   borrowed value does not live long enough
   |         argument requires that `t` is borrowed for `'static`
10 |     }
   |     - `t` dropped here while still borrowed

看起来好像是 t 的借用没有达到 'static 的要求,t 不是一个全局有效的静态变量,如果将 t 提升为一个静态变量 static t: String = String::new();,那么确实可以解决编译错误。

但是编译器给出的错误信息并不是唯一的解法,'static 约束的 T 也表示需要 T 的所有权,所以也可以直接将临时作用域中的对象所有权传递进去。

就像我们常使用的 Box,它有一个隐性的生命周期注解,Box<T: 'static>

回到图片中,函数的返回值里有一个 Box,它是持有一个动态分配的 Future 对象,并且这个对象受到 'async_trait 约束,这表示这个对象必须是受到 'async_trait 约束的借用(必须存活比 'async_trait 久),或者持有所有权。

待续

这一篇先写到这里,下一篇继续,会介绍 PinFuture