一、trait

trait有点类似go的interface,但和go的接口有点不一样,他是静态分发的。静态分发可能有点不好理解。下面聊dyn就明白了。 rust里面的trait可以实现多态。

1.定义trait

定义直接使用trait后跟trait

1
2
3
4
// work trait
trait Person {
    fn work(&self);
}

2.给类型实现trait里面的方法

这里定义了两个角色ProgrammerTeacher,他们都实现了work方法。

语法impl trait_name for struct_name

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 程序员
struct Programmer {}

// 老师
struct Teacher {}

// 给老师实现work方法
impl Person for Teacher {
    fn work(&self) {
        println!("Teacher:每年有寒暑假");
    }
}

// 给程序员实现work方法
impl Person for Programmer {
    fn work(&self) {
        println!("Programmer:每周有996");
    }
}

3. 使用trait

由于我们给ProgrammerTeacher都实现work,现在在一个包装函数里面调用就行,理想化是参数不同,输出不同。 先使用arg_name:impl trait_name声明函数参数。p.work()调用,就完事

1
2
3
4
5
6
7
8
9
// p是一个实现Person函数
fn do_work(p: impl Person) {
    p.work()
}

fn main() {
    do_work(Programmer {});
    do_work(Teacher {});
}

二、dyn

如果说rust什么最像go的interface,那一定是dyn。为什么需要dyn。修改下上面的例子,以年龄作为分界线,之前做Programmer,之后当Teacher。 这里稍作准备,给每个角色加age。然后洋洋洒洒写下如下代码 if else 生成不同的对象,然后运行。

1
2
3
4
5
6
7
   fn select_person(age: i32) -> impl Person {
       if age <= 35 {
           Programmer {}
       } else {
           Teacher {}
       }
   }

1.报错

然后不出意外,又被rust编译器打脸。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
   error[E0308]: `if` and `else` have incompatible types
     --> src/bin/dyn-trait.rs:36:9
      |
   33 | /     if age <= 35 {
   34 | |         Programmer {}
      | |         ------------- expected because of this
   35 | |     } else {
   36 | |         Teacher {}
      | |         ^^^^^^^^^^ expected struct `Programmer`, found struct `Teacher`
   37 | |     }
      | |_____- `if` and `else` have incompatible types
      |
   help: you could change the return type to be a boxed trait object
      |
   32 | fn select_person(age: i32) -> Box<dyn Person> {
      |                               ^^^^^^^       ^
   help: if you change the return type to expect trait objects, box the returned expressions
      |
   34 |         Box::new(Programmer {})
   35 |     } else {
   36 |         Box::new(Teacher {})
      |

   error: aborting due to previous error

从上面的报错信息,可以看出,impl Person。已经实现为Programmer。else部分找到Teacher。然后编译器和贴心,也给出修改方法。

2.修改之后的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn dyn_do_work(p: Box<dyn Person>) {
    p.work();
}

fn select_person(age: i32) -> Box<dyn Person> {
    if age <= 35 {
        Box::new(Programmer { age: age })
    } else {
        Box::new(Teacher { age: age })
    }
}

fn main() {
    dyn_do_work(select_person(30));
    dyn_do_work(select_person(36));
}

ok,成功运行。rust编译器需要知道每个函数的返回类型需要多大。所以函数编译之后就需要一个具体的类型。 我们在上面if else代码块里面返回两个不同类型,他们都实现trait。在编译之后会报错,选择第一个类型,类型固化就和第二个类型不一样,这也是上面报错的原因。 在函数里面动态选择不同实现是个很常见的需求,所以rust使用dyn关键字解决。Box返回指向堆的指针。绕过类型不一样的限制。 dyn作为关键字,在语义上和trait有所区分。简单来说一个静态分发一个动态分发

三、完整代码地址如下

https://github.com/guonaihong/rust-example/tree/main/trait-example