Rust 语言从入门到实战 唐刚--读书笔记10

基础篇 (11讲)

10|再探trait:带类型参数的trait及trait object

继续 trait 相关。

回顾:类型参数出现的地方。

  • 用 trait 对 T 作类型空间的约束,如 T: TraitA。
  • blanket implementation 时的 T,如 impl TraitA for T {}。
  • 函数的 T 参数,如 fn doit(a: T) {}。

不同位置的 T,基础意义都是类型参数,但其侧重的意义不同。

  • T: TraitA  强调“参数”是 T,用 TraitA 来削减 T 的类型空间。
  • impl TraitA for T {}  末尾的 T 强调“类型”,为 T 类型实现 TraitA。
  • doit(a: T) {}  第二个 T 表示某种类型,强调类型参数的“类型”部分。

这节是另外一个东西,里面也带 T 参数。与之前几种形式有什么不同?

trait 上带类型参数

trait 上带类型参数:

trait TraitA {}

这个 trait 里面的函数或方法,会用到这个类型参数 T 。定义 trait 时,还没确定这个类型参数的具体类型。等到 impl 甚至使用类型方法时,才会具体化这个 T 的具体类型。

注意,这时  TraitA  是一个整体,表示一个 trait。如 TraitA  和 TraitA 是两个不同的 trait,这里单独把 TraitA 拿出来说,没有意义。

实现时要在 impl 后面先定义类型参数:

impl TraitA for Atype {}

也可以在对类型实现时,将 T 参数具体化:

impl TraitA for Atype {}

如果被实现的类型上自身也带类型参数,那情况会更复杂。

trait TraitA {}
struct Atype {
    a: U,
}
impl TraitA for Atype {}

这些类型参数都可以在 impl 时被约束:

use std::fmt::Debug;

trait TraitA {}
struct Atype {
    a: U,
}
impl TraitA for Atype 
where
    T: Debug,      // 在 impl 时添加了约束
    U: PartialEq,  // 在 impl 时添加了约束
{}

注:以上代码都在 playground 中编译通过。

impl 示例

具体的实例,体会带类型参数的 trait 的威力。

要实现一个模型:

  1. 平面上的一个点与另一个点相加,形成新的点。算法是两个点的 x 分量和 y 分量分别相加。
  2. 平面上的一个点加一个整数 i32,形成新的点。算法是分别在 x 分量和 y 分量上加 i32 参数。

本例借鉴了:https://github.com/pretzelhammer/rust-blog/blob/master/posts/tour-of-rusts-standard-library-traits.md#generic-types-vs-associated-types

代码:

// 定义一个带类型参数的trait
trait Add {
    type Output;
    fn add(self, rhs: T) -> Self::Output;
}

struct Point {
    x: i32,
    y: i32,
}

// 为 Point 实现 Add 这个 trait
impl Add for Point {
    type Output = Self;
    fn add(self, rhs: Point) -> Self::Output {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

// 为 Point 实现 Add 这个 trait
impl Add for Point { 
    type Output = Self;
    fn add(self, rhs: i32) -> Self::Output {
        Point {
            x: self.x + rhs,
            y: self.y + rhs,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 1 };
    let p2 = Point { x: 2, y: 2 };
    let p3 = p1.add(p2);  // 两个Point实例相加
    assert_eq!(p3.x, 3);
    assert_eq!(p3.y, 3);
    
    let p1 = Point { x: 1, y: 1 };
    let delta = 2;
    let p3 = p1.add(delta);   // 一个Point实例加一个i32
    assert_eq!(p3.x, 3);
    assert_eq!(p3.y, 3);
}

详细解释:

Add 这个 trait,带一个类型参数 T,还带一个关联类型 Output。

对 Point 类型,实现了两个 trait:Add 和 Add。是两个不同的 trait ,故能对同一个类型实现。强调:一个 trait 只能对一个类型实现一次。

根据需求,运算后的类型也是 Point,故两个 trait 中的关联类型都是 Self。注意:两个 trait 中实现的不同算法。

通过这种形式,在同一个类型上实现了同名方法(add 方法)参数类型的多种形态。即:Point 实例的 add 方法既可以接收 Point 参数,又可以接收 i32 参数,Rustc 可以根据不同的参数类型自动找到对应的方法调用。

在 Java、C++ 中,有语言层面的函数重载特性来支持这种功能。Rust 并不直接支持函数重载特性,但用 trait 就轻松实现了同样的效果(全新的思路)。

trait 类型参数的默认实现

定义带类型参数的 trait 的时候,可以为类型参数指定一个默认类型

如 trait TraitA {}。这样,impl TraitA for SomeType {} 就等价于 impl TraitA for SomeType {}。

例子。

// Self可以用在默认类型位置上
trait TraitA {
    fn func(t: T) {}
}

// 这个默认类型为i32
trait TraitB {
    fn func2(t: T) {}
}

struct SomeType;

// 这里省略了类型参数,所以这里的T为Self
// 进而T就是SomeType本身
impl TraitA for SomeType {
    fn func(t: SomeType) {}
}
// 这里省略了类型参数,使用默认类型i32
impl TraitB for SomeType {
    fn func2(t: i32) {}
}
// 这里不省略类型参数,明确指定类型参数为String
impl TraitA for SomeType {
    fn func(t: String) {}
}
// 这里不省略类型参数,明确指定类型参数为String
impl TraitB for SomeType {
    fn func2(t: String) {}
}

默认参数给表达上带来了一定程度的简洁,但增加了初学者识别和理解上的困难。

上一节课讲关联类型时我们提到过,在使用约束时可以具化关联类型。那里也是用的=号。如:

trait TraitA {
    type Item;
}
// 这里,定义结构体类型时,用到了TraitA作为约束
struct Foo> {
    x: T
}

初看这里容易混淆。区别:关联类型的具化是在应用约束时类型参数的默认类型指定是在定义 trait 时,通过 trait 出现的场景可以区分。

trait 中的类型参数与关联类型的区别

trait 上的类型参数和关联类型都具有延迟具化的特点,区别是什么呢?为什么要设计两种不同的机制呢?

首先要明确的一点是,Rust 本身也在持续演化过程中。有些特性先出现,有些特性是后出现的。最后演化出功能相似但是不完全一样的特性是完全有可能的。

具体有两点不同:

  1. 类型参数可在 impl 类型时具化,也可延迟到使用时具化。关联类型在被 impl 时必须具化。
  2. 类型参数和 trait 名一起组成了完整的 trait 名,不同的具化类型会构成不同的 trait,看起来同一个定义可以在目标类型上实现“多次”。关联类型没有这个作用。

第一点的示例:

use std::fmt::Debug;

trait TraitA 
where
    T: Debug,  // 定义TraitA的时候,对T作了约束
{
    fn play(&self, _t: T) {}
}

struct Atype;

impl TraitA for Atype
where
    T: Debug + PartialEq,  // 将TraitA实现到类型Atype上时,加强了约束
{}

fn main() {
    let a = Atype;
    a.play(10u32);  // 在使用时,通过实例方法传入的参数类型具化T
}

几个要点。

  1. 定义带类型参数的 trait 时,可用 where 表达提供约束。
  2. impl trait 时可对类型参数加强约束,如:例子中的 Debug + PartialEq。
  3. impl trait 时可不具化类型参数。
  4. 可在使用方法时具化类型参数。如 a.play(10u32),把 T 具化成了 u32。

当然,在 impl 的时候也可指定成 u32 类型:

use std::fmt::Debug;

trait TraitA 
where
    T: Debug,
{
    fn play(&self, _t: T) {}
}

struct Atype;

impl TraitA for Atype {} // 这里具化成了 TraitA

fn main() {
    let a = Atype;
    a.play(10u32);
}

但这样就没前面那么灵活了,如 a.play(10u64) 就不行。

第二点:对关联类型,如在 impl 时不对其具化,就无法编译通过。

trait Add {
    type ToAdd;    // 多定义一个关联类型
    type Output;
    fn add(self, rhs: Self::ToAdd) -> Self::Output;
}

struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type ToAdd = Point;
    type Output = Point;
    fn add(self, rhs: Point) -> Point {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

impl Add for Point { // 这里重复impl了同一个trait,无法编译通过
    type ToAdd = i32;
    type Output = Point;
    fn add(self, rhs: i32) -> Point {
        Point {
            x: self.x + rhs,
            y: self.y + rhs,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 1 };
    let p2 = Point { x: 2, y: 2 };
    let p3 = p1.add(p2);
    assert_eq!(p3.x, 3);
    assert_eq!(p3.y, 3);
    
    let p1 = Point { x: 1, y: 1 };
    let delta = 2;
    let p3 = p1.add(delta); // 这句是错的
    assert_eq!(p3.x, 3);
    assert_eq!(p3.y, 3);

编译器出错:

error[E0119]: conflicting implementations of trait `Add` for type `Point`:
  --> src/main.rs:23:1
   |
12 | impl Add for Point {
   | ------------------ first implementation here
...
23 | impl Add for Point {
   | ^^^^^^^^^^^^^^^^^^ conflicting implementation for `Point`

对 Point 类型实现了多次 Add,冲突。编译不通过。所以这个模型仅用关联类型来实现,是写不出来的。

  • 看起来,带类型参数的 trait 功能更强大。
  • 关联类型的优点,没有类型参数,不存在多引入一个参数的问题。
  • 类型参数具有传染性,在一个调用层次很深的系统中,增删一个类型参数可能会导致整个项目文件到处都需要改,非常头疼。
  • 而关联类型没有这个问题。在一些场合下,关联类型正好是减少类型参数数量的一种方法。
  • 模型比较简单,不需要多态特性,这时用关联类型就更简洁,代码可读性更好。

trait object

从一个函数要返回不同的类型说起。

如果写成返回固定类型的函数签名,那就只能返回那个类型:

struct Atype;
struct Btype;
struct Ctype;
fn doit() -> Atype {
    let a = Atype;
    a
}

第一个办法---用 enum。

struct Atype;
struct Btype;
struct Ctype;

enum TotalType {
  A(Atype),    // 用变体把目标类型包起来
  B(Btype),
  C(Ctype),
}

fn doit(i: u32) -> TotalType {  // 返回枚举类型
  if i == 0 {
    let a = Atype;
    TotalType::A(a)    // 在这个分支中返回变体A
  } else if i == 1 {
    let b = Btype;
    TotalType::B(b)    // 在这个分支中返回变体B
  } else {
    let c = Ctype;
    TotalType::C(c)    // 在这个分支中返回变体C
  }
}

enum 常用于聚合类型。无脑 + 强行揉在一起。enum 聚合类型是编码时已知的类型。在聚合前,需要知道待聚合类型的边界,一旦定义完成,之后运行时就不能改动了,是封闭类型集

第二种办法---用类型参数:

struct Atype;
struct Btype;
struct Ctype;
fn doit() -> T {
  let a = Atype;
  a
}

无法通过编译:

error[E0308]: mismatched types
 --> src/lib.rs:6:3
  |
4 | fn doit() -> T {
  |         -       - expected `T` because of return type
  |         |
  |         this type parameter
5 |   let a = Atype;
6 |   a
  |   ^ expected type parameter `T`, found `Atype`
  |
  = note: expected type parameter `T`
                     found struct `Atype`

这里这个类型参数 T 是在这个函数调用时指定,而不是在这个函数定义时指定的。没法在这里直接返回一个具体的类型代入 T。只能尝试用 T 来返回,改出第二个版本。

struct Atype;
struct Btype;
struct Ctype;

impl Atype {
    fn new() -> Atype {
        Atype
    }
}

impl Btype {
    fn new() -> Btype {
        Btype
    }
}

impl Ctype {
    fn new() -> Ctype {
        Ctype
    }
}

fn doit() -> T {
  T::new()
}

编译还是报错。

error[E0599]: no function or associated item named `new` found for type parameter `T` in the current scope
  --> src/main.rs:24:6
   |
23 | fn doit() -> T {
   |         - function or associated item `new` not found for this type parameter
24 |   T::new()
   |      ^^^ function or associated item not found in `T`

Rustc并不知道我们定义这个类型参数 T 里面有 new 这个关联函数。

联想前面学过的,可以用 trait 来定义这个协议,让 Rust 认识它。第三个版本:

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {
    fn new() -> Self;    // TraitA中定义了new()函数
}

impl TraitA for Atype {
    fn new() -> Atype {
        Atype
    }
}

impl TraitA for Btype {
    fn new() -> Btype {
        Btype
    }
}

impl TraitA for Ctype {
    fn new() -> Ctype {
        Ctype
    }
}

fn doit() -> T {
  T::new()
}

fn main() {
    let a: Atype = doit::();
    let b: Btype = doit::();
    let c: Ctype = doit::();
}

这个版本顺利通过编译。在这个示例中,我们认识到了引入 trait 的必要性,就是让 Rustc 小助手知道我们在协议层面有一个 new() 函数,一旦类型参数被 trait 约束后,它就可以去 trait 中寻找协议定义的函数和方法。

费了不少力气。Rust 提供了更优雅的方案来解决这个需求。用 trait 提供了一种特殊语法 impl trait:

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit() -> impl TraitA {  // 注意这一行的函数返回类型
  let a = Atype;
  a
  // 或
  // let b = Btype;
  // b
  // 或
  // let c = Ctype;
  // c
}

表达非常简洁,同一个函数签名可以返回多种不同的类型,并且在函数定义时就可以返回具体的类型的实例。更重要的是消除了类型参数 T

上述代码已经很有用了,但不够灵活,如用 if 逻辑选择不同的分支返回不同的类型,会遇到问题。

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit(i: u32) -> impl TraitA {  
  if i == 0 {
    let a = Atype;
    a                    // 在这个分支中返回类型a
  } else if i == 1 {
    let b = Btype;
    b                    // 在这个分支中返回类型b
  } else {
    let c = Ctype;
    c                    // 在这个分支中返回类型c
  }
}

提示:

error[E0308]: `if` and `else` have incompatible types
  --> src/lib.rs:22:5
   |
17 |     } else if i == 1 {
   |  __________-
18 | |     let b = Btype;
19 | |     b
   | |     - expected because of this
20 | |   } else {
21 | |     let c = Ctype;
22 | |     c
   | |     ^ expected `Btype`, found `Ctype`
23 | |   }
   | |___- `if` and `else` have incompatible types

if else 要求返回同一种类型,Rust 检查确实严格。可以通过加 return 跳过 if else 的限制。

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit(i: u32) -> impl TraitA {
  if i == 0 {
    let a = Atype;
    return a;          // 这里用return语句直接从函数返回
  } else if i == 1 {
    let b = Btype;
    return b;
  } else {
    let c = Ctype;
    return c;
  }
}

但还是报错。

error[E0308]: mismatched types
  --> src/lib.rs:19:12
   |
13 | fn doit(i: u32) -> impl TraitA {  // 这一行
   |                    ----------- expected `Atype` because of return type
...
19 |     return b
   |            ^ expected `Atype`, found `Btype`

说期望 Atype,却得到了 Btype。这个报错其实有点奇怪,它们不是都满足 impl TraitA 吗?

原来问题在于,impl TraitA 作为函数返回值这种语法,其实也只是指代某一种类型而已,而这种类型是在函数体中由返回值的类型来自动推导出来的。例中,Rustc 遇到 Atype 分支时,已确定了函数返回类型为 Atype,当分析到后面的 Btype 分支,就发现类型不匹配了。可以将条件分支顺序换一下,看一下报错的提示,加深印象。

该怎么处理这种问题呢?用 trait object

trait object。形式上,是在 trait 名前加 dyn 关键字修饰,本例是 dyn TraitA。

dyn TraitName 本身就是一种类型,它和 TraitName 这个 trait 相关,但是它们不同,dyn TraitName 是一个独立的类型。

用 dyn TraitA 改写上面的代码。

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit(i: u32) -> dyn TraitA { // 注意这里的返回类型换成了 dyn TraitA
  if i == 0 {
    let a = Atype;
    return a
  } else if i == 1 {
    let b = Btype;
    return b
  } else {
    let c = Ctype;
    return c
  }
}

但是编译会报错。

error[E0746]: return type cannot have an unboxed trait object
  --> src/lib.rs:13:20
   |
13 | fn doit(i: u32) -> dyn TraitA {
   |                    ^^^^^^^^^^ doesn't have a size known at compile-time
   |
help: return an `impl Trait` instead of a `dyn Trait`, if all returned values are the same type
   |
13 | fn doit(i: u32) -> impl TraitA {
   |                    ~~~~
help: box the return type, and wrap all of the returned values in `Box::new`
   |
13 ~ fn doit(i: u32) -> Box {
14 |   if i == 0 {
15 |     let a = Atype;
16 ~     return Box::new(a)
17 |   } else if i == 1 {
18 |     let b = Btype;
19 ~     return Box::new(b)
20 |   } else {
21 |     let c = Ctype;
22 ~     return Box::new(c)

提示很经典:

  • dyn TraitA 编译时尺寸未知。dyn trait 确实不是一个固定尺寸类型。
  • 第一个建议:可用 impl TraitA 来解决,前提是所有分支返回同一类型。
  • 第二个建议,可用 Box 把 dyn TraitA 包起来。

(‍:有没有 ChatGPT 的即时感,聪明得不太像一个编译器。)

第一个建议已经试过了,Pass。按第二种建议试试。

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit(i: u32) -> Box {
    if i == 0 {
        let a = Atype;
        Box::new(a)
    } else if i == 1 {
        let b = Btype;
        Box::new(b)
    } else {
        let c = Ctype;
        Box::new(c)
    }
}

编译通过,达成目标,成功地将不同类型的实例在同一个函数中返回了。

Box 的作用是可以保证获得里面值的所有权,必要的时候会进行内存的复制,比如把栈上的值复制到堆中去。一旦值到了堆中,就很容易掌握到它的所有权。

具体到这个示例中,因为 a、b、c 都是函数中的局部变量,这里如果返回引用 &dyn TraitA 的话是万万不能的,因为违反了所有权规则。而 Box 就能满足这里的要求。后续我们在智能指针一讲中会继续讲解 Box

整个推导过程,令人印象深刻的类型“体操”值得我们多品味几次。

利用 trait object 传参

impl trait 和 dyn trait 可用于函数传参。

impl trait 示例:

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit(x: impl TraitA) {}
// 等价于
// fn doit(x: T) {}

fn main() {
    let a = Atype;
    doit(a);
    let b = Btype;
    doit(b);
    let c = Ctype;
    doit(c);
}

dyn trait 示例:

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit(x: &dyn TraitA) {}  // 注意这里用了引用形式 &dyn TraitA

fn main() {
    let a = Atype;
    doit(&a);
    let b = Btype;
    doit(&b);
    let c = Ctype;
    doit(&c);
}

两种都可以。区别是什么?

impl trait 是编译器静态展开,编译时具化(单态化)。上面 impl trait 示例展开后类似于:

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn doit_a(x: Atype) {}
fn doit_b(x: Btype) {}
fn doit_c(x: Ctype) {}

fn main() {
    let a = Atype;
    doit_a(a);
    let b = Btype;
    doit_b(b);
    let c = Ctype;
    doit_c(c);
}

dyn trait 版本不会在编译期间做任何展开,dyn TraitA 自己就是一个类型,这个类型相当于一个代理类型,用于在运行时代理相关类型及调用对应方法。既然是代理,也就是调用方法的时候需要多跳转一次,从性能上来说,当然要比在编译期直接展开一步到位调用对应函数要慢一点

静态展开,会使编译出来的内容体积增大,而 dyn trait 不会。各有利弊,可以根据需求视情况选择。另外,impl trait 和 dyn trait 都是消除类型参数的办法

impl trait 和 dyn trait 和 enum 相比:

  • enum 封闭类型集,可把任意类型包裹成一个统一的单一类型。后续的任何变动,都需要改这个统一类型,及基于这个 enum 的模式匹配等相关代码。
  • impl trait 和 dyn trait 开放类型集。只要对新的类型实现 trait,就可以传入使用了 impl trait 或 dyn trait 的函数,其函数签名不用变。

上述区别对于库的提供者非常重要。如果你提供了一个库,里面的多类型使用的 enum 包装,那么库的使用者没办法对你的 enum 进行扩展。因为一般来说,我们不鼓励去修改库里面的代码。而用 impl trait 或 dyn trait 就可以让接口具有可扩展性。用户只需要给他们的类型实现你的库提供的 trait,就可以代入库的接口使用了。

impl trait 目前只能用于:函数参数、函数返回值。其他的静态展开场景就得用类型参数形式。

dyn trait 本身是一种非固定尺寸类型,比 impl trait 应用于更多场合,如利用 trait obj 把不同的类型装进集合里。

利用 trait obj 将不同的类型装进集合里

示例,把三种类型装进一个 Vec 里。

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn main() {
    let a = Atype;
    let b = Btype;
    let c = Ctype;
    
    let v = vec![a, b, c];
}

报错:

error[E0308]: mismatched types
  --> src/main.rs:19:21
   |
19 |     let v = vec![a, b, c];
   |                     ^ expected `Atype`, found `Btype`

Vec 要求元素是同一种类型(不同的类型实例,错误)。用 trait object “绕”过这个限制。

struct Atype;
struct Btype;
struct Ctype;

trait TraitA {}

impl TraitA for Atype {}
impl TraitA for Btype {}
impl TraitA for Ctype {}

fn main() {
    let a = Atype;
    let b = Btype;
    let c = Ctype;
    let v: Vec<&dyn TraitA> = vec![&a, &b, &c];
}

成功,不同类型的实例(实际是实例的引用)竟然被放进了同一个 Vec 中!也可以,将不同类型的实例放入 HashMap 中。

注意应用限制:性能损失,不是所有的 trait 都可以 dyn 化(不是所有的 trait 都能转成 trait object 使用)。

哪些 trait 能用作 trait object?

只有满足对象安全(object safety)的 trait 才能被用作 trait object。

Rust 参考手册上有关于 object safety 的详细规则,比较复杂。这里我们了解常用的模式就行。

安全的 trait object:

trait TraitA {
    fn foo(&self) {}
    fn foo_mut(&mut self) {}
    fn foo_box(self: Box) {}
}

不安全的 trait object:

trait NotObjectSafe {
    const CONST: i32 = 1;  // 不能包含关联常量

    fn foo() {}  // 不能包含这样的关联函数
    fn selfin(self); // 不能将Self所有权传入
    fn returns(&self) -> Self; // 不能返回Self
    fn typed(&self, x: T) {} // 方法中不能有类型参数
}

规则确实比较复杂,你可以简单记住几种场景。

  1. 不要在 trait 里面定义构造函数,如 new 这种返回 Self 的关联函数。Rust 生态中都没有将构造函数定义在 trait 中的习惯。
  2. trait 里尽量定义传引用 &self 或 &mut self 的方法,不要定义传值 self 的方法。

并不是所有的 trait 都能以 trait object 形式(dyn trait)使用,实际上,以 dyn trait 使用的场景是少数。可在遇到编译器报错时,再回头来审视 trait 定义得是否合理。大部分情况下可以放心使用。

小结

Rust 语言从入门到实战 唐刚--读书笔记10_第1张图片

前半部分,讲解了 trait 中带类型参数的情况。各种符号组合起来,越来越复杂。模式就那几种,要花点时间熟悉理解。

用带类型参数的 trait 实现了其他语言中函数重载的功能。一条全新的思路:以往的语言必须给自身添加各种特性来满足用户的要求,在 Rust 中,用好 trait 就能搞定。对 Rust 的未来充满期待,不会像 C++、Java 那样永不停歇地添加可能会导致组合爆炸的新特性,而让自身越来越臃肿。

讨论了带类型参数的 trait 与关联类型的区别。它们之间并不存在绝对优势的一方,在合适的场景下选择合适的方案是最重要的。

如何让一个 Rust 函数返回可能的多种类型?推导出了引入 trait object 方案的必要性。整个推导过程比较曲折,同时也是对 Rust 类型系统的一次精彩探索。在这个探索过程中,我们和 Rustc 小助手成为了好朋友,在它的协助下,我们找到了最佳方案。

最后我们了解了 trait object 的一些用途,并讨论了 trait object、impl trait,还有使用枚举对类型进行聚合这三种方式之间的区别。类型系统(类型 + trait)是 Rust 的大脑,你可以多加练习,熟悉它的形式,掌握它的用法。

思考题

请谈谈在函数参数中传入 &dyn TraitA 与 Box 两种类型的区别。

答:

  • &dyn TraitA没有所有权,而Box有所有权。
  • rust生命周期的独特设计,导致了该语言需要设计一些处理方式应对特殊情况,比如生命周期的标注(主要是给编译器进行代码处理时的提示)。事实上,我们在日常开发中应该避免一些陷入复杂情况的方式:比如,传入参数都用引用(borrow),传出结果都应该是owner。rust也为我们提供了处理各种情况的工具。所以,一般来说,我们应该在传入参数的时候用&dyn T,传出结果用Box
  • 关联类型之所以要单独设计,因为编译器可以通过输入判断具体类型,而无法推断出输出类型,所以,输出的类型需要明确指定
  • 1. &dyn TraitA 是一个引用,引用指向实现了TraitA特征的具体类型,没有这个具体类型的所有权,相当于借用。 2. Box 是一个智能指针,将实现了TraitA特征的具体类型保存在堆上,并且拥有这个具体类型的所有权。

你可能感兴趣的:(Rust,语言从入门到实战,唐刚,学习笔记,rust,开发语言,后端)