0
点赞
收藏
分享

微信扫一扫

C++工程师的Rust迁移之路(5)- 继承与组合 - 下


在上一篇文章 zhuanlan.zhihu.com/p/76 中,我介绍多态、静态分发和动态分发的概念,以及他们各自在C++和Rust中的实现方式。

在本文中,我会重点讲Rust中的Trait实现的静态分发与C++ 20(准确的说,现在还叫做C++ 2a)中的concepts的区别。

在具体介绍这个区别之前,我想跟大家介绍一个概念,叫做duck typing(鸭子类型)。

鸭子类型

呃……你没有看错,这个鸭子就是你平常理解的那个鸭子,我也没有翻译错……

鸭子类型[1]是鸭子测试的一个应用:

如果它走起来像鸭子,也跟鸭子一样发出嘎嘎的叫声,那么它就是鸭子

听起来似乎非常无厘头,但这个模式实际上被广泛的应用于多种语言。

在C++中的应用

template <typename T>
concept bool Stream = requires(T a) {
{ a.read(std::uint8_t*, size_t) } -> size_t;
{ a.write(const std::uint8_t*, size_t) } -> size_t;
};

class Console { ... };
class FileStream { ... };

在Golang中的应用

type Stream interface {
Read(uint32) []byte
Write([]byte) uint32
}

type Console struct { ... }
type FileStream struct { ... }

func (c Console) Read(size uint32) []byte {
...
}

func (c Console) Write(data []byte) uint32 {
...
}

在上面的两个例子中,我们可以注意到,Console和FileStream这两个类型都没有显示的声明自己兼容Stream concept(interface),但在编译阶段,编译器可以根据他们实现的方法来判断他们支持Stream要求的操作,从而实现多态。

这个功能看似非常诱人,省去了显式声明的麻烦,但也带来了问题。

鸭子类型的局限性

程序员的造词能力通常是非常匮乏的(大家每次要给变量命名时的抓耳挠腮可以证明这一点),所以非常容易在方法名上重复,但在两个语境中又可能具有完全不同的语义。

举个例子:

template <typename T>
concept bool Thread = requires(T a) {
{ a.kill(int signal) } -> void;
};

class DuckFlock {
public:
void kill(int amount);
};

void nofity_thread(Thread& t) {
t.kill(SIGUSR1);
}

原本我以为给鸭群发了一个信号,让它们打印一下状态,结果一不小心就杀掉了10只鸭子[2],真的只能召唤华农兄弟了。

Rust的设计

在Rust中,是不允许这种情况出现的,不许显式的生命类型实现的是哪个trait:

trait Thread {  fn kill(&mut self, signal:i32);}trait Flock {  fn kill(&mut self, amount:i32);}struct DuckFlock {  ducks: i32
}impl DuckFlock { pub fn new(amount: i32) -> DuckFlock { DuckFlock{ ducks: amount } }}impl Thread for DuckFlock { fn kill(&mut self, signal: i32) { if signal == 10 { println!("We have {} ducks", self.ducks); } else { println!("Unknown signal {}", signal); } }}impl Flock for DuckFlock { fn kill(&mut self, amount: i32) { self.ducks -= amount; println!("{} ducks killed", amount); }}fn main() { let mut flock = DuckFlock::new(100); { let thread:&mut Thread = &mut flock; thread.kill(10); } { let flock:&mut Flock = &mut flock; flock.kill(10); } { let thread:&mut Thread = &mut flock; thread.kill(10); }}

同样的,这个例子我也放到Rust Playground,欢迎大家前去玩耍。

Makers

在Rust中,由于实现Trait必须要显式声明,这就衍生出了一种特殊类型的trait,它不包含任何的函数要求:

trait TonyFavorite {}trait Food {    fn name(&self) -> String;}struct PeikingDuck;impl Food for PeikingDuck {    fn name(&self) -> String {        "Peiking Duck".to_owned()    }}impl TonyFavorite for PeikingDuck {}struct Liver;impl Food for Liver {    fn name(&self) -> String {        "Liver".to_owned()    }}fn eat<T: Food + TonyFavorite>(food: T) {    println!("Tony only eat his favorite food like {}", food.name());}fn main() {    eat(PeikingDuck);    // eat(Liver); // compile error}

这里例子的Playground在此。

事实上,在Rust中,类似的Marker还有非常多,比如Copy、Sync、Send等等。在后续的文章中,再跟大家逐一解释这些trait的含义与妙用。

在下一节的文章中,我会介绍Rust类型系统和C++类型系统最大的不同之一:Rust结构体不能继承,以及为什么。敬请期待。


举报

相关推荐

0 条评论