暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

写给 Web 开发者的 Rust 语言入门教程(15.泛化特化)

最后的叶子的奇妙小屋 2021-07-27
567

之前的文章:

0.引言

1.模块基础

2.变量

3.字符串

4.分支循环

5.数据表示

6.模块化

7.接口抽象

8.泛型

9.错误处理

10.内存布局

11.所有权

12.引用传递

13.引用字段

14.引用计数

本章主要介绍 rust 的 boxed trait 相关机制,以及与运行时多态相关的语言特性。

Boxed Trait

在 rust 中,数组和 Vec 只能存放同一数据类型的数据。试图存放多个不同类型的数据会导致编译失败,例如:

    struct Fruit {
    name: String,
    durability_days: u32,
    price: u32,
    }


    struct Book {
    name: String,
    author: String,
    price: u32,
    }


    fn main() {
    let apple = Fruit {
    name: format!("Apple"),
    durability_days: 30,
    price: 5,
    };
    let geometry = Book {
    name: format!("Geometry"),
    author: format!("Euclid"),
    price: 60,
    };
    let cart = vec![apple, geometry];
    // 编译失败!
    // apple 和 geometry 并不是同一个类型
    }

    在上例中,你不能将 Fruit Book 直接放在 cart 中,因为它们是不同的数据类型。要解决这个问题,可以定义一个 Product trait ,让 Fruit Book 都实现这个 trait ,然后就可以用下例中的方式将它们都放在 cart 中:

      struct Fruit {
      name: String,
      durability_days: u32,
      price: u32,
      }


      struct Book {
      name: String,
      author: String,
      price: u32,
      }


      trait Product {
      fn price(&self) -> u32;
      }


      // 为 Fruit 实现 Product
      impl Product for Fruit {
      fn price(&self) -> u32 {
      self.price
      }
      }


      // 为 Book 实现 Product
      impl Product for Book {
      fn price(&self) -> u32 {
      self.price
      }
      }


      fn main() {
      let apple = Fruit {
      name: format!("Apple"),
      durability_days: 30,
      price: 5,
      };
      let geometry = Book {
      name: format!("Geometry"),
      author: format!("Euclid"),
      price: 60,
      };
      // cart 中的每一项是 Box<dyn Product> 类型
      let cart: Vec<Box<dyn Product>> = vec![
      // 将 Fruit 放入 Box 中
      // Box<Fruit> 可以用作 Box<dyn Product>
      Box::new(apple),
      // Box<Book> 也可以用作 Box<dyn Product>
      Box::new(geometry),
      ];
      let mut total_price = 0;
      for item in &cart {
      // 每个 item 都是 &Box<dyn Product>
      // 可以调用 trait Product 中的函数
      total_price += item.price();
      }
      println!("{}", total_price); // 输出 65
      }

      这种方式称为 boxed trait ,即 Box 内的数据类型可以被转换为该数据类型的一个 trait 。这样,可以将这个 trait 的多个具体数据类型“泛化”成一个“泛用的”数据类型。

      不过,有些 trait 并不能这样使用,典型的例子是 trait 中包含的函数参数或返回值中有 Self

        trait Product {
        fn price(&self) -> u32;
            fn my_trait_fn(&self) -> Self;
        }
        // 不能作为 Box<dyn Product> 使用!

        这种情况下,需要加上一个限制 Self: Sized

          trait Product {
          fn price(&self) -> u32;
              // 下面这个函数不对 Box<dyn Product> 使用
          fn my_trait_fn(&self) -> Self where Self: Sized;
          }

          这样就可以作为 boxed trait 使用了。(不过,受限的函数本身只能在 boxed trait 以外的情况下使用。)

          Downcast

          通常 boxed trait 只能调用 trait 中定义的函数。有些情况下,希望调用到具体数据类型的关联函数,此时可以将 boxed trait “特化”为具体数据类型。这个过程称为 downcast 。

          第三方 crate downcast-rs 可以辅助实现对 boxed trait 的 downcast 。首先在 Cargo.toml 中引入:

            [dependencies]
            downcast-rs = "1.2"

            具体用法示例:

              use downcast_rs::{Downcast, impl_downcast};


              struct Fruit {
              name: String,
              durability_days: u32,
              price: u32,
              }


              struct Book {
              name: String,
              author: String,
              price: u32,
              }


              // 声明为支持 Downcast 的 trait
              trait Product: Downcast {
              fn price(&self) -> u32;
              }


              // 为 Product 添加 downcast 支持
              impl_downcast!(Product);


              impl Product for Fruit {
              fn price(&self) -> u32 {
              self.price
              }
              }


              impl Product for Book {
              fn price(&self) -> u32 {
              self.price
              }
              }


              fn main() {
              let apple = Fruit {
              name: format!("Apple"),
              durability_days: 30,
              price: 5,
              };
              let geometry = Book {
              name: format!("Geometry"),
              author: format!("Euclid"),
              price: 60,
              };
              let cart: Vec<Box<dyn Product>> = vec![
              Box::new(apple),
              Box::new(geometry),
              ];
              for item in &cart {
              // 尝试 downcast 为 &Fruit 类型
              if let Some(fruit) = item.downcast_ref::<Fruit>() {
              // 如果是 Fruit 类型,执行这里
              println!("{}", fruit.durability_days); 输出 30
              }
              }
              }

              Boxed Trait 和 enum 的选用

              需要注意的是, boxed trait 是有开销的。具体来说, boxed trait 附带有一个“具体是哪个数据类型”的信息,并在运行期间,根据具体的数据类型来选用 trait 函数的对应实现。

              相对而言,由于 enum 是零开销的,实践中常用 enum 来代替 boxed trait ,例如:

                struct Fruit {
                name: String,
                durability_days: u32,
                price: u32,
                }


                struct Book {
                name: String,
                author: String,
                price: u32,
                }


                enum Product {
                Fruit(Fruit),
                Book(Book),
                }


                fn main() {
                let apple = Fruit {
                name: format!("Apple"),
                durability_days: 30,
                price: 5,
                };
                let geometry = Book {
                name: format!("Geometry"),
                author: format!("Euclid"),
                price: 60,
                };
                let cart: Vec<Product> = vec![
                Product::Fruit(apple),
                Product::Book(geometry),
                ];
                let mut total_price = 0;
                for item in &cart {
                total_price += match item {
                Product::Book(b) => b.price,
                Product::Fruit(f) => f.price,
                };
                }
                println!("{}", total_price); // 输出 65
                }

                这种使用 enum 的方式具有更好的性能。不过它仅适用于 Product 的种类可以逐个列举的情况(上例中只有 Fruit Book 两类),如果 Product 的种类很多,就很难用甚至不能用这种方法了。

                此外,这种 enum 的方式会往往需要大量的 match 来逐个处理每个分支,使得代码比较难读。对于这个问题,第三方 crate  enum_dispatch 提供了一种很好的解决方式,让代码看起来更像 boxed trait ,但具有 enum 的性能。首先在 Cargo.toml 中引入:

                  [dependencies]
                  enum_dispatch = "0.3"

                  具体用法示例:

                    use enum_dispatch::enum_dispatch;


                    struct Fruit {
                    name: String,
                    durability_days: u32,
                    price: u32,
                    }


                    struct Book {
                    name: String,
                    author: String,
                    price: u32,
                    }


                    // 列举 Product 所有可能的具体类型
                    #[enum_dispatch]
                    enum Product {
                    Fruit,
                    Book,
                    }


                    // 定义 Product 所需的 trait
                    #[enum_dispatch(Product)]
                    trait ProductTrait {
                    fn price(&self) -> u32;
                    }


                    impl ProductTrait for Fruit {
                    fn price(&self) -> u32 {
                    self.price
                    }
                    }


                    impl ProductTrait for Book {
                    fn price(&self) -> u32 {
                    self.price
                    }
                    }


                    fn main() {
                    let apple = Fruit {
                    name: format!("Apple"),
                    durability_days: 30,
                    price: 5,
                    };
                    let geometry = Book {
                    name: format!("Geometry"),
                    author: format!("Euclid"),
                    price: 60,
                    };
                    let cart: Vec<Product> = vec![
                    apple.into(),
                    geometry.into(),
                    ];
                    let mut total_price = 0;
                    for item in &cart {
                    total_price += item.price();
                    }
                    println!("{}", total_price); // 输出 65
                    }

                    这种方法依然只适用于 Product 具体类型可以被逐个列举的情况。

                    boxed trait 还有另一些更重要、不可替代的使用场景,这将在之后的文章中介绍。

                    文章转载自最后的叶子的奇妙小屋,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论