Rust-泛型与特征

 Rust  泛型与特征 󰈭 6006字

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后, 编译器默认调用的是类型中的方法

  • 为了调用特征下的方法, 需要给出显示调用的语法:

    1fn 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不能单独作为特征对象的定义, 因为特征对象可以是任意类型, 编译器不知道其具体类型大小, 因而需要使用&dynBox<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()而言, cCounter类型, 因而Self::Item就是Counter::Itemu32.

事实上也可以使用泛型实现, 但是关联类型的好处在于可读性好.

比如看到如下的两个代码段.

  • 泛型写法:

     1pub 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
    
  • 关联类型写法:

     1    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)

参考

泛型和特征 - Rust语言圣经(Rust Course)

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核, 后端开发, Python, Rust 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改记录:
  • 2023-05-29 23:05:14大幅重构了python脚本的目录结构,实现了若干操作博客内容、sqlite的助手函数;修改原本的文本数 据库(ok)为sqlite数据库,通过嵌入front-matter的page_id将源文件与网页文件相关联
  • 2023-05-08 21:44:36博客架构修改升级
  • 2023-02-24 20:02:25进一步补充Rust特征相关内容
  • 2023-02-22 18:38:52Rust泛型与特征, 以及一些Rust习题
Rust-泛型与特征