Skip to content

trait上带类型参数

trait上也是可以带类型参数的,形式像下面这样:

rust
trait TraitA<T> {}

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

注意,这个时候 TraitA<T> 是一个整体,表示一个trait。比如 TraitA<u8>TraitA<u32> 就是两个不同的trait,这里单独把TraitA拿出来说是没有意义的。

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

rust
impl<T> TraitA<T> for Atype {}

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

rust
impl TraitA<u8> for Atype {}

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

rust
trait TraitA<T> {}
struct Atype<U> {
    a: U,
}
impl<T, U> TraitA<T> for Atype<U> {}

这些类型参数都是可以在impl时被约束的,像下面这样:

rust
use std::fmt::Debug;

trait TraitA<T> {}
struct Atype<U> {
    a: U,
}
impl<T, U> TraitA<T> for Atype<U>
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

代码如下:

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

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

// 为 Point 实现 Add<Point> 这个 trait
impl Add<Point> 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<i32> 这个 trait
impl Add<i32> 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<T> 这个trait,带一个类型参数T,还带一个关联类型 Output。

对Point类型,我们实现了两个trait: Add<Point>Add<i32>。注意这已经是两个不同的trait了,所以能对同一个类型实现。前面我们反复强调过,同一个trait只能对一个类型实现一次。

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

通过这种形式,我们在同一个类型上实现了同名方法(add方法)参数类型的多种形态。在这里看起来就是,Point实例的add方法既可以接收Point参数,又可以接收i32参数,Rustc小助手可以根据不同的参数类型自动找到对应的方法调用。在Java、C++这些语言中,有语言层面的函数重载特性来支持这种功能,Rust中自身并不直接支持函数重载特性,但是它用trait就轻松实现了同样的效果,这是一种全新的思路。

trait 类型参数的默认实现

定义带类型参数的trait的时候,可以为类型参数指定一个默认类型,比如 trait TraitA<T = u64> {}。这样使用时, impl TraitA for SomeType {} 就等价于 impl TraitA<u64> for SomeType {}

我们来看一个完整的例子。

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

// 这个默认类型为i32
trait TraitB<T = i32> {
    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<String> for SomeType {
    fn func(t: String) {}
}
// 这里不省略类型参数,明确指定类型参数为String
impl TraitB<String> for SomeType {
    fn func2(t: String) {}
}

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

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

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

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

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

现在你可能会有些疑惑:trait上的类型参数和关联类型都具有延迟具化的特点,那么它们的区别是什么呢?为什么要设计两种不同的机制呢?

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

具体到这两者来说,它们主要有两点不同。

  1. 类型参数可以在impl 类型的时候具化,也可以延迟到使用的时候具化。而关联类型在被impl时就必须具化。

  2. 由于类型参数和trait名一起组成了完整的trait名字,不同的具化类型会构成不同的trait,所以看起来同一个定义可以在目标类型上实现“多次”。而关联类型没有这个作用。

下面我们分别举例说明。

对于第一点,请看下面的示例:

rust
use std::fmt::Debug;

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

impl<T> TraitA<T> for Atype
where
    T: Debug + PartialEq,  // 将TraitA<T>实现到类型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类型,所以下面的代码也可以。

rust
use std::fmt::Debug;

trait TraitA<T>
where
    T: Debug,
{
    fn play(&self, _t: T) {}
}
struct Atype;

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

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

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

对应的,对关联类型来说,如果你在impl时不对其具化,就无法编译通过。所以对于第二点,我也给出一个例子来解释。我们把前面对Point类型实现Add的模型尝试用关联类型实现一遍。

rust
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);

编译器会抱怨:

rust
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

下面我们开始讲trait object。

我们从一个函数要返回不同的类型说起。比如一个常见的需求,要在一个Rust函数中返回可能的多种类型,应该怎么写?

如果我们写成返回固定类型的函数签名,那么它就只能返回那个类型。比如:

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

你想到的第一个办法可能是利用enum。

rust
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可以 无脑+强行 把它们揉在一起。enum聚合类型是编码时已知的类型,也就是说在聚合前,需要知道待聚合类型的边界,一旦定义完成,之后运行时就不能改动了,它是 封闭类型集

第二种办法是利用类型参数,我们试着引入一个类型参数,改写一下。

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

很明显,这种代码无法通过编译。提示:

rust
error[E0308]: mismatched types
 --> src/lib.rs:6:3
  |
4 | fn doit<T>() -> 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来返回,于是我们改出第二个版本。

rust
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 {
  T::new()
}

编译还是报错。

rust
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>() -> 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认识它。

第三个版本:

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: TraitA>() -> T {
  T::new()
}

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

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

为了解决上面那个问题,我们真的是费了不少力气。实际上,Rust提供了更优雅的方案来解决这个需求。Rust利用trait提供了一种特殊语法 impl trait,你可以看一下示例。

rust
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逻辑选择不同的分支返回不同的类型,就会遇到问题。

rust
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
  }
}

提示:

rust
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 的限制。

rust
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;
  }
}

但是还是报错。

rust
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分支时,就发现类型不匹配了。问题就在这里。你可以将条件分支顺序换一下,看一下报错的提示,加深印象。

那我们应该怎么处理这种问题呢?

好在,Rust还给我们提供了进一步的措施: trait object。形式上,就是在trait名前加 dyn 关键字修饰,在这个例子里就是 dyn TraitA。 dyn TraitName 本身就是一种类型,它和 TraitName 这个 trait 相关,但是它们不同,dyn TraitName 是一个独立的类型。

我们使用dyn TraitA改写上面的代码。

rust
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
  }
}

但是编译会报错。

rust
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<dyn TraitA> {
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,我们按照第二种建议改一下试试。

rust
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<dyn TraitA> {
    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<T>Box<T> 的作用是可以保证获得里面值的所有权,必要的时候会进行内存的复制,比如把栈上的值复制到堆中去。一旦值到了堆中,就很容易掌握到它的所有权。

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

这里我们先暂停,我希望你可以用一点时间来回顾一下整个推导过程,这次令人印象深刻的类型“体操”值得我们多品味几次。

利用trait object传参

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

impl trait的示例:

rust
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<T: TraitA>(x: T) {}

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

dyn trait的示例:

rust
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示例展开后类似于下面这个样子。

rust
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都是消除类型参数的办法

那它们和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里面。

rust
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];
}

报错:

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

因为Vec中要求每一个元素是同一种类型,不能将不同的类型实例放入同一个Vec。而利用trait object,我们可以“绕”过这个限制。

请看示例:

rust
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 object这么好用,那是不是可以随便使用呢?不是的。除了前面提到的性能损失之外,还有一个问题,不是所有的trait都可以做dyn化,也就是说,不是所有的trait都能转成trait object使用。

哪些trait能用作trait object?

只有满足对象安全(object safety)的trait才能被用作trait object。Rust参考手册上有关于 object safety 的详细规则,比较复杂。这里我们了解常用的模式就行。

安全的trait object:

rust
trait TraitA {
    fn foo(&self) {}
    fn foo_mut(&mut self) {}
    fn foo_box(self: Box<Self>) {}
}

不安全的trait object:

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

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

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

  1. 不要在trait里面定义构造函数,比如new这种返回Self的关联函数。你可以发现,确实在整个Rust生态中都没有将构造函数定义在trait中的习惯。

  2. trait里面尽量定义传引用 &self 或 &mut self的方法,而不要定义传值 self 的方法。

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

标准库中的常用trait

Default

我们来看Default trait的定义以及对Default trait的实现和使用。

rust
trait Default {
    fn default() -> Self;
}
struct Color(u8, u8, u8);
impl Default for Color {
    // 默认颜色是黑色 (0, 0, 0)
    fn default() -> Self {
        Color(0, 0, 0)
    }
}

fn main() {
    let color = Color::default();
    // 或
    let color: Color = Default::default();
}

还有其他一些地方用到了Default,比如 Option<T>unwrap_or_default(),在类型参数上调用 default() 函数。

rust
fn paint(color: Option<Color>) {
    // 如果没有颜色参数传进来,就用默认颜色
    let color = color.unwrap_or_default();
    // ...
}

// 由于default()是在trait中定义的关联函数,因此可方便的由类型参数调用
fn guarantee_length<T: Default>(mut vec: Vec<T>, min_len: usize) -> Vec<T> {
    for _ in 0..min_len.saturating_sub(vec.len()) {
        vec.push(T::default());  // 这里用了 T::default() 这种形式
    }
    vec
}

前面讲过,如果是struct,还可以使用部分更新语法,这个时候其实是Default在发挥作用。

rust
#[derive(Default)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}
impl Color {
    fn new(r: u8, g: u8, b: u8) -> Self {
        Color {
            r,
            g,
            b,
        }
    }
}
impl Color {
    fn red(r: u8) -> Self {
        Color {
            r,
            ..Color::default()    // 注意这一句
        }
    }
    fn green(g: u8) -> Self {
        Color {
            g,
            ..Color::default()    // 注意这一句
        }
    }
    fn blue(b: u8) -> Self {
        Color {
            b,
            ..Color::default()    // 注意这一句
        }
    }
}

Rust标准库实际给我们提供了一个标注,也就是 #[derive()] 里面放 Default,方便我们为结构体自动实现Default trait。

rust
#[derive(Default)]
struct Color {
    r: u8,
    g: u8,
    b: u8
}

#[derive(Default)]
struct Color2(u8, u8, u8);

注意这里的细节,我们用 #[derive()] 在两个结构体上作了标注,这里面出现的这个 Default 不是trait,它是一个同名的派生宏(我们后面会讲到)。这种派生宏标注帮助我们实现了 Default trait。Rustc能正确区分Default到底是宏还是trait,因为它们出现的位置不一样。

为什么可以自动实现Default trait呢?因为Color里面的类型是基础类型u8,而u8是实现了Default trait的,默认值为 0。

Display

我们看Display trait的定义。

rust
trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

Display trait对应于格式化符号 "{}",比如 println!("{}", s),用于决定一个类型如何显示,其实就是把类型转换成字符串表达。Display需要我们自己手动去实现。

示例:

rust
use std::fmt;
#[derive(Default)]
struct Point {
    x: i32,
    y: i32,
}
// 为Point实现 Display
impl fmt::Display for Point {
    // 实现唯一的fmt方法,这里定义用户自定义的格式
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)  // write!宏向stdout写入
    }
}

fn main() {
    println!("origin: {}", Point::default());
    // 打印出 "origin: (0, 0)"
    // 在 format! 中用 "{}" 将类型表示/转换为 String
    let stringified = format!("{}", Point::default());
    assert_eq!("(0, 0)", stringified); // ✅
}

ToString

我们来看ToString trait 定义。

rust
trait ToString {
    fn to_string(&self) -> String;
}

它提供了一个 to_string() 方法,方便把各种类型实例转换成字符串。但实际上不需要自己去给类型实现ToString trait,因为标准库已经给我们做了总实现( 第 9 讲 提到过),像下面这个样子。

rust
impl<T: Display> ToString for T

也就是说,凡是实现了Display的就实现了ToString。这两个功能本质是一样的,就是把类型转换成字符串表达。只不过Display侧重于展现,ToString侧重于类型转换。下面这个示例证明这两者是等价的。

rust
#[test] // ✅
fn display_point() {
    let origin = Point::default();
    assert_eq!(format!("{}", origin), "(0, 0)");
}
#[test] // ✅
fn point_to_string() {
    let origin = Point::default();
    assert_eq!(origin.to_string(), "(0, 0)");
}
#[test] // ✅
fn display_equals_to_string() {
    let origin = Point::default();
    assert_eq!(format!("{}", origin), origin.to_string());
}

所以把一个符合条件的类型实例转换成字符串有两种常用方法。

rust
let s = format!("{}", obj);
// 或
let s = obj.to_string();

Debug

Debug 跟 Display 很像,也主要是用于调试打印。打印就需要指定格式,区别在于Debug trait 是配对 "{:?}" 格式的,Display是配对 "{}" 的。它们本身都是将类型表示或转换成 String 类型。一般来说,Debug的排版信息比Display要多一点,因为它是给程序员调试用的,不是给最终用户看的。Debug还配套了一个美化版本格式 "{:#?}",用来把类型打印得更具结构化一些,适合调试的时候查看,比如json结构会展开打印。

Rust标准库提供了Debug宏。一般来说,我们都是以这个宏为目标类型自动生成Debug trait,而不是由我们自己手动去实现,这一点和Display正好相对,std标准库里并没有提供一个 Display 宏,来帮助我们自动实现 Display trait,需要我们手动实现它。

再提醒你一下,Rust的类型能够自动被derive的条件是,它里面的每个元素都能被derive,比如下面这个结构体里的每个字段,都是i32类型的,这种基础类型在标准库里已经被实现过Debug trait了,所以可以直接在Point上做derive为Point类型实现Debug trait。这个原则适用于所有trait,后面不再赘述。

rust
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

PartialEq和Eq

如果一个类型上实现了PartialEq,那么它就能比较两个值是否相等。这种可比较性满足数学上的对称性和传递性,我们通过两个例子具体来看。

  • 对称性(symmetry): a == b 导出 b == a

  • 传递性(transitivity): a == b && b == c 导出 a == c

而Eq定义为PartialEq的subtrait,在PartialEq的对称性和传递性的基础上,又添加了自反性,也就是对所有 a 都有 a == a。最典型的就是Rust中的浮点数只实现了PartialEq,没实现Eq,因为根据IEEE的规范,浮点数中存在一个NaN,它不等于自己,也就是 NaN ≠ NaN。而对整数来说,PartialEq和Eq都实现了。

如果一个类型,它的所有字段都实现了PartialEq,那么使用标准库中定义的PartialEq派生宏,我们可以为目标类型自动实现可比较能力,用==号,或者用 assert_eq!() 做判断。

rust
#[derive(PartialEq, Debug)]    // 注意这一句
struct Point {
    x: i32,
    y: i32,
}

fn example_assert(p1: Point, p2: Point) {
    assert_eq!(p1, p2);        // 比较
}

fn example_compare_collections<T: PartialEq>(vec1: Vec<T>, vec2: Vec<T>) {
    if vec1 == vec2 {    // 比较
        // some code
    } else {
        // other code
    }
}

PartialOrd和Ord

PartialOrd和PartialEq差不多,PartialEq只判断相等或不相等,PartialOrd在这个基础上进一步判断是小于、小于等于、大于还是大于等于。可以看到,它就是为排序功能准备的。

PartialOrd被定义为 PartialEq的subtrait。它们在类型上可以用过程宏一起derive实现。

rust
#[derive(PartialEq, PartialOrd)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(PartialEq, PartialOrd)]
enum Stoplight {
    Red,
    Yellow,
    Green,
}

类似的,Ord 定义为 Eq + PartialOrd 的 subtrait。如果我们为一个类型实现了 Ord,那么对那个类型的所有值,我们可以做出一个严格的总排序,比如u8,我们可以严格地从0排到255,形成一个确定的从小到大的序列。

同样的,浮点数实现了 PartialOrd,但是没实现 Ord。

由于Ord严格的顺序性,如果一个类型实现了Ord,那么这个类型可以被用作BTreeMap或BTreeSet的key。

BTreeMap、BTreeSet:相对于HashMap和HashSet,是两种可排序结构。

示例:

rust
use std::collections::BTreeSet;

#[derive(Ord, PartialOrd, PartialEq, Eq)]  // 注意这一句,4个都写上
struct Point {
    x: i32,
    y: i32,
}

fn example_btreeset() {
    let mut points = BTreeSet::new();
    points.insert(Point { x: 0, y: 0 }); // 作key值插入
}

// 实现了Ord trait的类型的集合,可调用 .sort() 排序方法
fn example_sort<T: Ord>(mut sortable: Vec<T>) -> Vec<T> {
    sortable.sort();
    sortable
}

运算符重载

Rust提供了一个Add trait,用来对加号(+)做自定义,也就是运算符重载。

你可以看一下Add的定义,它带一个类型参数Rhs,这里的类型参数可以是任意名字,默认类型是Self,一个关联类型Output,一个方法add()。

rust
trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

像下面我给出的这个示例一样去使用它就可以,非常简单。

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

// 为 Point 类型实现 Add trait,这样两个Point实例就可以直接相加
impl Add for Point {
    type Output = Point;
    fn add(self, rhs: Point) -> Point {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    let p3 = p1 + p2; // 这里直接用+号作用在两个Point实例上
    assert_eq!(p3.x, p1.x + p2.x); // ✅
    assert_eq!(p3.y, p1.y + p2.y); // ✅
}

实际上,Rust标准库提供了一套完整的与运算符对应的trait,你在 这里 可以找到可重载的运算符。你可以按类似的方式练习如何自定义各种运算符。

Clone

定义:

rust
trait Clone {
    fn clone(&self) -> Self;
}

这个trait给目标类型提供了clone()方法用来完整地克隆实例。使用标准库里面提供的Clone派生宏可以方便地为目标类型实现Clone trait。

比如:

rust
#[derive(Clone)]
struct Point {
    x: u32,
    y: u32,
}

因为每一个字段(u32类型)都实现了Clone,所以通过derive,自动为Point类型实现了Clone trait。实现后,Point的实例 point 使用 point.clone() 就可以把自己克隆一份了。

通过方法的签名,可以看到方法使用的是实例的不可变引用。

rust
    fn clone(&self) -> Self;

这里面有两种情况。

  • 第一种是已经拿到实例的所有权,clone一份生成一个新的所有权并被局部变量所持有。

  • 第二种是只拿到一个实例的引用,想拿到它的所有权,如果这个类型实现了Clone trait,那么就可以clone一份拿到这个所有权。

clone() 是对象的深度拷贝,可能会有比较大的额外负载,但是就大多数情况来说其实还好。不要担心在Rust中使用clone(),先把程序功能跑通最重要。Rust的代码,性能一般都不会太差,毕竟起点很高。

注:浅拷贝是按值拷贝一块连续的内存,只复制一层,不会去深究这个值里面是否有到其它内存资源的引用。与之相对,深拷贝就会把这些引用对象递归全部拷贝。

在Rust生态的代码中,我们经常看到clone()。为什么呢?因为它把对实例引用的持有转换成了对对象所有权的持有。一旦我们拿到了所有权,很多代码写起来就比较轻松了。

Copy

接下来,我们看Copy trait的定义。

rust
trait Copy: Clone {}

定义为Clone的subtrait,并且不包含任何内容,仅仅是一个标记(marker)。有趣的是,我们不能自己为自定义类型实现这个trait。比如下面这个示例就是不行的。

rust
impl Copy for Point {} // 这是不行的

但是Rust标准库提供了Copy过程宏,可以让我们自动为目标类型实现Copy trait。

rust
#[derive(Copy, Clone)]
struct SomeType;

因为Copy是Clone的subtrait。所以理所当然要把Clone trait也一起实现,我们在这里一次性derive过来。

Copy和Clone的区别是,Copy是浅拷贝只复制一层,不会去深究这个值里面是否有到其他内存资源的引用,比如一个字符串的动态数组。

rust
struct Atype {
    num: u32,
    a_vec: Vec<u32>,
}
fn main() {
    let a = Atype {
        num: 100,
        a_vec: vec![10, 20, 30],
    };
    let b = a;  // 这里发生了移动
}

代码第10行的操作是将a的所有权移动给b( 第 2 讲 的内容)。

如果我们给这个结构体实现了Clone trait的话,我们可以调用.clone() 来产生一份新的所有权。

rust
#[derive(Clone, Debug)]
struct Atype {
    num: u32,
    a_vec: Vec<u32>,    // 动态数组资源在堆内存中
}
fn main() {
    let a = Atype {
        num: 100,
        a_vec: vec![10, 20, 30],
    };
    let mut b = a.clone();  // 克隆,也将堆内存中的Vec资源部分克隆了一份
    b.num = 200;            // 更改b的值
    b.a_vec[0] = 11;
    b.a_vec[1] = 21;
    b.a_vec[2] = 31;

    println!("{a:?}");  // 对比两份值
    println!("{b:?}");
}
// 输出
Atype { num: 100, a_vec: [10, 20, 30] }
Atype { num: 200, a_vec: [11, 21, 31] }

通过例子可以看到,clone()一份新的所有权出来,b改动的值不影响a的值。

而一旦你想在 Atype 上实现 Copy trait的话,就会报错。

rust
error[E0204]: the trait `Copy` cannot be implemented for this type
 --> src/main.rs:1:10
  |
1 | #[derive(Copy, Clone, Debug)]
  |          ^^^^
...
4 |     a_vec: Vec<u32>,    // 动态数组资源在堆内存中
  |     --------------- this field does not implement `Copy`

它说动态数组字段 a_vec 没有实现Copy trait,所以你不能对Atype实现Copy trait。原因也好理解,Vec是一种所有权结构,如果你在它上面实现了Copy,那再赋值的时候,就会出现对同一份资源的两个指向,冲突了!

一旦一个类型实现了Copy,它就会具备一个特别重要的特性: 再赋值的时候会复制一份自身。那么就相当于新创建一份所有权。我们来看下面这个值全在栈上的类型。

rust
#[derive(Clone)]
struct Point {
    x: u32,
    y: u32,
}

fn main() {
  let a = Point {x: 10, y: 10};
  let b = a; // 这里发生了所有权move,a在后续不能使用了
}

我们对 Point 实现Clone和Copy。

rust
#[derive(Copy, Clone)]
struct Point {
    x: u32,
    y: u32,
}

fn main() {
  let a = Point {x: 10, y: 10};
  let b = a; // 这里发生了复制,a在后续可以继续使用
  let c = a; // 这里又复制了一份,这下有3份了
}

仔细体会一下,现在你知道我们在第2讲里面讲到的复制与移动的语义区别根源在哪里了吧!

你可能会问,Point结构体里面的字段其实全都是固定尺寸的,并且u32是copy语义的,按理说Point也是编译时已知固定尺寸的,为什么它默认不实现copy语义呢?

这其实是Rust设计者故意这么做的。因为Copy trait其实关联到赋值语法,仅仅从这个语法(let a = b;),很难一下子看出来这到底是copy还是move,它是一种 隐式行为

而在所有权的第一设计原则框架下,Rust默认选择了move语义。所以方便起见,Rust设计者就只让最基础的那些类型,比如u32、bool等具有copy语义。而用户自定义的类型,一概默认move语义。如果用户想给自定义类型赋予copy语义内涵,那么他需要显式地在那个类型上添加Copy的derive。

我们再回过头来看Clone,一个类型实现了Clone后,需要显式地调用 .clone() 方法才会导致对象克隆,这就在代码里面留下了足迹。而如果一个类型实现了Copy,那么它在用 = 号对实例再赋值的时候就发生了复制,这里缺少了附加的足迹。这就为潜在的Bug以及性能的降低埋下了隐患,并且由于没有附加足迹,导致后面再回头来审查的时候非常困难。

试想,如果是.clone(),那么我们只需要用代码搜索工具搜索代码哪些地方出现了clone函数就可以了。这个设计,在 Option<T>Result<T, E>unwrap() 系列函数上也有体现。

显式地留下足迹,是Rust语言设计重要的哲学之一

至于Copy为什么要定义成Clone的subtrait,而不是反过来,也是跟这个设计哲学相关。可以这么说,一般情况下,Rust鼓励优先使用Clone而不鼓励使用Copy,于是让开发者在derive Copy的时候,也必须derive Clone,相当于多打了几个字符,多付出了一点代价。也许开发者这时会想,可能Clone就能满足我的要求了,能在结构体上的derive宏里面少打几个字符,也是一件好事儿。

还有一个原因其实是,Clone和Copy在本质上其实是一样的,都是内存的按位复制,只是复制的规则有一些区别。

ToOwned

ToOwned相当于是Clone更宽泛的版本。ToOwned给类型提供了一个 to_owned() 方法,可以将引用转换为所有权实例。

常见的比如:

rust
let a: &str = "123456";
let s: String = a.to_owned();

通过查看标准库和第三方库接口文档,你可以确定有没实现这个trait。

Deref

Deref trait可以用来把一种类型转换成另一种类型,但是要在引用符号&、点号操作符 . 或其他智能指针的触发下才会产生转换。比如标准库里最常见的 &String 可以自动转换到 &str(请回顾 第 4 讲),就是因为String类型实现了Deref trait。

null

还有 &Vec<T> 可以自动转换为 &[T],也是因为 Vec[T] 实现了Deref。

null

到这里,Rust里很多魔法就开始揭开神秘面纱了。有了这些trait以及在各种类型上的实现,Rust让我们可以写出顺应直觉、赏心悦目、功能强大的代码。

你还可以在标准库文档中搜索Deref,查阅所有实现了Deref trait的 implementors

这里需要提醒你一下,有人尝试 用 Deref 机制去实现 OOP 继承,但是那是徒劳和不完整的,有兴趣的话你可以看一下我给出的链接。

Drop

Drop trait用于给类型做自定义垃圾清理(回收)。

rust
trait Drop {
    fn drop(&mut self);
}

实现了这个trait的类型的实例在走出作用域的时候,触发调用drop()方法,这个调用发生在这个实例被销毁之前。你可以看一下它的使用方式。

rust
struct A;
impl Drop for A {
  fn drop(&mut self){
    // 可以尝试在这里打印点东西看看什么时候调用
  }
}

一般来说,我们不需要为自己的类型实现这个trait,除非遇到特殊情况,比如我们要调用外部的C库函数,然后在C那边分配了资源,由C库里的函数负责释放,这个时候我们就要在Rust的包装类型(对C库中类型的包装)上实现Drop,并调用那个C库中释放资源的函数。课程最后两讲FFI编程中,你会看到Drop的具体使用。

闭包相关trait

标准库中有3个trait与闭包相关,分别是FnOnce、FnMut、Fn。你可以看一下它们的定义。

rust
trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}
trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}
trait Fn<Args>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}

前面我们也讲过,闭包就是一种能捕获上下文环境变量的函数。

rust
let range = 0..10;
let get_range_count = || range.count();

代码里的这个 get_range_count 就是闭包,range是被这个闭包捕获的环境变量。

虽然说它是一种函数,但是不通过fn进行定义。在Rust中,并不把这个闭包的类型处理成fn这种函数指针类型,而是有单独的类型定义。

那么,具体是什么类型呢?其实我们也不知道。闭包的类型是由Rust编译器在编译时确定的,并且在确定类型的时候要根据这个闭包捕获上下文环境变量时的行为来确定。

总的来说有三种行为(⚠️ 所有权三态再现)。

  1. 获取了上下文环境变量的所有权,对应 FnOnce。

  2. 只获取了上下文环境变量的&mut引用,对应 FnMut。

  3. 只获取了上下文环境变量的&引用,对应 Fn。

根据这三种不同的行为,Rust编译器在编译时把闭包生成为这三种不同类型中的一种。这三种不同类型的闭包,具体类型形式我们不知道,Rust没有暴露给我们。但是Rust给我们暴露了FnOnce、FnMut、Fn这3个trait,就刚好对应于那三种类型。结合我们前面讲到的trait object,就能在我们的代码中对那些类型进行描述了。

FnOnce代表的闭包类型只能被调用一次,比如;

rust
fn main() {
    let range = 0..10;
    let get_range_count = || range.count();
    assert_eq!(get_range_count(), 10); // ✅
    get_range_count(); // ❌
}

再调用就报错了。

FnMut代表的闭包类型能被调用多次,并且能修改上下文环境变量的值,不过有一些副作用,在某些情况下可能会导致错误或者不可预测的行为。比如:

rust
fn main() {
    let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
    let mut min = i32::MIN;
    let ascending = nums.into_iter().filter(|&n| {
        if n <= min {
            false
        } else {
            min = n;  // 这里修改了环境变量min的值
            true
        }
    }).collect::<Vec<_>>();
    assert_eq!(vec![0, 4, 8, 10, 15, 18], ascending); // ✅
}

Fn 代表的这类闭包能被调用多次,但是对上下文环境变量没有副作用。比如:

rust
fn main() {
    let nums = vec![0, 4, 2, 8, 10, 7, 15, 18, 13];
    let min = 9;
    let greater_than_9 = nums.into_iter().filter(|&n| n > min).collect::<Vec<_>>();
    assert_eq!(vec![10, 15, 18, 13], greater_than_9); // ✅
}

另外,fn这种函数指针,用在不需要捕获上下文环境变量的场景,比如:

rust
fn add_one(x: i32) -> i32 {
    x + 1
}

fn main() {
    let mut fn_ptr: fn(i32) -> i32 = add_one;  // 注意这里的类型定义
    assert_eq!(fn_ptr(1), 2); // ✅

    // 如果一个闭包没有捕捉环境变量,它可以通过类型转换转成 fn 类型
    fn_ptr = |x| x + 1; // same as add_one
    assert_eq!(fn_ptr(1), 2); // ✅
}

From<T>Into<T>

接下来,我们看 Rust 标准库中的两个关联的 trait From<T>Into<T>,它们用于类型转换。 From<T> 可以把类型T转为自己,而 Into<T> 可以把自己转为类型T。

rust
trait From<T> {
    fn from(T) -> Self;
}
trait Into<T> {
    fn into(self) -> T;
}

可以看到它们是互逆的trait。实际上,Rust只允许我们实现 From<T>,因为实现了From后,自动就实现了Into,请看标准库里的这个实现。

rust
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

对一个类型实现了From后,就可以像下面这样约束和使用。

rust
fn function<T>(t: T)
where
    // 下面这两种约束是等价的
    T: From<i32>,
    i32: Into<T>
{
    // 等价
    let example: T = T::from(0);
    let example: T = 0.into();
}

我们来举一个具体的例子。

rust
struct Point {
    x: i32,
    y: i32,
}
impl From<(i32, i32)> for Point {    // 实现从(i32, i32)到Point的转换
    fn from((x, y): (i32, i32)) -> Self {
        Point { x, y }
    }
}
impl From<[i32; 2]> for Point {      // 实现从[i32; 2]到Point的转换
    fn from([x, y]: [i32; 2]) -> Self {
        Point { x, y }
    }
}
fn example() {
    // 使用from()转换不同类型
    let origin = Point::from((0, 0));
    let origin = Point::from([0, 0]);
    // 使用into()转换不同类型
    let origin: Point = (0, 0).into();
    let origin: Point = [0, 0].into();
}

其实From是单向的。对于两个类型要互相转的话,是需要互相实现From的。

本身, From<T>Into<T> 都隐含了所有权, From<T> 的Self是具有所有权的, Into<T> 的T也是具有所有权的。 Into<T> 有个常用的比 From<T> 更自然的场景是,如果你已经拿到了一个变量,想把它变成具有所有权的值,Into写起来更顺手。因为 into() 是方法,而 from() 是关联函数。

比如:

rust
struct Person {
    name: String,
}
impl Person {
    // 这个方法只接收String参数
    fn new1(name: String) -> Person {
        Person { name }
    }
    // 这个方法可接收
    // - String
    // - &String
    // - &str
    // - Box<str>
    // - char
    // 这几种参数,因为它们都实现了Into<String>
    fn new2<N: Into<String>>(name: N) -> Person {
        Person { name: name.into() }  // 调用into(),写起来很简洁
    }
}

TryFrom TryInto

TryFrom<T>TryInto<T>From<T>Into<T> 的可失败版本。如果你认为转换可能会出现失败的情况,就选择这两个trait来实现。

rust
trait TryFrom<T> {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

trait TryInto<T> {
    type Error;
    fn try_into(self) -> Result<T, Self::Error>;
}

可以看到,调用 try_from()try_into() 后返回的是Result,你需要对Result进行处理。

FromStr

从字符串类型转换到自身。

rust
trait FromStr {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

其实我们前面已经遇到过这个trait,它就是字符串的 parse() 方法背后的trait。

rust
use std::str::FromStr;

fn example<T: FromStr>(s: &str) {
    // 下面4种表达等价
    let t: Result<T, _> = FromStr::from_str(s);
    let t = T::from_str(s);
    let t: Result<T, _> = s.parse();
    let t = s.parse::<T>(); // 最常用的写法
}

AsRef<T>

AsRef<T> 的定义类似下面这个样子:

rust
trait AsRef<T> {
    fn as_ref(&self) -> &T;
}

它把自身的引用转换成目标类型的引用。和Deref的区别是, **deref() 是隐式调用的,而 as_ref() 需要你显式地调用**。所以代码会更清晰,出错的机会也会更少。

AsRef<T> 可以让函数参数中传入的类型更加多样化,不管是引用类型还是具有所有权的类型,都可以传递。比如;

rust
// 使用 &str 作为参数可以接收下面两种类型
//  - &str
//  - &String
fn takes_str(s: &str) {
    // use &str
}
// 使用 AsRef<str> 作为参数可以接受下面三种类型
//  - &str
//  - &String
//  - String
fn takes_asref_str<S: AsRef<str>>(s: S) {
    let s: &str = s.as_ref();
    // use &str
}
fn example(slice: &str, borrow: &String, owned: String) {
    takes_str(slice);
    takes_str(borrow);
    takes_str(owned); // ❌
    takes_asref_str(slice);
    takes_asref_str(borrow);
    takes_asref_str(owned); // ✅
}

在这个例子里,具有所有权的String字符串也可以直接传入参数中了,相对于 &str 的参数类型表达更加扩展了一步。

你可以把 Deref 看成是隐式化(或自动化)+弱化版本的 AsRef<T>

Released under the GPL License.