栈上动态分发

说明

我们可以动态分发多个值,然而为了实现此功能,需要声明多个变量来绑定不同类型的对象。我们可以使用延迟条件初始化(deferred conditional initialization)来扩展生命周期,如下所示:

例子

  1. use std::io;
  2. use std::fs;
  3. # fn main() -> Result<(), Box<dyn std::error::Error>> {
  4. # let arg = "-";
  5. // 它们必须活的比 `readable`长, 因此先声明:
  6. let (mut stdin_read, mut file_read);
  7. // We need to ascribe the type to get dynamic dispatch.
  8. let readable: &mut dyn io::Read = if arg == "-" {
  9. stdin_read = io::stdin();
  10. &mut stdin_read
  11. } else {
  12. file_read = fs::File::open(arg)?;
  13. &mut file_read
  14. };
  15. // Read from `readable` here.
  16. # Ok(())
  17. # }

出发点

Rust默认是单态的代码。这就意味着对每个类型都要生成相对应的代码并且单独优化。这种模式虽然在热路径(hot path)上执行的很快,但是它空间上将非常臃肿。当性能不是致命关键的时候,我们还是要考虑考虑编译时间和cache的使用。

幸运的是,Rust允许我们使用动态分发,但是我们需要显式的声明。

优点

我们不用在堆上申请任何空间。既不用初始化任何用不上的东西,也不用单态化全部代码,便可同时支持FileStdin

缺点

这样写代码比使用Box实现的版本需要更多活动部件(moving parts):

  1. // We still need to ascribe the type for dynamic dispatch.
  2. let readable: Box<dyn io::Read> = if arg == "-" {
  3. Box::new(io::stdin())
  4. } else {
  5. Box::new(fs::File::open(arg)?)
  6. };
  7. // Read from `readable` here.

讨论

初学Rust之人通常会学到Rust需要所有变量在使用前需要初始化,所以常会忽略没有用到的变量可能不会初始化的问题。Rust付出大量工作来确保只有初始化过的值在离开作用域时会销毁。

上面这个例子符合我们所有的限制条件:

  • 所有的变量都在使用前初始化(这个例子中是借用)
  • 每个变量都只有单一类型。在我们的例子中,stdin对应Stdin类型,file对应File类型,readable对应&mut dyn Read类型
  • 每个借用的值的生命周期都比借用他们的场。

参阅

  • Finalisation in destructors and RAII guards can benefit from tight control over lifetimes.
  • For conditionally filled Option<&T>s of (mutable) references, one can initialize an Option<T> directly and use its .as_ref() method to get an optional reference.