Rust的泛型(Generics)和特征(Trait)看的一个头两个大… 需要仔细学习整理一下, rust_course的标题分类感觉有点confusing…
泛型
如何使用泛型?
泛型函数:
1fn largest<T>(list: &[T]) -> T{
2 ..
3}
泛型结构体:
1struct Point<T> {
2 x: T,
3 y: T,
4}
5
6fn main() {
7 let integer = Point { x: 5, y: 10 };
8 let float = Point { x: 1.0, y: 4.0 };
9}
泛型枚举:
1enum Option<T> {
2 Some(T),
3 None,
4}
5
6enum Result<T, E> {
7 Ok(T),
8 Err(E),
9}
泛型方法:
1struct Point<T> {
2 x: T,
3 y: T,
4}
5
6impl<T> Point<T> {
7 fn x(&self) -> &T {
8 &self.x
9 }
10}
总的来说, 这几个泛型的定义都比较简单.
为具体的泛型实现方法
对于泛型来说, 可以指定某个具体的类型, 比如:
1struct Point<T>{
2 x:T, y:T
3}
4
5impl Point<f64> {
6 fn foo(&self){
7 println!("({:.10}, {:.10})", self.x, self.y);
8 }
9}
10
11// impl<T> Point<T>{
12// fn foo(&self){
13// println!("This is default method");
14// }
15// }
16
17
18fn main(){
19 let p = Point{x:1.0, y:2.0};
20 p.foo();
21
22 // let q = Point{x:1, y:2};
23 // q.foo();
24}
在这里, 仅仅对于f64
这个类型实现了foo
方法, 对于q
来说其类型为i32则无法调用foo
.
此外, 如果尝试加上L11-15企图获得默认的foo方法呢? 好像不太行, 编译器会告诉我们duplicate definitions for "foo"
那么如何能实现默认foo方法以及f64
独自的foo方法呢? 答案是可以用特征:
Todo.
const泛型
这是rust1.51引入的特性, 分为const泛型表达式和const fn.
Todo.
特征
特征与接口类似, 其实际是定义了一个可以被共享的行为, 只要实现了该特征, 就能使用该行为.
特征的定义与实现
可以这样定义一个特征Summary
, 不过在我的视角下更像是一个声明(Declaration)? 因为其只给出了该方法的签名, 却不给出具体的实现. 因此之后所有拥有该特征的类型都需要具体实现该特征的具体方法. 需要注意此时方法的签名结尾是一个;
而不是{}
.
1pub trait Summary {
2 fn summarize(&self) -> String;
3}
或者可以真正的定义一个特征, 使其拥有默认的实现, 这样之后的类型可以选择使用默认的方法或者自己重载该方法.
1pub trait Summary {
2 fn summarize(&self) -> String {
3 String::from("(Read more...)")
4 }
5}
此外, 在特征的默认实现中, 可以使用一个尚未实现的方法, 比如:
1pub trait Summary {
2 fn summarize_author(&self) -> String;
3
4 fn summarize(&self) -> String {
5 format!("(Read more from {}...)", self.summarize_author())
6 }
7}
这样, 类型实际在实现特征的时候可以只实现summarize_author
方法. 那么类型如何能自己实现/重载一个特征的方法呢, 差不多长这样:
1pub trait Summary {
2 fn summarize(&self) -> String;
3}
4
5pub struct Post {
6 pub title: String,
7 pub author: String,
8 pub content: String,
9}
10
11impl Summary for Post {
12 fn summarize(&self) -> String {
13 format!("文章{}, 作者是{}", self.title, self.author)
14 }
15}
孤儿规则与绕过方案
Rust的孤儿规则(Orphan Rule, OR)约束了当为某类型实现某trait时, 必须保证类型或特征至少有一个在当前作用域中定义. 这个原则可以保证别人用你库的时候不会破坏你库里函数的功能(同理, 你也不能破坏标准库函数的功能), 以此来保证一致性.
如何绕过孤儿规则? 使用newtype模式
, 即创建一个新的元组结构体来封装该类型.
比如, 如何为Vec重载Display特征呢:
1use std::fmt;
2
3struct Wrapper(Vec<String>);
4
5impl fmt::Display for Wrapper {
6 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7 write!(f, "[{}]", self.0.join(", "))
8 }
9}
10
11fn main() {
12 let w = Wrapper(vec![String::from("hello"), String::from("world")]);
13 println!("w = {}", w);
14}
好消息是, 其在运行时没有任何性能损耗, 因为在编译期间, 该类型就会被自动忽略; 同时, 还可以通过重载warpper的一些方法来避免暴露底层的所有方法, 实现隐藏的目的.
坏消息是, 访问Vec必须要通过self.0
才能获得, 十分啰嗦. 不过一个可能的解决方案为实现Deref
特征, 该特征可以自动做一层类似类型转换的操作, 直接将wrapper当作vec使用, 不过这里先不深究了!
特征作为函数参数
比如这样一个函数, 接受类型为&impl Summary
的参数, 这是非常妙的: 只要这个参数实现了该特征就可以作为参数进入该函数.
1pub fn notify(item: &impl Summary) {
2 println!("Breaking news! {}", item.summarize());
3}
特征作为函数返回值
特征也可以作为返回值, 如下的-> impl Summary
语法即表明返回了一个实现了Summary
特征的类型.
1fn returns_summarizable() -> impl Summary {
2 Weibo {
3 username: String::from("sunface"),
4 content: String::from(
5 "m1 max太厉害了,电脑再也不会卡",
6 )
7 }
8}
其优点在于:
这种 impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回。例如,闭包和迭代器就是很复杂,只有编译器才知道那玩意的真实类型,如果让你写出来它们的具体类型,估计内心有一万只草泥马奔腾,好在你可以用 impl Iterator 来告诉调用者,返回了一个迭代器,因为所有迭代器都会实现 Iterator 特征。
但其缺点在于其只能有一个具体的类型, 不能通过if分支等手段返回不同的类型(解决方案为 特征对象):
特征约束
在 特征作为函数参数的语法中, 其实用到了特征约束(trait bound), 不过该形式只是一个语法糖, 其完整的形式为:
1pub fn notify<T: Summary>(item: &T) {
2 println!("Breaking news! {}", item.summarize());
3}
一般情况下语法糖形式即可, 但是在一些复杂的情况下, 比如需要两个参数, 且必须为同一类型时, 使用语法糖则无法限制其类型相同, 此时可以转为使用完整的特征约束:
1// 无法规定类型相同!
2// pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
3
4pub fn notify<T: Summary>(item1: &T, item2: &T) {}
多重约束的语法类似:
1pub fn notify(item: &(impl Summary + Display)) {}
2pub fn notify<T: Summary + Display>(item: &T) {}
当约束变得更多更复杂时, 可以使用where
关键字分离约束:
1fn some_function<T, U>(t: &T, u: &U) -> i32
2 where T: Display + Clone,
3 U: Clone + Debug
4{}
有条件地实现方法,特征
解锁了特征约束后, 我们可以使用该功能有条件地实现方法或特征.
例如, 为pair类型实现一个带有约束的方法cmp_display
, 只有可展示且拥有偏序关系的类型才能获得此方法.
1use std::fmt::Display;
2
3struct Pair<T> {
4 x: T,
5 y: T,
6}
7
8impl<T: Display + PartialOrd> Pair<T> {
9 fn cmp_display(&self) {
10 if self.x >= self.y {
11 println!("The largest member is x = {}", self.x);
12 } else {
13 println!("The largest member is y = {}", self.y);
14 }
15 }
16}
或者有条件地实现特征, 比如标准库就为任何实现了 Display 特征的类型实现了 ToString 特征:
1impl<T: Display> ToString for T {
2 // --snip--
3}
实现largest函数
使用特征约束, 即可写一个largest函数. 偏序关系使得可以通过<
进行大小比较, Copy特征允许深拷贝, 不然无法将一个函数中的变量返回到外面(即变量largest
)
1fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
2 let mut largest = list[0];
3
4 for &item in list.iter() {
5 if item > largest {
6 largest = item;
7 }
8 }
9
10 largest
11}
12
13fn main() {
14 let number_list = vec![34, 50, 25, 100, 65];
15
16 let result = largest(&number_list);
17 println!("The largest number is {}", result);
18
19 let char_list = vec!['y', 'm', 'a', 'q'];
20
21 let result = largest(&char_list);
22 println!("The largest char is {}", result);
23}
supertrait约束
supertrait
指的是对特征的特征约束. 为了实现特征A, 必须还要实现特征B, 其语法与一般的类型上的特征约束类似, 下面看一个例子:
1use std::fmt::Display;
2
3trait OutlinePrint: Display {
4 fn outline_print(&self) {
5 let output = self.to_string();
6 let len = output.len();
7 println!("{}", "*".repeat(len + 4));
8 println!("*{}*", " ".repeat(len + 2));
9 println!("* {} *", output);
10 println!("*{}*", " ".repeat(len + 2));
11 println!("{}", "*".repeat(len + 4));
12 }
13}
此时, 如果要实现OutlinePrint
特征, 必须也为该类型实现Displaly
特征才可成功通过编译!
特征派生
被derive
标记的对象会自动实现对应的默认特征代码, 继承相应的功能.
最常见的是#[derive(Debug)]
派生一个Debug
特征, 可以通过println!("{:?}", s)
打印对象内容; 再如 Copy 特征, 可以让这个类型自动实现 Copy 特征, 进而可以调用 copy 方法进行自我复制.
调用特征方法
为了使用一个特征方法, 必须要该特征引入当前的作用域中, 不过Rust提供了一个非常便利的办法: 即把最常用的标准库中的特征通过 std::prelude
模块提前引入到当前作用域中, 这样无需use
也可以调用最常见的各个方法.
特征的同名方法
不同的特征可以拥有同名的方法, 甚至会与特征的方法同名(2个trait的fly和一个Human类型的fly):
1trait Pilot {
2 fn fly(&self);
3}
4
5trait Wizard {
6 fn fly(&self);
7}
8
9struct Human;
10
11impl Pilot for Human {
12 fn fly(&self) {
13 println!("This is your captain speaking.");
14 }
15}
16
17impl Wizard for Human {
18 fn fly(&self) {
19 println!("Up!");
20 }
21}
22
23impl Human {
24 fn fly(&self) {
25 println!("*waving arms furiously*");
26 }
27}
如何调用同名的方法呢?
-
直接调用fly后, 编译器默认调用的是类型中的方法
-
为了调用特征下的方法, 需要给出显示调用的语法:
rust1fn main() { 2 let person = Human; 3 Pilot::fly(&person); // 调用Pilot特征上的方法 4 Wizard::fly(&person); // 调用Wizard特征上的方法 5 person.fly(); // 调用Human类型自身的方法 6}
这是由于fly的参数为self
, 通过强制指定类型即可唯一确定是哪个trait的方法.
需要注意, 所有的方法都有self
参数! 没有该参数的是某个类型的关联函数!
下面看一组没有self
参数的同名函数:
1trait Animal {
2 fn baby_name() -> String;
3}
4
5struct Dog;
6
7impl Dog {
8 fn baby_name() -> String {
9 String::from("Spot")
10 }
11}
12
13impl Animal for Dog {
14 fn baby_name() -> String {
15 String::from("puppy")
16 }
17}
18
19fn main() {
20 println!("A baby dog is called a {}", Dog::baby_name());
21}
与human fly相比, 这里baby_name参数没有self, 因而这是一组trait同名函数和关联函数的重名问题.
关联函数的调用很简单, 直接用::
即可; 但此时如何调用Animal特征下的方法呢?
直接使用Animal::baby_name()
是不行的, 因为尽管在上述代码中只有小狗实现了该特征, 但是其余动物类型也允许实现该特征, 因而Animal::baby_name()
完全无法限定究竟是哪个类型的Animal特征.
正确的方法是完全限定语法, 这是最为明确的调用函数的方式, 该语法的定义为:
1<Type as Trait>::function(receiver_if_method, next_arg, ...);
因此可以将上述问题完整写为:
1fn main() {
2 println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
3}
由于大多数时候rust编译器会自动推断出具体目标函数的类型, 因而只有在同名函数等复杂的情况下该语法才会被实际使用.
特征对象
在 特征作为函数返回值中遇到了返回值类型不统一的问题, Rust的解决方案为特征对象.
一个例子如下:
1trait Draw {
2 fn draw(&self) -> String;
3}
4
5impl Draw for u8 {
6 fn draw(&self) -> String {
7 format!("u8: {}", *self)
8 }
9}
10
11impl Draw for f64 {
12 fn draw(&self) -> String {
13 format!("f64: {}", *self)
14 }
15}
16
17// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被
18// 隐式转换成函数参数签名中的 Box<dyn Draw>
19fn draw1(x: Box<dyn Draw>) -> String {
20 // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,
21 // 然后调用该值对应的类型上定义的 `draw` 方法
22 x.draw()
23}
24
25fn draw2(x: &dyn Draw) -> String {
26 x.draw()
27}
28
29fn main() {
30 let x = 1.1f64;
31 // do_something(&x);
32 let y = 8u8;
33
34 // x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在
35 // 函数调用时隐式地被转换为特征对象 Box<dyn Draw>
36 // 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上
37 println!("{}", draw1(Box::new(x)));
38 println!("{}", draw2(&y));
39}
-
可以通过
&
引用或者Box<T>
智能指针的方式来创建特征对象。 -
dyn
关键字只用在特征对象的类型声明上, 创建时无需使用.
注意dyn
不能单独作为特征对象的定义, 因为特征对象可以是任意类型, 编译器不知道其具体类型大小, 因而需要使用&dyn
和Box<dyn>
这类大小已知的类型作为特的征对象的定义.
特征对象的分发
对于泛型来说, 编译器会为每个具体类型生成一份代码, 这种是静态分发(static dispatch), 而对于特征对象而言则一定是动态分发, 因为只有到运行时才知道需要调用什么方法, dyn
关键字正强调了该特点.
如图, 相比于一般的Box类型, dyn
的类型还在栈上保存一个虚表vtable, 虚表保存了该实例对应的特征方法. 也因此, 一旦一个实例变成了特征对象的实例, 就不再是之前具体类型的实例了, 不能调用以前类型的方法了, 只能调用当前特征对象的方法.
特征对象的限制
不是所有特征都能拥有特征对象, 只有对象安全的特征才行. 当一个特征的所有方法都满足:
-
方法的返回类型不能是
Self
-
方法没有任何泛型参数
这两个约束主要的原因都在于一旦有了特征对象, 原来的数据类型就会被遗忘.. 那么特征方法如果返回Self
, 其实没人知道这个到底对应什么类型, 使用泛型也是, 无法得知放入泛型的参数到底原本是什么类型.
比如标准库的Clone
就不是安全的:
1pub trait Clone {
2 fn clone(&self) -> Self;
3}
关联类型
关联类型是在特征定义的语句块中, 申明一个自定义类型, 这样就可以在特征的方法签名中使用该类型:
1pub trait Iterator {
2 type Item;
3 fn next(&mut self) -> Option<Self::Item>;
4}
5
6impl Iterator for Counter {
7 type Item = u32;
8 fn next(&mut self) -> Option<Self::Item> {
9 // --snip--
10 }
11}
12
13fn main() {
14 let c = Counter{..}
15 c.next()
16}
此时对于c.next()
而言, c
是Counter
类型, 因而Self::Item
就是Counter::Item
即u32
.
事实上也可以使用泛型实现, 但是关联类型的好处在于可读性好.
比如看到如下的两个代码段.
-
泛型写法:
rust1pub trait Converter<T> { 2 fn convert(&self) -> T; 3} 4 5struct MyInt; 6 7impl Converter<i32> for MyInt { 8 fn convert(&self) -> i32 { 9 42 10 } 11} 12 13impl Converter<f32> for MyInt { 14 fn convert(&self) -> f32 { 15 52.0 16 } 17} 18 19 20fn main() { 21 let my_int = MyInt; 22 23 // Error: could not use turbofish syntax here 24 // let output = my_int.convert::<i32>(); 25 let output: i32 = my_int.convert(); 26 println!("output is: {}", output); 27 28 // Error: could not use turbofish syntax here 29 // let output = my_int.convert::<f32>(); 30 let output: f32 = my_int.convert(); 31 println!("output is: {}", output); 32 33} 34 35// 输出: 36// 37// output is: 42 38// output is: 52
-
关联类型写法:
rust1 pub trait Converter { 2 type Output; 3 4 fn convert(&self) -> Self::Output; 5} 6 7struct MyInt; 8 9impl Converter for MyInt { 10 type Output = i32; 11 12 fn convert(&self) -> Self::Output { 13 42 14 } 15} 16 17fn main() { 18 let my_int = MyInt; 19 20 let output = my_int.convert(); 21 println!("output is: {}", output); 22} 23 24// 输出: 25// 26// output is: 42
可以看到一些对比:
-
在泛型写法中必须在函数头部也加入泛型的声明, 明确标注说明使用的是哪一个具体的实现.
-
泛型的一些好处在于其无需指定具体的类型, 但关联类型就必须在
impl
时就使用type
绑定, 因而只能绑定一个目标类型
参考: 【Rust每周一知】Rust 中 trait、关联类型与泛型配合的常见模式 进一步的学习.
默认泛型类型参数
当在特征中使用泛型类型参数时, 可以为其指定一个默认的具体类型, 比如:
1pub trait Converter<T=i32> {
2 fn convert(&self) -> T;
3}
4
5struct MyInt;
6
7impl Converter for MyInt {
8 fn convert(&self) -> i32 {
9 42
10 }
11}
12
13impl Converter<f32> for MyInt {
14 fn convert(&self) -> f32 {
15 52.0
16 }
17}
18
19
20fn main() {
21 let my_int = MyInt;
22
23 let output: i32 = my_int.convert();
24 println!("output is: {}", output);
25
26 let output: f32 = my_int.convert();
27 println!("output is: {}", output);
28
29}
30
31// 输出:
32//
33// output is: 42
34// output is: 52
-
L7实现的
Converter
特征的默认类型就为默认的i32
, 而L13也可以额外指定了f32
类型 -
L23和L26居然会自动进行匹配, 感觉有点高级
再看一个标准库的例子std::ops::Add
特征:
1trait Add<RHS=Self> {
2 type Output;
3 fn add(self, rhs: RHS) -> Self::Output;
4}
默认的类型为Self
, 意味着其默认支持同类型相加, 下面给一个具体的Add的例子:
1use std::ops::Add;
2
3#[derive(Debug, PartialEq)]
4struct Point {
5 x: i32,
6 y: i32,
7}
8
9impl Add for Point {
10 type Output = Point;
11
12 fn add(self, other: Point) -> Point {
13 Point {
14 x: self.x + other.x,
15 y: self.y + other.y,
16 }
17 }
18}
19
20fn main() {
21 assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
22 Point { x: 3, y: 3 });
23}
可以看到:
-
为了为Add实现方法, 需要将Add通过use引入到当前作用域下(L1)
-
通过实现(更精确的说, 重载)
+
运算符, 定义了两个相同的Point
类型相加.
再比如:
1use std::ops::Add;
2
3struct Millimeters(u32);
4struct Meters(u32);
5
6impl Add<Meters> for Millimeters {
7 type Output = Millimeters;
8
9 fn add(self, other: Meters) -> Millimeters {
10 Millimeters(self.0 + (other.0 * 1000))
11 }
12}
此时就不能使用默认的RHS=Self
了, 需要为其指定另一个新的类型.
总结的说, 特征中泛型默认的类型参数可以减少需要实现的模板代码, 更重要的是其允许我们无需大规模修改原有代码即可拓展新的类型.
对于第二点,也很好理解,如果你在一个复杂类型的基础上,新引入一个泛型参数,可能需要修改很多地方,但是如果新引入的泛型参数有了默认类型,情况就会好很多,添加泛型参数后,使用这个类型的代码需要逐个在类型提示部分添加泛型参数,就很麻烦;但是有了默认参数(且默认参数取之前的实现里假设的值的情况下)之后,原有的使用这个类型的代码就不需要做改动了。
最后看一个稍综合的例子结束本小节, 其中包含了关联类型, 泛型参数, 默认参数以及Self:
1pub trait Converter<T=Self> {
2 type Output;
3
4 fn convert(&self) -> (Self::Output, T);
5}
6
7#[derive(Debug, Copy, Clone)]
8struct MyInt(i32);
9
10impl Converter for MyInt {
11 type Output = Self;
12
13 fn convert(&self) -> (Self::Output, Self) {
14 (*self, *self)
15 }
16}
17
18impl Converter<f32> for MyInt {
19 type Output = Self;
20
21 fn convert(&self) -> (Self::Output, f32) {
22 (*self, 52.0)
23 }
24}
25
26
27fn main() {
28 let my_int = MyInt(42);
29
30 let output: (MyInt, MyInt) = my_int.convert();
31 println!("output is: {:?}", output);
32
33 let output: (MyInt, f32) = my_int.convert();
34 println!("output is: {:?}", output);
35
36}
37
38// 输出:
39//
40// output is: (MyInt(42), MyInt(42))
41// output is: (MyInt(42), 52.0)