💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:
目录
定义
SpreadsheetCell
枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface,GUI)工具的例子,它通过遍历列表并调用每一个项目的
draw
方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做gui
的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如Button
或TextField
。在此之上,gui
的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加Image
,另一个可能会增加SelectBox
。这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是
gui
需要记录一系列不同类型的值,并需要能够对其中每一个值调用draw
方法。这里无需知道调用draw
方法时具体会发生什么,只要该值会有那个方法可供我们调用。在拥有继承的语言中,可以定义一个名为
Component
的类,该类上有一个draw
方法。其他的类比如Button
、Image
和SelectBox
会从Component
派生并因此继承draw
方法。它们各自都可以覆盖draw
方法来定义自己的行为,但是框架会把所有这些类型当作是Component
的实例,并在其上调用draw
。不过 Rust 并没有继承,我们得另寻出路。
🎯定义通用行为的trait
为了实现
gui
所期望的行为,让我们定义一个Draw
trait,其中包含名为draw
的方法。接着可以定义一个存放 trait 对象(trait object)的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的 trait 方法的表。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。
之前提到过,Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和
impl
块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说 则 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。pub trait Draw { fn draw(&self); }
一个存放了名叫
components
的 vector 的结构体Screen
。这个 vector 的类型是Box<dyn Draw>
,此为一个 trait 对象:它是Box
中任何实现了Draw
trait 的类型的替身。pub struct Screen { pub components: Vec<Box<dyn Draw>>, }
在
Screen
结构体上,我们将定义一个run
方法,该方法会对其components
上的每一个组件调用draw
方法impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。
pub struct Screen<T: Draw> { pub components: Vec<T>, } impl<T> Screen<T> where T: Draw, { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }
这限制了
Screen
实例必须拥有一个全是Button
类型或者全是TextField
类型的组件列表。如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。另一方面,通过使用 trait 对象的方法,一个
Screen
实例可以存放一个既能包含Box<Button>
,也能包含Box<TextField>
的Vec<T>
。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
🎯实现trait
现在来增加一些实现了
Draw
trait 的类型。我们将提供Button
类型。再一次重申,真正实现 GUI 库超出了本书的范畴,所以draw
方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个Button
结构体可能会拥有width
、height
和label
字段:pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { // code to actually draw a button } }
在
Button
上的width
、height
和label
字段会和其他组件不同,比如TextField
可能有width
、height
、label
以及placeholder
字段。每一个我们希望能在屏幕上绘制的类型都会使用不同的代码来实现Draw
trait 的draw
方法来定义如何绘制特定的类型,像这里的Button
类型(如上提到的并不包含任何实际的 GUI 代码)。除了实现Draw
trait 之外,比如Button
还可能有另一个包含按钮点击如何响应的方法的impl
块。这类方法并不适用于像TextField
这样的类型。如果一些库的使用者决定实现一个包含
width
、height
和options
字段的结构体SelectBox
,并且也为其实现了Draw
traituse gui::Draw; struct SelectBox { width: u32, height: u32, options: Vec<String>, } impl Draw for SelectBox { fn draw(&self) { // code to actually draw a select box } }
库使用者现在可以在他们的
main
函数中创建一个Screen
实例。至此可以通过将SelectBox
和Button
放入Box<T>
转变为 trait 对象再放入Screen
实例中。use gui::{Button, Screen}; fn main() { let screen = Screen { components: vec![ Box::new(SelectBox { width: 75, height: 10, options: vec![ String::from("Yes"), String::from("Maybe"), String::from("No"), ], }), Box::new(Button { width: 50, height: 10, label: String::from("OK"), }), ], }; screen.run(); }
当编写库的时候,我们不知道何人会在何时增加
SelectBox
类型,不过Screen
的实现能够操作并绘制这个新类型,因为SelectBox
实现了Draw
trait,这意味着它实现了draw
方法。这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 鸭子类型(duck typing)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中
Screen
上的run
实现中,run
并不需要知道各个组件的具体类型是什么。它并不检查组件是Button
或者SelectBox
的实例。通过指定Box<dyn Draw>
作为components
vector 中值的类型,我们就定义了Screen
为需要可以在其上调用draw
方法的值。
🎯trait对象执行分发
当对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行 静态分发(static dispatch)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 动态分发 (dynamic dispatch)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的场景下,编译器会生成负责在运行时确定该调用什么方法的代码。
当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例 17-5 和可以支持示例 17-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。