Generic Types

Rust 中的泛型跟 TS 里的理念大致相同,如果有学过 TS 的应该相对容易理解一些。在 TS 中可以声明泛型为某类型的超集( <T extends XXX> ),Rust 中也行,不过 Rust 中的写法是 T: XXX 的方式,这个 XXX 就是后面讲到的 Trait 。

类型推导是泛型的基本能力,比如下面 structs 中的例子,就能推导出 integer 中的 T 是 i32 ,U 是 i32 ,float 的 T 是 i32 ,U 是 f32。

  1. struct Point<T, U> {
  2. x: T,
  3. y: U,
  4. }
  5. fn main() {
  6. let integer = Point { x: 5, y: 10 };
  7. let float = Point { x: 1, y: 4.0 };
  8. }

可以在函数中定义泛型,比如在前面错误处理里定义的带回调函数的函数,使用的时候就能自动推导出 T 是 u32 ,所以 r2 的类型就是 u32 了。

  1. fn main() {
  2. let r2 = test_fn_once(|str| {
  3. let u: u32 = str.parse().expect("failed to parse to number");
  4. u
  5. });
  6. println!("{}", r2);
  7. }
  8. fn test_fn_once<T, F: Fn(String) -> T> (f: F) -> T {
  9. f(String::from("123"))
  10. }

Enum 也一样,比如 Option<T>Result<T, E>

还有 impl 的时候也可以定义泛型( impl structs 或者 impl enum 都可以 )

  1. struct Point<T> {
  2. x: T,
  3. y: T,
  4. }
  5. impl<T> Point<T> {
  6. fn x(&self) -> &T {
  7. &self.x
  8. }
  9. }
  10. fn main() {
  11. let p = Point { x: 5, y: 10 };
  12. println!("p.x = {}", p.x());
  13. }

impl 定义泛型的时候,还有很有意思的 feature ,就是可以指定当类型定义方法,比如基于上面的代码,可以再加个 impl ,在这个 impl 中指定泛型 T 为 f32 的时候就拓展一个 sqrt 方法。

可以 impl 同个 struct 拓展多次,最终会合并,有点类似于 ts 里的 declaration merging ?

  1. // ..snip..
  2. impl Point<f32> {
  3. fn sqrt(&self) -> f32 {
  4. (self.x.powi(2) + self.y.powi(2)).sqrt()
  5. }
  6. }
  7. fn main() {
  8. let p: Point<f32> = Point { x: 1.0, y: 2.0 };
  9. println!("sqrt = {}", p.sqrt());
  10. }

Traits

Rust 中的 trait 是类似于其他语言中的 interface ,可以被 implement 。通过 impl ... for 的语法可以按照 trait 定义给 struct 实现方法,trait 中的方法也可以有默认值,如果有默认值的话,就可以不实现。

  1. struct Man {
  2. name: String,
  3. }
  4. trait CanSay {
  5. fn say_hi(&self) -> ();
  6. fn say_yo(&self) {
  7. println!("yo!");
  8. }
  9. }
  10. impl CanSay for Man {
  11. fn say_hi(&self) -> () {
  12. println!("hi, {}", self.name);
  13. }
  14. }
  15. fn main() {
  16. let m = Man { name: String::from("axes") };
  17. m.say_hi();
  18. m.say_yo();
  19. }

定义函数的时候,也可以约束入参是需要实现了 trait 的 struct ,两种写法,一种是 &impl XXX ,还有一种是使用泛型的方式 &T

&T 这种写法算是一种语法糖,Rust 把这种写法叫做 trait bound,用来在某种场景下减少重复代码,比如多个参数都需要 impl XXX 的情况下,就可以把 XXX 写到泛型,然后每个参数都用 &T 就好了。

  1. // ...snip...
  2. fn say_hi(m: &impl CanSay) {
  3. m.say_hi();
  4. }
  5. fn say_yo<T: CanSay>(m: &T) {
  6. m.say_yo();
  7. }
  8. fn main() {
  9. let m = Man { name: String::from("axes") };
  10. m.say_hi();
  11. m.say_yo();
  12. say_hi(&m);
  13. say_yo(&m);
  14. }

同个 struct 可以 impl 多个 trait ,函数入参的约束也可以约束 struct 必须要 impl 了指定的多个 trait ,可以通过 + 操作符实现类型合并的效果( 类似于 TS 中的交叉类型 )。比如我有个 trait A 和一个 trait B ,那么就可以通过 A + B 的方式来合并两个 trait 。

运用到入参的 trait 声明中也是有多种写法,一种是 &(impl A + B),一种是通过泛型 T: A + B,还有一种是用 where

  1. trait CanWalk {
  2. fn walk(&self) {
  3. println!("walk walk walk");
  4. }
  5. }
  6. impl CanWalk for Man {}
  7. fn say_yo_and_walk(m: &(impl CanSay + CanWalk)) {
  8. m.say_yo();
  9. m.walk();
  10. }
  11. fn say_hi_and_walk<T: CanSay + CanWalk>(m: &T) {
  12. m.say_hi();
  13. m.walk();
  14. }
  15. fn say_hi_and_walk_2<T>(m: &T)
  16. where T: CanSay + CanWalk
  17. {
  18. m.say_hi();
  19. m.walk();
  20. }

返回类型也可以约束

  1. fn should_return_walk() -> impl CanWalk {
  2. Man { name: String::from("111") }
  3. }
  4. fn should_return_say_walk() -> impl CanWalk + CanSay {
  5. Man { name: String::from("111") }
  6. }

impl 的时候也可以使用 trait 合并的方式来定义

  1. use std::fmt::Display;
  2. struct Pair<T> {
  3. x: T,
  4. y: T,
  5. }
  6. impl<T> Pair<T> {
  7. fn new(x: T, y: T) -> Self {
  8. Self { x, y }
  9. }
  10. }
  11. impl<T: Display + PartialOrd> Pair<T> {
  12. fn cmp_display(&self) {
  13. if self.x >= self.y {
  14. println!("The largest member is x = {}", self.x);
  15. } else {
  16. println!("The largest member is y = {}", self.y);
  17. }
  18. }
  19. }

最后来分析一个使用标准库中的 trait 的案例,PartialOrd 代表是可以用来比大小的数据,Copy 则代表是固定存储在 Stack 中的数据( 即大小固定的,详见 ownership 那章 )比如整型、布尔类型等。

因为 largest函数中存在对列表里的子元素进行比大小的操作,所以需要 impl PartialOrd ,又因为 list[0] 这个逻辑需要将列表中的值直接读出来,所以需要是 impl Copy 的数据( 如果是 heap 的那种数据,需要使用引用 )。

  1. fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
  2. let mut largest = list[0];
  3. for &item in list {
  4. if item > largest {
  5. largest = item;
  6. }
  7. }
  8. largest
  9. }
  10. fn main() {
  11. let number_list = vec![34, 50, 25, 100, 65];
  12. let result = largest(&number_list);
  13. println!("The largest number is {}", result);
  14. let char_list = vec!['y', 'm', 'a', 'q'];
  15. let result = largest(&char_list);
  16. println!("The largest char is {}", result);
  17. }

如果不用 Copy,那么就用引用也可以。

  1. fn largest<T: PartialOrd>(list: &[T]) -> &T {
  2. let mut largest = &list[0];
  3. for item in list {
  4. if item > largest {
  5. largest = item;
  6. }
  7. }
  8. largest
  9. }

Lifetimes

lifetime 是啥

Rust 中变量 valid 的时间段会被称为 lifetime ,编译器中有个 borrow checker ,就会根据这个 lifetime 来判断当前的引用是否合法的,文档中有两个例子可以很好的说清楚 lifetime 是啥

  1. {
  2. let r; // ---------+-- 'a
  3. // |
  4. { // |
  5. let x = 5; // -+-- 'b |
  6. r = &x; // | |
  7. } // -+ |
  8. // |
  9. println!("r: {}", r); // |
  10. } // ---------+

其中 'a'b 就是表示的两个变量的 lifetime ,前者是 r 的,后者是 x 的,因为 x 定义在了小的 scope 里,而 r 是在外面的 scope ,所以 r 的 lifetime 是比 x 的 lifetime 长的,当 x 的 lifetime 结束的时候,又有 println r 的操作的时候就会有问题,因为此时 r 的 lifetime 未结束但是 x 是已经结束了。

而下面这种就合法了,因为 x 的 lifetime 是比 r 长的,所以 r 赋值 x 的引用是合法的,因为当 r 还是 valid 的时候,x 肯定是 valid ( 因为跳出 scope 才会 invalid )。

  1. {
  2. let x = 5; // ----------+-- 'b
  3. // |
  4. let r = &x; // --+-- 'a |
  5. // | |
  6. println!("r: {}", r); // | |
  7. // --+ |
  8. } // ----------+

所以 lifetime 的判断逻辑就是:引用的 lifetime 需要小于或等于被引用的变量的 lifetime 。

使用场景

大部分场景下,Rust 可以自己推断出各个变量的 lifetime ,但是有部分场景不能推断出来,就需要开发者通过声明 lifetime 的方式告诉 Rust 的 borrow checker,否则 borrow checker 会报错,比如下面这段简单的示例代码

  1. fn longest(x: &str, y: &str): &str {
  2. if (x.len() > y.len()) {
  3. x
  4. } else {
  5. y
  6. }
  7. }

由于函数的返回类型是引用,Rust 的编译器不知道这个引用的 lifetime 是啥,( 前面的例子中是直接通过赋值的方式 x = &y 所以编译器很清晰的知道 x 依赖 y 的 lifetime ,但是这里 Rust 不知道 )。

所以就需要类似于声明依赖关系一样,通过 lifetime 泛型告诉编译器返回的引用的 lifetime 依赖 x 和 y 的 lifetime ,即只要 x 或者 y 中的任何一个 lifetime 结束,返回值的 lifetime 也就结束不能再使用( 标记为 invalid )

lifetime 的声明,感觉有点像历史包袱,大部分 lifetime 其实 Rust 自己应该能推导出来,当然这个需要 Rust 的持续完善,估计随着 Rust 的迭代,未来需要定义 lifetime 依赖关系的代码会越来越少。

  1. fn longest<'a> (x: &'a str, y: &'a str): &'a str {
  2. if (x.len() > y.len()) {
  3. x
  4. } else {
  5. y
  6. }
  7. }

再举个例子,比如下面这个,就表示返回的引用的 lifetime,就是依赖 x 的 lifetime 。

  1. fn longest<'a>(x: &'a str, y: &str) -> &'a str {
  2. x
  3. }

lifetime 的声明跟泛型一样,都可以作用于 struct impl 等,比如下面这个例子,也是申明 ImportantExcerpt 这个 struct 的 lifetime 是依赖其属性 part 这个引用的 lifetime 的。

  1. struct ImportantExcerpt<'a> {
  2. part: &'a str,
  3. }
  4. fn main() {
  5. let novel = String::from("Call me Ishmael. Some years ago...");
  6. let first_sentence = novel.split('.').next().expect("Could not find a '.'");
  7. let i = ImportantExcerpt {
  8. part: first_sentence,
  9. };
  10. }

真正写代码的时候,不需要过度去关注是否要声明 lifetime,一般来说当需要声明 lifetime 的时候,编译器也会提醒你加上,就提醒的时候再加上即可,只要理解 lifetime 是干嘛的就行。如果编译器能正常编译,说明编译器能推导出 lifetime ,自己也就没必要去加了。