1. 程式人生 > 其它 >rust為什麼顯示不了國服_C++工程師的Rust遷移之路(5)- 繼承與組合 - 下

rust為什麼顯示不了國服_C++工程師的Rust遷移之路(5)- 繼承與組合 - 下

技術標籤:rust為什麼顯示不了國服

2020-11-25 更新:

  1. 修正了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

參考

  1. ^Duck typinghttps://en.wikipedia.org/wiki/Duck_typing
  2. ^在Linux下SIGUSR1等於10