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,欢迎大家前去玩耍。

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 typing https://en.wikipedia.org/wiki/Duck_typing
  2. ^在Linux下SIGUSR1等于10
编辑于 2019-09-25