rust学习教程26-特征对象(代码片段)

孙飞Sunface 孙飞Sunface     2022-12-05     203

关键词:

本文节选自<<Rust语言圣经>>一书
欢迎大家加入Rust编程学院,一起学习交流:
QQ群:1009730433

特征对象

在上一节中有一段代码无法通过编译:

fn returns_summarizable(switch: bool) -> impl Summary 
    if switch 
        Post 
           // ...
        
     else 
        Weibo 
            // ...
        
    

其中PostWeibo都实现了Summary特征,因此上面的函数试图通过返回impl Summary来返回这两个类型,但是编译器无情的报错了,原因是
impl Trait的返回值类型并不支持多种不同的类型返回,那如果我们想返回多种类型,该怎么办?

再来考虑一个问题: 现在在做一款游戏,需要将多个对象渲染在屏幕上,这些对象拥有不同的类型,存储在列表中,渲染的时候,循环该列表顺序渲染每个对象即可,在Rust中该怎么实现?

聪明的同学可能已经能想到一个办法,利用枚举:

#[derive(Debug)]
enum UiObject 
    Button,
    SelectBox,


fn main() 
    let objects = [
        UiObject::Button,
        UiObject::SelectBox
    ];

    for o in objects 
        draw(o)
    


fn draw(o: UiObject) 
    println!(":?",o);

Bingo,这个确实是一个办法,但是问题来了,如果你的对象集合并不能明确知道呢?或者别人想要实现一个UI组件呢?是不是还要修改你的代码增加一个枚举成员?

总之,在编写这个UI库时,我们无法知道所有的UI对象类型,只知道的是:

  • UI对象的类型不同
  • 需要一个统一的类型来处理这些对象,无论是作为函数参数还是作为列表中的一员
  • 需要对每一个对象调用draw方法

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

特征对象定义

为了解决上面的所有问题,Rust引入了一个概念 - 特征对象。

在介绍特征对象之前,先来为之前的UI组件定义一个特征:

pub trait Draw 
    fn draw(&self);

只要组件实现了Draw特征,就可以调用draw方法来进行渲染。假设有一个ButtonSelectBox组件实现了Draw特征:

pub struct Button 
    pub width: u32,
    pub height: u32,
    pub label: String,


impl Draw for Button 
    fn draw(&self) 
        // 绘制按钮的代码
    


struct SelectBox 
    width: u32,
    height: u32,
    options: Vec<String>,


impl Draw for SelectBox 
    fn draw(&self) 
        // 绘制SelectBox的代码
    


此时,还需要一个动态数组来存储这些UI对象:

pub struct Screen 
    pub components: Vec<?>,

注意到上面代码中的?吗?它的意思是:我们应该填入什么类型,可以说就之前学过的内容里,你找不到哪个类型可以填入这里,但是因为ButtonSelectBox都实现了Draw特征,那我们是不是可以把Draw特征的对象作为类型,填入到数组中呢?答案是肯定的。

特征对象指向实现了Draw特征的类型的实例,也就是指向了Button或者SelectBox的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。

可以通过&引用或者Box<T>智能指针的方式来创建特征对象:

trait Draw  fn draw(&self) -> String; 

impl Draw for u8  fn draw(&self) -> String  format!("u8: ", *self)  
impl Draw for f64  fn draw(&self) -> String  format!("f64: ", *self)  

fn draw1(x: Box<dyn Draw>) 
    x.draw();


fn draw2(x: &dyn Draw) 
    x.draw();


fn main() 
    let x = 1.1f64;
    // do_something(&x);
    let y = 8u8;

    draw1(Box::new(x));
    draw1(Box::new(y));
    draw2(&x);
    draw2(&y);

上面代码,有几个非常重要的点:

  • draw1函数的参数是Box<dyn Draw>形式的特征对象,该特征对象是通过Box::new(x)的方式创建的
  • draw2函数的参数是&dyn Draw形式的特征对象,该特征对象是通过&x的方式创建的
  • dyn关键字只用在特征对象的类型声明上,在创建时无需使用dyn

因此,可以使用特征对象来代表泛型或具体的类型。

继续来完善之前的UI组件代码,首先来实现Screen:

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

其中存储了一个动态数组,里面元素的类型是Draw特征对象: Box<dyn Draw>, 任何实现了Draw特征的类型,都可以存放其中。

再来为Screen定义run方法,用于将列表中的UI组件渲染在屏幕上:

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

至此,我们就完成了之前的目标:在列表中存储多种不同类型的实例,然后将它们使用同一个方法逐一渲染在屏幕上!

再来看看,如果通过泛型实现,会如何:

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的列表中,存储了类型为T的元素,然后在Screen中使用特征约束让T实现了Draw特征,进而可以调用draw方法。

这限制了Screen 实例必须拥有一个全是 Button 类型或者全是 SelectBox 类型的组件列表。如果只需要同质(相同类型)集合,倾向于使用泛型和 特征约束,因为实现更清晰,且性能更好(特征对象,需要在运行时从vtable动态查找需要调用的方法).

现在来运行渲染下咱们精心设计的UI组件列表:

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

上面使用Box::new(T)的方式来创建了两个Box<dyn Draw>特征对象,如果在未来,还需要增加一个UI组件,那么让该组件实现Draw特征,则可以很轻松的将其渲染在屏幕上,甚至用户可以引入我们的库作为三方库,然后在自己的库中为自己的类型实现Draw特征,然后进行渲染。

在动态类型语言中,有一个很重要的概念: 鸭子类型duck typing),简单来说,就是只关心值长啥样,而不关心它实际是什么。当一个东西走起来像鸭子,叫起来像鸭子,那么它就是一只鸭子,就算它实际上是一个奥特曼,也不重要,我们就当它是鸭子。

在上例中,Screenrun时, 并不需要知道各个组件的具体类型是什么。它并不检查组件到底是 Button还是SelectBox 的实例,只要它实现了Draw特征,就能通过Box::new包装成Box<dyn Draw>特征对象,然后被渲染在屏幕上。

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

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

    screen.run();

因为String类型没有实现Draw特征,编译器直接就会报错,不会让上述代码运行。如果想要String类型被渲染在屏幕上,那么只需要为其实现Draw特征即可,非常容易。

特征对象的动态分发

回一下泛型章节我们提到过的,泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是静态分发(static dispatch),因为是在编译期完成的,对于运行期性能完全没有任何影响。

与静态分发相对应的是动态分发(dynamic dispatch),在这种情况下,直到运行时,才能确定需要调用什么方法。

当使用特赠对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用特征对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。

Self与self

在Rust中,有两个self,一个指代当前的实例对象,一个指代特征或者方法类型的别名:

trait Draw 
    fn draw(&self) ->  Self;


#[derive(Clone)]
struct Button;
impl Draw for Button 
    fn draw(&self) -> Self 
        return self.clone()
    


fn main() 
    let button = Button;
    let newb = button.draw();

上述代码中,self指代的就是当前的实例对象,也就是button.draw()中的button实例,Self则指代的是Button类型.

当理解了selfSelf的区别后,我们再来看看何为对象安全。

特征对象的限制

不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,该对象才是安全的:

  • 方法的返回类型不能是Self
  • 方法没有任何泛型参数

对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再知道实现该特征的具体类型是什么了。如果特征方法返回具体的Self类型,但是特征对象忘记了其真正的类型,那这个Self就非常尴尬,因为没人知道它是谁了。同理对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么。

标准库中的 Clone特征就符合对象安全的要求:

pub trait Clone 
    fn clone(&self) -> Self;

因为它的其中一个方法,返回了Self类型,因此它是对象不安全的。

String类型实现了 Clone特征, String 实例上调用 clone 方法时会得到一个 String 实例。类似的,当调用 Vec<T> 实例的 clone 方法会得到一个 Vec<T> 实例。clone 的签名需要知道什么类型会代替 Self,因为这是它的返回值。

如果违反了对象安全的规则,编译器会提示你。例如,如果尝试使用之前的Screen结构体来存放实现了 Clone特征的类型:

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`

这意味着不能以这种方式使用此特征作为特征对象。

rust学习教程26-特征对象(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433特征对象在上一节中有一段代码无法通过编译:fnreturns_summarizable(switch:bool)->implSummaryifswitchPost//...elseWeibo//...其中Post和Weibo... 查看详情

rust语言圣经26-特征对象(代码片段)

...的Rust学习社区官网:https://college.rsQQ群:1009730433特征对象在上一节中有一段代码无法通过编译:fnreturns_summarizable(switch:bool)->implSummaryifswitchPost//...elseWeibo//...其中Post和Weibo都实现了Summary特征,因此上面的函数试图通... 查看详情

rust学习教程27-深入了解特征(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433深入了解特征特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为... 查看详情

rust学习教程27-深入了解特征(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433深入了解特征特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为... 查看详情

rust学习教程25-特征trait(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433特征Trait如果我们想定义一个文件系统,那么把该系统跟底层存储解耦是很重要的。文件操作主要包含三个:open、wri... 查看详情

rust学习教程25-特征trait(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433特征Trait如果我们想定义一个文件系统,那么把该系统跟底层存储解耦是很重要的。文件操作主要包含三个:open、wri... 查看详情

rust极简教程(代码片段)

目录简介特性特征用途安装核心组件常用命令基础语法数据类型标量类型复合类型示例条件语句循环输出&输入输出输出花括号输出非基础类型输入所有权切片结构体枚举MatchIf-letOption集合vector容器String代码组织命名空间访问... 查看详情

rust特征对象(代码片段)

...语法真丑啊....看完trait之后,我整个人都不好了,然后看特征对象的时候,我人是比较懵逼的。0概述特征对象:指向实现了某个特征的一系列实例。这种映射关系存在一张表内(vtable),可以在运行时通过特征对象找到具体调用的... 查看详情

rust学习教程23-方法method(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433方法Method从面向对象语言过来的同学对于方法肯定不陌生,class里面就充斥着方法的概念,在Rust中方法的概念也大差... 查看详情

rust学习教程23-方法method(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433方法Method从面向对象语言过来的同学对于方法肯定不陌生,class里面就充斥着方法的概念,在Rust中方法的概念也大差... 查看详情

rust语言圣经27-深入了解特征(代码片段)

...社区官网:https://college.rsQQ群:1009730433深入了解特征特征之于Rust更甚于接口之于其他语言,因此特征在Rust中很重要也相对较为复杂,我们决定把特征分为两篇进行介绍,第一篇在之前已经讲过,现在就是... 查看详情

rust语言圣经23-方法method(代码片段)

...法的联动使用不同,Rust的方法往往跟结构体、枚举、特征一起使用,特征将在后面几章进行介绍。定义方法Rust使用impl来定义方法,例如以下代码:structCirclex:f64,y:f64,radius:f64,implCircle//new是Circle的关联函数,因为它... 查看详情

rust语言圣经25-特征trait(代码片段)

...的Rust学习社区官网:https://college.rsQQ群:1009730433特征Trait如果我们想定义一个文件系统,那么把该系统跟底层存储解耦是很重要的。文件操作主要包含三个:open、write、read,这些操作可以发生在硬盘,也可以发... 查看详情

rust学习教程32-动态数组vec(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433动态数组Vector动态数组类型用Vec<T>表示,事实上,在之前的章节,它的身影多次出现,我们一直没有细... 查看详情

Rust 无法推断通用特征 impl 的返回类型

】Rust无法推断通用特征impl的返回类型【英文标题】:Rustunabletoinferreturntypeforgenerictraitimpl【发布时间】:2021-04-1507:36:26【问题描述】:我又一次尝试满足Rust中的类型检查器的乐趣。这是我对即将出现的代码示例的模糊看法。我... 查看详情

机器学习的数学基础(代码片段)

...个体,表现实中有意义的事物,不能轻易拆分。对象是被特征化的客观事物,而表(或矩阵)是容纳这些对象的容器。换句话说,对象是表中的元素,表是对象的集合(表中的每个对象都有相同的特征和维度,对象对于每个特征... 查看详情

rust学习教程28-深入类型转换(代码片段)

本文节选自<<Rust语言圣经>>一书欢迎大家加入Rust编程学院,一起学习交流:QQ群:1009730433类型转换Rust是类型安全的语言,因此在Rust中做类型转换不是一件简单的事,这一章节我们将对Rust中的类型转换进行... 查看详情

rust学习(代码片段)

Rust  1.install  2.playonline  curlhttps://sh.rustup.rs-sSf|shecho‘PATH="$PATH:$HOME/.cargo/bin"‘>>~/.bashrcrustupdoc Tutorialorglearn (entry)   查看详情