理解 Rust 2018 edition 的两个新关键字 —— impl 和 dyn

栏目: 编程语言 · Rust · 发布时间: 6年前

内容简介:最先出现的官方原文介绍:

Rust 2018 edition 虽然实际发布时间还没到(本文开始写的时间是 18 年七月底),但是有些 2018 edition 的特性已经随着 Rust 的新版本发布放出,这些已经进入 stable 版的特性必然是应当了解并学习的。其中就有两个本文所要讨论的关键字 —— impldyn

最先出现的 impl 是大家已经熟悉的关键字,不过这次这个关键字除了用于表示实现一个 Trait ,还有新的意义,即表达一个 既存类型(Existential types) ,我们可以理解为一个实现了一个特征的 具体对象

官方原文介绍: impl Trait https://rust-lang-nursery.github.io/edition-guide/2018/transitioning/traits/impl-trait.html

impl Trait is the new way to specify unnamed but concrete types that implement a specific trait. There are two places you can put it: argument position, and return position.

trait Trait {}

// argument position
fn foo(arg: impl Trait) {
}

// return position
fn foo() -> impl Trait {
}

不过其意义是什么?与我们另一个要介绍的 dyn Trait 又有什么关系?下面我们正式开始。

使用抽象的一些问题

在使用 Rust 时,我们常常带入一些之前其他语言的惯性思维,无论是 JavaGo 还是 PHP,我们可以通过定义接口来抽象一个函数或方法的返回值,只要这个返回值是这个接口的实现即可。Rust 有一个和这些语言类似的东西: Trait

A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic can be any type that has certain behavior.

即 trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。 trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 trait bounds 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。

不过这里需要强调,trait 与 interface 是存在差异的

当我们在某些函数需要返回一个 trait 的实现时,我们可能写出如下代码:

// 注:Iterator 是一个迭代器 trait
fn get_iter() -> Iterator<Item=u8> {
    // ...
}

这样的代码将会报错,因为 Rust 要求必须返回一个具体的类型而不是一个抽象,因为抽象对于 Rust 是一个模糊的不具名信息,无法在编译期确定很多细节(这其实是可以解决的,不过由于 Rust 目前对于 DST 即动态大小类型的支持还在未来特性中,为保证 Rust 的稳定推进,目前只能这样)。那如何解决呢?可以通过装箱语法实现:

fn build_trait() -> Box<Iterator<Item=u8>> {
    // ...
}

使用装箱语法意味着我们在返回时需要使用 Box::new() 包装,但是使用装箱意味着这一过程属于运行时的动态分派,无法再将对象定于栈上。除此之外,我们可能还有另外的需求,就是返回一个匿名函数,这在当下业务场景中十分常见,根据上面的描述,我们若想要返回一个匿名函数,代码得如下书写:

fn foo<T>(add: u8) -> Box<T>
    where T: Fn(u8) -> u8
{
    Box::new(move |origin: u8| {
        origin + add
    })
}

因为匿名函数是编译器生成的匿名类型,根本不存在具体对象一说,这意味着它无法有一个明确的 size,所以只能被放置于 堆内存 之中,并取得一个 胖指针 (即除了原始指针以外还包括对指针、指针指向内存的额外描述信息等的 “指针”),我们知道,凡是非静态分派,又和堆内存打交道的(废话),性能开销相对于栈上的工作,都是十分可观的,因此我们的新语法呼之欲出。

impl Trait

我们继续刚刚返回匿名函数的例子,使用新语法后代码如下:

fn foo<T>(add: u8) -> impl Fn(u8) -> u8
{
    move |origin: u8| {
        origin + add
    }
}

该语法表示返回值是一个满足其指明的 trait 的约束的具体类型。另外,由于这个实现是该函数返回值自行指定,还解决了某些场景使用泛型时的一些问题,比如上面的代码例子中,我们使用了泛型,而泛型的实际类型是由调用者决定的,这在使用装箱语法时会报错,虽然你在返回时通过 where 指明了泛型 T 的约束,但那并不是指示泛型具体类型的。

而通过 impl Trait 则是一个具体类型,且由返回者指定。

不过我想看了这部分内容的,都可能还有点模糊的地方,就是 调用者指定类型 ,或者说有没有更直观例子来辨别,当然有,我们以官方对于这部分说明的例子来写:

trait Trait {}

fn foo<T: Trait>(arg: T) {
}

fn foo(arg: impl Trait) {
}

上述两种实现,前者是泛型,表示 T 泛型需要是一个 Trait 的实现,后者不是泛型但也是要求满足 Trait 约束,这两个乍一看是类似的,实际却大不一样。

使用泛型时我们说,其类型是调用者决定,具体代码上体现就是我们可以这样调用 foo::<usize>(1) 表示我们传入的参数是 usize 类型,亦或 let a: usize = 1 然后 foo(a) ,在编译时,编译器会将泛型转换为实际被调用者传入的类型: usize ,这就是所谓的调用者决定其类型。

而对于 impl Trait 这种形式,则无需调用者指定,仅需要保证满足约束即可。

dyn 来解决另外的歧义

我们在之前例子中,说过在没有 impl Trait 这种语法糖之前,需要靠装箱解决问题。这个地方其实还有一个问题,我们看代码:

fn my_function() -> Box<Foo> {
    // ...
}

上述代码存在一个歧义, Foo 到底是 trait 还是一个具体的类型?这两个是有明显区别的。通过新的关键字来明确两者差异。以下是官方例子:

trait Trait {}

impl Trait for i32 {}

// old
fn function1() -> Box<Trait> {
}

// new
fn function2() -> Box<dyn Trait> {
}

对于使用新关键字后的代码则不再存在歧义,且后续可能将不再支持 Box<Trait> 的写法,而是 Box<dyn Trait> 。当然,对于目前而言,这两者似乎并没什么区别,关于这个语法其实还有很多讨论,可以查看 reddit 的这篇内容了解: https://www.reddit.com/r/rust/comments/8su7r3/i_dont_understand_the_purpose_of_dyn/


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms to Live By

Algorithms to Live By

Brian Christian、Tom Griffiths / Henry Holt and Co. / 2016-4-19 / USD 30.00

A fascinating exploration of how insights from computer algorithms can be applied to our everyday lives, helping to solve common decision-making problems and illuminate the workings of the human mind ......一起来看看 《Algorithms to Live By》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具