The Rust Programming Language - 第17章 Rust的面向对象编程特性 - 17.2 为使用不同类型的值而设计的trait对象

17 Rust的面向对象编程特性

面向对象编程(OOP)是一种模式话编程方式

17.2 为使用不同类型的值而设计的trait对象

之前我们了解了vector,它有个缺陷就是只能存储同类型的元素,但是我们可以使用枚举或者结构体来存储不同类型的数据

但是呢,在实际中,我们希望这种类型的集合能够扩展。我们来通过一个例子说明这一点

我们将创建一个图形用户接口(Graphic User Interface,GUI)工具,它会遍历列表并调用每一个项目draw方法来将其绘制到屏幕上

我们也将创建一个叫做gui的库crate,它含一个GUI库的结构

这个GUI库包含一些可供开发者使用的类型,比如Button或TextField。在此之上,gui的用户希望创建自定义的可以绘制于屏幕上的类型。如:一个程序猿可以增加Image,另一个可以增加SelectBox

我们所知道的是gui需要记录一系列不同类型的值,并需要能够对其中每一个值调用draw方法。这里无需知道调用draw方法具体会发生什么,只要该值会有那个方法可供我们调用

在拥有继承的语言中,可以定义一个名为component的类,该类上有一个draw方法,其他类比如Button、Image和SelectBox会从component派生并因此继承draw方法。它们各自都可以覆盖draw方法来定义自己的行为,但是框架会把所有这些类型当作是Component的实例,并在其上调用draw。不过rust并没有继承,所以我们得另寻出路

定义通用行为的trait

定义一个Draw trait,其中包含名为draw的方法

pub trait Draw {
     fn draw(&self);
}

定义一个存放trait 对象的vector,trait对象指向一个实现了我们指定trait的类型的实例,以及一个用于在运行时查找该类型的trait方法的列表。我们通过某种指针来创建trait对象,例如&引用或Box智能指针,还有dyn keyword以及指定相关的trait

我们使用trait对象替代泛型或者具体类型

任何使用trait对象的位置,Rust的类型系统会在编译时确保任何在此上下文中使用的值会实现其trait对象的trait。如此就不需要在编译时就知晓所有可能的类型

pub struct Screen {
     pub components: Vec<Box<dyn Draw>,
}

Rust不刻意将枚举和结构体称为对象,以便于和其他语言区别

在结构体或者枚举中,字段数据和impl中的行为是分开的,而其他语言会讲数据和行为组合进一个称为对象的概念中

这里的trait将数据和行为组合,有点像其他语言中的对象。但是不能像trait 对象中增加数据,trait对象并不像其他语言中对象那么通用,其(trait对象)具体的作用是允许对通用行为进行抽象

impl Screen {
    pub fn run(&self) {
         for components in self.components.iter() {
              components.draw();
         }
    }
}

在Screen结构体上我们定义一个run方法,该方法会对其components上的每一个组件调用draw方法

这与定义使用了trait bound的泛型类型参数的结构体不同,泛型类型参数一次只能替代一个具体类型,而trait 对象则允许在运行时替代多种具体类型

例如,可以定义Screen 结构体来使用泛型和trait bound

impl<T> Screen<T> 
    where T: Draw{
        pub fn run(&self){
            for component in self.components.iter() {
                component.draw();
        }
    }
}

一种Screen结构体的替代实现,其run方法使用泛型和trait bound

这限制了Screen实例必须拥有一个全是Button类型或者全是TextField类型的组件列表,如果只需要相同类型的集合,则倾向于使用泛型和trait bound,因为其定义会在编译时采用具体类型进行单态化

另一方面,通过使用trait对象的方法,一个Screen实例可以存放一个技能包含Box,也能包含Box的Vec,让我们来看看它是如何工作的以及运行时对性能的影响

实现trait

现在我们来增加一些实现了Draw trait的类型,我们将提供button类型。因为要真正实现GUI库会超出我们所讨论的范围,所以draw方法体中不会有任何有意义的实现

我们来想象一下Button这个结构体

pub struct Button {
    pub width:u32,
    pub height:u32,
    pub label:String,
}
impl Draw for Button {
    fn draw(&self){
        //实际绘制按钮的代码
    }
}

Button上的字段和其它组件会不同,比如TextField 可能有width、height、label以及placeholder字段

每一个我们绘制的类型都会使用不同的代码来实现Draw Trait的draw方法来定义如何绘制特定的类型,像这里的button类型

除了实现 draw trait 之外,比如button还可能包含另一个按钮点击如何响应的方法的impl块。但这类方法并不适用于像TextField这样的类型

如果一些库的使用者决定实现一个结构体SelectBox,它包含width、height和Options字段,并且也为其实现Draw trait

use 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 
    }
}

另一个使用gui的crate中,在SelectBox结构体上实现 Draw trait

库使用者可以现在在main函数中创建一个Screen实例,可以将SelectBox和Button放入Box转变为trait对象来增加组件

接着调用Screen的run方法,它会调用每个组件的draw方法

}
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()
}

使用trait对象来存储实现了相同的trait的不同类型的值、

当编写库的时候,没人知道何时会增加SelectBox类型,但是Screen可以绘制这个类型,因为SelectBox实现了Draw trait,这意味这它实现了draw 方法

这个概念只关心所反映的信息而不是具体类型,类似于动态类型语言中称为鸭子类型的概念,如果它走、叫起来像一只鸭子,那它就是一直鸭子,如之前Screen上的run,run并不需要知道各个组件的具体类型是什么它并不检查组件时Botton还是SelectBox实例。通过指定Box作为components vector中值的类型,我们就定义了Screen为需要可以在其上调用draw 方法的值

使用trait对象和Rust类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定的方法或者担心在调用时因为值没有实现方法而产生错误,如果值没有实现trait对象所需的trait则rust不会编译这些代码

创建一个String作为其组件的Screen时发生的情况

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(String::from("Hi")),
            ],
    };
    screen.run();
}

尝试使用一种没有实现trait对象的trait类型

error[E0277]: the trait bound `std::string::String: gui::Draw` is not satisfied
  --> src/main.rs:7:13
   |
 7 |             Box::new(String::from("Hi")),
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait gui::Draw is not
   implemented for `std::string::String`
   |
   = note: required for the cast to the object type `gui::Draw`

我们会遇到错误,因为String没有实现rust_gui::Draw trait

trait对象执行动态分发

当对泛型使用trait bound时,编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现,但态化所产生的代码进行静态分发,静态分发适用于编译时编译器就知道使用了什么方法

动态分发则是编译器在编译时不知道调用了什么方法,这个具体调用什么方法运行时才能确定

当使用trait对象时,必须使用动态分发

Trait 对象要求对象安全

如果一个trait中的所有方法有如下属性时,则trait对象是安全的

1.返回值类型不是self

2.方法没有任何泛型类型参数

self是我们在实现trait或方法时使用的类型别名

我们看一个反例

pub trait Clone {
    fn clone(&self) -> Self;
}
pub struct Screen {
    pub components: Vec<Box<dyn Clone>>,
}

error[E0038]: the trait `std::clone::Clone` cannot be made into an object
 --> src/lib.rs:2:5
  |
2 |     pub components: Vec<Box<dyn Clone>>,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone`
  cannot be made into an object
  |
  = note: the trait cannot require that `Self : Sized`

如果尝试做一些违反有关 trait 对象的对象安全规则的事情,编译器会提示你

上一篇:搜索帮助——选择屏幕SCREEN


下一篇:Linux云计算集群架构师->第十章-Linux系统进程管理