本文发表于知乎专栏:https://zhuanlan.zhihu.com/p/78333162
已获得作者授权。
在上一篇文章 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中,是不允许这种情况出现的,不许显式的生命类型实现的是哪个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,欢迎大家前去玩耍。
在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结构体不能继承,以及为什么。敬请期待。