文档章节

Rust程序设计语言第二版ch17-02

itfanr
 itfanr
发布于 2017/04/10 16:09
字数 2923
阅读 28
收藏 0
点赞 0
评论 0

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

ch17-02-trait-objects.md

commit 872dc793f7017f815fb1e5389200fd208e12792d

在第8章,我们谈到了vector的局限是vectors只能存储同种类型的元素。我们在Listing 8-1有一个例子,其中定义了一个SpreadsheetCell 枚举类型,可以存储整形、浮点型和text,这样我们就可以在每个cell存储不同的数据类型了,同时还有一个代表一行cell的vector。当我们的代码编译的时候,如果交换地处理的各种东西是固定的类型是已知的,那么这是可行的。

<!-- The code example I want to reference did not have a listing number; it's
the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
get Chapter 8 for editing. /Carol -->

有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

定义一个带有自定义行为的Trait

不过,在Rust语言中,我们可以定义一个名为Draw的trait,其上有一个名为draw的方法。我们定义一个带有trait对象的vector,绑定了一种指针的trait,比如&引用或者一个Box<T>智能指针。

我们提到,我们不会调用结构体和枚举的对象,从而区分于其他语言的对象。在结构体的数据或者枚举的字段和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个概念里。Trait对象更像其他语言的对象,在这种场景下,他们组合了由指针组成的数据到实体对象,该对象带有在trait中定义的方法行为。但是,trait对象是和其他语言是不同的,因为我们不能向一个trait对象增加数据。trait对象不像其他语言那样有用:它们的目的是允许从公有的行为上抽象。

trait定义了在给定场景下我们所需要的行为。在我们会使用一个实体类型或者一个通用类型的地方,我们可以把trait当作trait对象使用。Rust的类型系统会保证我们为trait对象带入的任何值会实现trait的方法。我们不需要在编译阶段知道所有可能的类型,我们可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

<span class="filename">Filename: src/lib.rs</span>

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

<span class="caption">Listing 17-3:Draw trait的定义</span>

<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->

因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen的结构体,里面有一个名为components的vector,components的类型是Box<Draw>。Box<Draw>是一个trait对象:它是一个任何Box内部的实现了Drawtrait的类型的替身。

<span class="filename">Filename: src/lib.rs</span>

# pub trait Draw {
#     fn draw(&self);
# }
#
pub struct Screen {
    pub components: Vec<Box<Draw>>,
}

<span class="caption">Listing 17-4: 定义一个Screen结构体,带有一个含有实现了Drawtrait的components vector成员

</span>

Screen结构体上,我们将要定义一个run方法,该方法会在它的components上调用draw方法,如Listing 17-5所示:

<span class="filename">Filename: src/lib.rs</span>

# pub trait Draw {
#     fn draw(&self);
# }
#
# pub struct Screen {
#     pub components: Vec<Box<Draw>>,
# }
#
impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

<span class="caption">Listing 17-5:在Screen上实现一个run方法,该方法在每个组件上调用draw方法 </span>

这是区别于定义一个使用带有trait绑定的通用类型参数的结构体。通用类型参数一次只能被一个实体类型替代,而trait对象可以在运行时允许多种实体类型填充trait对象。比如,我们已经定义了Screen结构体使用通用类型和一个trait绑定,如Listing 17-6所示:

<span class="filename">Filename: src/lib.rs</span>

# pub trait Draw {
#     fn draw(&self);
# }
#
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();
        }
    }
}

<span class="caption">Listing 17-6: 一种Screen结构体的替代实现,它的run方法使用通用类型和trait绑定 </span>

这个例子只能使我们有一个Screen实例,这个实例有一个组件列表,所有的组件类型是Button或者TextField。如果你有同种的集合,那么可以优先使用通用和trait绑定,这是因为为了使用具体的类型,定义是在编译阶段是单一的。

而如果使用内部有Vec<Box<Draw>> trait对象的列表的Screen结构体,Screen实例可以同时包含Box<Button>Box<TextField>Vec。我们看它是怎么工作的,然后讨论运行时性能的实现。

来自我们或者库使用者的实现

现在,我们增加一些实现了Drawtrait的类型。我们会再次提供Button,实际上实现一个GUI库超出了本书的范围,所以draw方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,Button结构体可能有 widthheightlabel`字段,如Listing 17-7所示:

<span class="filename">Filename: src/lib.rs</span>

# pub trait Draw {
#     fn draw(&self);
# }
#
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
    }
}

<span class="caption">Listing 17-7: 实现了Draw trait的Button 结构体</span>

Button上的 widthheightlabel会和其他组件不同,比如TextField可能有widthheight, labelplaceholder字段。每个我们可以在屏幕上绘制的类型会实现Drawtrait,在draw方法中使用不同的代码,定义了如何绘制Button(GUI代码的具体实现超出了本章节的范围)。除了Draw trait,Button可能也有另一个impl块,包含了当按钮被点击的时候的响应方法。这类方法不适用于TextField这样的类型。

有时,使用我们的库决定了实现一个包含widthheightoptions``SelectBox结构体。它们在SelectBox类型上实现了Drawtrait,如 Listing 17-8所示:

<span class="filename">Filename: src/main.rs</span>

extern crate rust_gui;
use rust_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
    }
}

<span class="caption">Listing 17-8: 另外一个crate中,在SelectBox结构体上使用rust_gui和实现了Draw trait </span>

我们的库的使用者现在可以写他们的main函数来创建一个Screen实例,然后通过把自身放入Box<T>变成trait对象,向screen增加SelectBoxButton。它们可以在每个Screen实例上调用run方法,这会调用每个组件的draw方法。 Listing 17-9展示了实现:

<span class="filename">Filename: src/main.rs</span>

use rust_gui::{Screen, Button};

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

<span class="caption">Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型 </span>

虽然我们不知道有些人可能有一天会增加SelectBox类型,但是我们的Screen 有能力操作SelectBox和绘制,因为SelectBox实现了Draw类型,这意味着它实现了draw方法。

只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它肯定是只鸭子!在Listing 17-5的Screenrun方法的实现中,run不需要知道每个组件的具体类型。它也不检查是否一个组件是Button或者SelectBox的实例,只是调用组件的draw方法即可。通过指定Box<Draw>作为componentsvector中的值类型,我们定义了:Screen需要可以被调用其draw方法的值。

使用trait对象和支持duck typing的Rust类型系统的好处是,我们永远不需要在运行时检查一个值是否实现了一个特殊方法,或者担心因为调用了一个值没有实现方法而遇到错误。如果值没有实现trait对象需要的trait,Rust不会编译我们的代码。

比如,Listing 17-10展示了当我们创建一个把String当做其成员的Screen时发生的情况:

<span class="filename">Filename: src/main.rs</span>

extern crate rust_gui;
use rust_gui::Draw;

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

    screen.run();
}

<span class="caption">Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型

</span>

我们会遇到这个错误,因为String没有实现 Drawtrait:

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

这个报错让我们知道,或者我们传入了本来不想传给Screen的东西,我们应该传入一个不同的类型,或者是我们应该在String上实现Draw,这样,Screen才能调用它的draw方法。

Trait对象执行动态分发

回忆一下第10章,我们讨论过当我们使用通用类型的trait绑定时,编译器执行单类型的处理过程:在我们需要使用通用类型参数的地方,编译器为每个实体类型产生了非通用的函数实现和方法。由于非单类型而产生的代码是 static dispatch:当方法被调用,代码会执行在编译阶段就决定的方法,这样寻找那段代码是非常快速的。

当我们使用trait对象,编译器不能执行单类型的,因为我们不知道可能被代码调用的类型。而,当方法被调用的时候,Rust跟踪可能被使用的代码,然后在运行时找出为了方法被调用时该使用哪些代码。这也是我们熟知的dynamic dispatch,当运行时的查找发生时是比较耗费资源的。动态分发也防止编译器选择内联函数的代码,这样防止了一些优化。虽然我们写代码时得到了额外的代码灵活性,不过,这是一个权衡考虑。

https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md

https://github.com/itfanr/rust-book-2rd-en

© 著作权归作者所有

共有 人打赏支持
itfanr
粉丝 114
博文 446
码字总数 161777
作品 1
济南
程序员
2018 年 Rust 走向如何?社区说了算!

2017 年对 Rust 系统程序语言来说具有特别的意义。Rust 开源项目的成员正积极汇整去年的成果(使 Rust 变得更易学易用),并准备发表 Rust 自 2015 年发布以来第一波的重大更新。 「我们要把...

火狐 ⋅ 04/13 ⋅ 0

hainuo/rust ebook

'The Rust Programming Language' as EBook Rust编程语言电子书 s 由 @hainuo 翻译成中文.由于第一次翻译科技文献,不太熟悉英语的表达方式,所以可能有不少地方不到位请大家见谅;同时请大家...

hainuo ⋅ 2015/05/24 ⋅ 0

rust语言初体验

Rust介绍: Rust 是一门系统级编程语言,被设计为保证内存和线程安全,并防止段错误。作为系统级编程语言,它的基本理念是 “零开销抽象”。理论上来说,它的速度与 C / C++ 同级。Rust 可以...

有力量的神经病 ⋅ 2016/08/12 ⋅ 0

C,D,Go,Rust,Nim 5语回文数大战!仅供娱乐参考!

娱乐!娱乐!请不要诋毁任何语言!!!!20151030测试了Rust 1.4;20151102测试了nim0.12;20151214测试了Rust 1.5 GCC版;20160127测试Rust 1.6 MSVC;20160127 Nim 0.13;20160131在树莓派...

捍卫机密 ⋅ 2015/09/25 ⋅ 14

Rust 0.6 测试版发布,Mozilla 的编程语言

Rust 0.6 发布了,下载地址: http://static.rust-lang.org/dist/rust-0.6.tar.gz http://static.rust-lang.org/dist/rust-0.6-install.exe 该版本已经包含了计划中 Rust 所有的特性,请大家......

oschina ⋅ 2013/04/01 ⋅ 4

Rust 2017 调查报告:学习曲线是最大痛点

Rust 官方在社区上做了一次调查,以了解用户如何看待 Rust 的发展。调查共收到 5368 份回复,其中有 大约 2/3 的是 Rust 用户,剩下的 1/3 是非 Rust 用户,调查结果如下。 点此查看完整调查...

王练 ⋅ 2017/09/07 ⋅ 52

Rust 1.0 正式版将于 5月15日 发布

Rust 官方博客今天发表 文章 称 Rust 1.0 正式版已经确定了将于5月15日发布。目前开发团队正在做一些相关的准备工作。 Rust 是 Mozilla 的一个新的编程语言,由web语言的领军人物Brendan Eic...

oschina ⋅ 2015/02/14 ⋅ 9

Rust 1.0 正式版发布,Mozilla 编程语言

Rust 1.0 正式版发布了,这是官方首次宣布的 Rust 稳定版本。当然 1.0 版本的发布并不代表 Rust 语言已经完工,还有很多特性需要完成。1.0 版本的发布要特别感谢以下贡献者: Rust 是 Mozill...

oschina ⋅ 2015/05/16 ⋅ 88

新 Red Hat 编译器工具箱:Clang、LLVM、GCC 等

为了让开发者用到最新的、稳定版本的开发工具,Red Hat 每年会发布两次编译器工具箱、脚本语言、开源数据库等工具的更新。这些产品被封装为 Red Hat 软件集合(脚本语言、开源数据库、Web工具...

雨田桑 ⋅ 04/13 ⋅ 8

Mozilla 演示 Servo 的实验性渲染器 Webrender

Mozilla开发者在湾区举行的Rust会议上演示了(视频)Servo的实验性渲染器Webrender。WebRender能以每秒数百帧的速度渲染任何网页,它能像游戏一样渲染网页内容。和Servo的其它项目一样,Web...

oschina ⋅ 2016/02/26 ⋅ 13

没有更多内容

加载失败,请刷新页面

加载更多

下一页

如何优雅的编程——C语言界面的一点小建议

我们鼓励在编程时应有清晰的哲学思维,而不是给予硬性规则。我并不希望你们能认可所有的东西,因为它们只是观点,观点会随着时间的变化而变化。可是,如果不是直到现在把它们写在纸上,长久以...

柳猫 ⋅ 24分钟前 ⋅ 0

从零手写 IOC容器

概述 IOC (Inversion of Control) 控制反转。熟悉Spring的应该都知道。那么具体是怎么实现的呢?下面我们通过一个例子说明。 1. Component注解定义 package cn.com.qunar.annotation;impo...

轨迹_ ⋅ 24分钟前 ⋅ 0

系统健康检查利器-Spring Boot-Actuator

前言 实例由于出现故障、部署或自动缩放的情况,会进行持续启动、重新启动或停止操作。它可能导致它们暂时或永久不可用。为避免问题,您的负载均衡器应该从路由中跳过不健康的实例,因为它们...

harries ⋅ 26分钟前 ⋅ 0

手把手教你搭建vue-cli脚手架-详细步骤图文解析[vue入门]

写在前面: 使用 vue-cli 可以快速创建 vue 项目,vue-cli很好用,但是在最初搭建环境安装vue-cli及相关内容的时候,对一些人来说是很头疼的一件事情,本人在搭建vue-cli的项目环境的时候也是...

韦姣敏 ⋅ 36分钟前 ⋅ 0

12c rman中输入sql命令

12c之前版本,要在rman中执行sql语句,必须使用sql "alter system switch logfile"; 而在12c版本中,可以支持大量的sql语句了: 比如: C:\Users\zhengquan>rman target / 恢复管理器: Release 1...

tututu_jiang ⋅ 50分钟前 ⋅ 0

Nginx的https配置记录以及http强制跳转到https的方法梳理

Nginx的https配置记录以及http强制跳转到https的方法梳理 一、Nginx安装(略) 安装的时候需要注意加上 --with-httpsslmodule,因为httpsslmodule不属于Nginx的基本模块。 Nginx安装方法: ...

Yomut ⋅ 今天 ⋅ 0

SpringCloud Feign 传递复杂参数对象需要注意的地方

1.传递复杂参数对象需要用Post,另外需要注意,Feign不支持使用GetMapping 和PostMapping @RequestMapping(value="user/save",method=RequestMethod.POST) 2.在传递的过程中,复杂对象使用...

@林文龙 ⋅ 今天 ⋅ 0

如何显示 word 左侧目录大纲

打开word说明文档,如下图,我们发现左侧根本就没有目录,给我们带来很大的阅读障碍 2 在word文档的头部菜单栏中,切换到”视图“选项卡 3 然后勾选“导航窗格”选项 4 我们会惊奇的发现左侧...

二营长意大利炮 ⋅ 今天 ⋅ 0

智能合约编程语言Solidity之线上开发工具

工具地址:https://ethereum.github.io/browser-solidity/ 实例实验: 1.创建hello.sol文件 2.调试输出结果

硅谷课堂 ⋅ 今天 ⋅ 0

ffmpeg 视频格式转换

转 Mp4 格式 #> ffmpeg -i input.avi -c:v libx264 output.mp4#> ffmpeg -i input.avi -c:v libx264 -strict -2 output.mp4#> ffmpeg -i input.avi -c:v libx264 -strict -2 -s 1......

Contac ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部