rust為什麼顯示不了國服_C++工程師的Rust遷移之路(5)- 繼承與組合 - 下
技術標籤:rust為什麼顯示不了國服
2020-11-25 更新:
- 修正了C++ 20中的concept語法
在上一篇文章 https://zhuanlan.zhihu.com/p/76740667 中,我介紹多型、靜態分發和動態分發的概念,以及他們各自在C++和Rust中的實現方式。
在本文中,我會重點講Rust中的Trait實現的靜態分發與C++ 20(準確的說,現在還叫做C++ 2a)中的concepts的區別。
在具體介紹這個區別之前,我想跟大家介紹一個概念,叫做duck typing(鴨子型別)。
鴨子型別
呃……你沒有看錯,這個鴨子就是你平常理解的那個鴨子,我也沒有翻譯錯……
鴨子型別[1]
如果它走起來像鴨子,也跟鴨子一樣發出嘎嘎的叫聲,那麼它就是鴨子
聽起來似乎非常無厘頭,但這個模式實際上被廣泛的應用於多種語言。
在C++中的應用
template <typename T> concept Stream = requires(T a, std::uint8_t* mut_buffer, size_t size, const std::uint8_t* buffer) { { a.read(mut_buffer, size) } -> std::convertible_to<size_t>; { a.write(buffer, size) } -> std::convertible_to<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 Thread = requires(T a, int signal) {
{ a.kill(signal) };
};
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,歡迎大家前去玩耍。
Markers
在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結構體不能繼承,以及為什麼。敬請期待。
延伸閱讀
上一篇
黃珏珅:C++工程師的Rust遷移之路(4)- 繼承與組合 - 中zhuanlan.zhihu.com下一篇
黃珏珅:C++工程師的Rust遷移之路(6)- 繼承與組合 - 後zhuanlan.zhihu.com參考
- ^Duck typinghttps://en.wikipedia.org/wiki/Duck_typing
- ^在Linux下SIGUSR1等於10