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

Rust 2018 edition

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

和 dyn

最先出现的 impl

是大家已经熟悉的关键字,不过这次这个关键字除了用于表示实现一个 Trait

,还有新的意义,即表达一个 既存类型(Existential types)

,我们可以理解为一个实现了一个特征的 具体对象

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 时,我们常常带入一些之前其他语言的惯性思维,无论是 Java 、 Go 还是 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 {

// ...

}

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

fn build_trait() -> Box> {

// ...

}

使用装箱语法意味着我们在返回时需要使用 Box::new()

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

fn foo(add: u8) -> Box

where T: Fn(u8) -> u8

{

Box::new(move |origin: u8| {

origin + add

})

}

因为匿名函数是编译器生成的匿名类型,根本不存在具体对象一说,这意味着它无法有一个明确的 size,所以只能被放置于 堆内存

之中,并取得一个 胖指针

(即除了原始指针以外还包括对指针、指针指向内存的额外描述信息等的 “指针”),我们知道,凡是非静态分派,又和堆内存打交道的(废话),性能开销相对于栈上的工作,都是十分可观的,因此我们的新语法呼之欲出。

impl Trait

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

fn foo(add: u8) -> impl Fn(u8) -> u8

{

move |origin: u8| {

origin + add

}

}

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

而通过 impl Trait

则是一个具体类型,且由返回者指定。

不过我想看了这部分内容的,都可能还有点模糊的地方,就是 调用者指定类型

,或者说有没有更直观例子来辨别,当然有,我们以官方对于这部分说明的例子来写:

trait Trait {}

fn foo(arg: T) {

}

fn foo(arg: impl Trait) {

}

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

使用泛型时我们说,其类型是调用者决定,具体代码上体现就是我们可以这样调用 foo::(1)

表示我们传入的参数是 usize

类型,亦或 let a: usize = 1

然后 foo(a)

,在编译时,编译器会将泛型转换为实际被调用者传入的类型: usize

,这就是所谓的调用者决定其类型。

而对于 impl Trait

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

dyn 来解决另外的歧义

我们在之前例子中,说过在没有 impl Trait

这种语法糖之前,需要靠装箱解决问题。这个地方其实还有一个问题,我们看代码:

fn my_function() -> Box {

// ...

}

上述代码存在一个歧义, Foo

到底是 trait 还是一个具体的类型?这两个是有明显区别的。通过新的关键字来明确两者差异。以下是官方例子:

trait Trait {}

impl Trait for i32 {}

// old

fn function1() -> Box {

}

// new

fn function2() -> Box {

}

对于使用新关键字后的代码则不再存在歧义,且后续可能将不再支持 Box

的写法,而是 Box

。当然,对于目前而言,这两者似乎并没什么区别,关于这个语法其实还有很多讨论,可以查看 reddit 的这篇内容了解: https://www.reddit.com/r/rust/comments/8su7r3/i_dont_understand_the_purpose_of_dyn/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值