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。
struct Point<T, U> {x: T,y: U,}fn main() {let integer = Point { x: 5, y: 10 };let float = Point { x: 1, y: 4.0 };}
可以在函数中定义泛型,比如在前面错误处理里定义的带回调函数的函数,使用的时候就能自动推导出 T 是 u32 ,所以 r2 的类型就是 u32 了。
fn main() {let r2 = test_fn_once(|str| {let u: u32 = str.parse().expect("failed to parse to number");u});println!("{}", r2);}fn test_fn_once<T, F: Fn(String) -> T> (f: F) -> T {f(String::from("123"))}
Enum 也一样,比如 Option<T> 跟 Result<T, E> 。
还有 impl 的时候也可以定义泛型( impl structs 或者 impl enum 都可以 )
struct Point<T> {x: T,y: T,}impl<T> Point<T> {fn x(&self) -> &T {&self.x}}fn main() {let p = Point { x: 5, y: 10 };println!("p.x = {}", p.x());}
impl 定义泛型的时候,还有很有意思的 feature ,就是可以指定当类型定义方法,比如基于上面的代码,可以再加个 impl ,在这个 impl 中指定泛型 T 为 f32 的时候就拓展一个 sqrt 方法。
可以 impl 同个 struct 拓展多次,最终会合并,有点类似于 ts 里的 declaration merging ?
// ..snip..impl Point<f32> {fn sqrt(&self) -> f32 {(self.x.powi(2) + self.y.powi(2)).sqrt()}}fn main() {let p: Point<f32> = Point { x: 1.0, y: 2.0 };println!("sqrt = {}", p.sqrt());}
Traits
Rust 中的 trait 是类似于其他语言中的 interface ,可以被 implement 。通过 impl ... for 的语法可以按照 trait 定义给 struct 实现方法,trait 中的方法也可以有默认值,如果有默认值的话,就可以不实现。
struct Man {name: String,}trait CanSay {fn say_hi(&self) -> ();fn say_yo(&self) {println!("yo!");}}impl CanSay for Man {fn say_hi(&self) -> () {println!("hi, {}", self.name);}}fn main() {let m = Man { name: String::from("axes") };m.say_hi();m.say_yo();}
定义函数的时候,也可以约束入参是需要实现了 trait 的 struct ,两种写法,一种是 &impl XXX ,还有一种是使用泛型的方式 &T 。
&T这种写法算是一种语法糖,Rust 把这种写法叫做trait bound,用来在某种场景下减少重复代码,比如多个参数都需要impl XXX的情况下,就可以把 XXX 写到泛型,然后每个参数都用&T就好了。
// ...snip...fn say_hi(m: &impl CanSay) {m.say_hi();}fn say_yo<T: CanSay>(m: &T) {m.say_yo();}fn main() {let m = Man { name: String::from("axes") };m.say_hi();m.say_yo();say_hi(&m);say_yo(&m);}
同个 struct 可以 impl 多个 trait ,函数入参的约束也可以约束 struct 必须要 impl 了指定的多个 trait ,可以通过 + 操作符实现类型合并的效果( 类似于 TS 中的交叉类型 )。比如我有个 trait A 和一个 trait B ,那么就可以通过 A + B 的方式来合并两个 trait 。
运用到入参的 trait 声明中也是有多种写法,一种是 &(impl A + B),一种是通过泛型 T: A + B,还有一种是用 where
trait CanWalk {fn walk(&self) {println!("walk walk walk");}}impl CanWalk for Man {}fn say_yo_and_walk(m: &(impl CanSay + CanWalk)) {m.say_yo();m.walk();}fn say_hi_and_walk<T: CanSay + CanWalk>(m: &T) {m.say_hi();m.walk();}fn say_hi_and_walk_2<T>(m: &T)where T: CanSay + CanWalk{m.say_hi();m.walk();}
返回类型也可以约束
fn should_return_walk() -> impl CanWalk {Man { name: String::from("111") }}fn should_return_say_walk() -> impl CanWalk + CanSay {Man { name: String::from("111") }}
impl 的时候也可以使用 trait 合并的方式来定义
use std::fmt::Display;struct Pair<T> {x: T,y: T,}impl<T> Pair<T> {fn new(x: T, y: T) -> Self {Self { x, y }}}impl<T: Display + PartialOrd> Pair<T> {fn cmp_display(&self) {if self.x >= self.y {println!("The largest member is x = {}", self.x);} else {println!("The largest member is y = {}", self.y);}}}
最后来分析一个使用标准库中的 trait 的案例,PartialOrd 代表是可以用来比大小的数据,Copy 则代表是固定存储在 Stack 中的数据( 即大小固定的,详见 ownership 那章 )比如整型、布尔类型等。
因为 largest函数中存在对列表里的子元素进行比大小的操作,所以需要 impl PartialOrd ,又因为 list[0] 这个逻辑需要将列表中的值直接读出来,所以需要是 impl Copy 的数据( 如果是 heap 的那种数据,需要使用引用 )。
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {let mut largest = list[0];for &item in list {if item > largest {largest = item;}}largest}fn main() {let number_list = vec![34, 50, 25, 100, 65];let result = largest(&number_list);println!("The largest number is {}", result);let char_list = vec!['y', 'm', 'a', 'q'];let result = largest(&char_list);println!("The largest char is {}", result);}
如果不用 Copy,那么就用引用也可以。
fn largest<T: PartialOrd>(list: &[T]) -> &T {let mut largest = &list[0];for item in list {if item > largest {largest = item;}}largest}
Lifetimes
lifetime 是啥
Rust 中变量 valid 的时间段会被称为 lifetime ,编译器中有个 borrow checker ,就会根据这个 lifetime 来判断当前的引用是否合法的,文档中有两个例子可以很好的说清楚 lifetime 是啥
{let r; // ---------+-- 'a// |{ // |let x = 5; // -+-- 'b |r = &x; // | |} // -+ |// |println!("r: {}", r); // |} // ---------+
其中 '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 )。
{let x = 5; // ----------+-- 'b// |let r = &x; // --+-- 'a |// | |println!("r: {}", r); // | |// --+ |} // ----------+
所以 lifetime 的判断逻辑就是:引用的 lifetime 需要小于或等于被引用的变量的 lifetime 。
使用场景
大部分场景下,Rust 可以自己推断出各个变量的 lifetime ,但是有部分场景不能推断出来,就需要开发者通过声明 lifetime 的方式告诉 Rust 的 borrow checker,否则 borrow checker 会报错,比如下面这段简单的示例代码
fn longest(x: &str, y: &str): &str {if (x.len() > y.len()) {x} else {y}}
由于函数的返回类型是引用,Rust 的编译器不知道这个引用的 lifetime 是啥,( 前面的例子中是直接通过赋值的方式 x = &y 所以编译器很清晰的知道 x 依赖 y 的 lifetime ,但是这里 Rust 不知道 )。
所以就需要类似于声明依赖关系一样,通过 lifetime 泛型告诉编译器返回的引用的 lifetime 依赖 x 和 y 的 lifetime ,即只要 x 或者 y 中的任何一个 lifetime 结束,返回值的 lifetime 也就结束不能再使用( 标记为 invalid )
lifetime 的声明,感觉有点像历史包袱,大部分 lifetime 其实 Rust 自己应该能推导出来,当然这个需要 Rust 的持续完善,估计随着 Rust 的迭代,未来需要定义 lifetime 依赖关系的代码会越来越少。
fn longest<'a> (x: &'a str, y: &'a str): &'a str {if (x.len() > y.len()) {x} else {y}}
再举个例子,比如下面这个,就表示返回的引用的 lifetime,就是依赖 x 的 lifetime 。
fn longest<'a>(x: &'a str, y: &str) -> &'a str {x}
lifetime 的声明跟泛型一样,都可以作用于 struct impl 等,比如下面这个例子,也是申明 ImportantExcerpt 这个 struct 的 lifetime 是依赖其属性 part 这个引用的 lifetime 的。
struct ImportantExcerpt<'a> {part: &'a str,}fn main() {let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().expect("Could not find a '.'");let i = ImportantExcerpt {part: first_sentence,};}
真正写代码的时候,不需要过度去关注是否要声明 lifetime,一般来说当需要声明 lifetime 的时候,编译器也会提醒你加上,就提醒的时候再加上即可,只要理解 lifetime 是干嘛的就行。如果编译器能正常编译,说明编译器能推导出 lifetime ,自己也就没必要去加了。
