本节所讲的impl trait是一个全新的语法。比如下这样:

fn foo(n: u32) - >impl Iterator < Item = u32 > { 
(0..n).map( | x | x * 100)
}

下面我们来讲解 impl Iterator<Item=u32>。我们注意到,在写泛型函数的时候,参数传递方式可以有:静态分派或动态分派两种 选择:

fn consume_iter_static < I: Iterator < Item = u8 >> (iter: I) 
fn consume_iter_dynamic(iter: Box < dyn Iterator < Item = u8 >> )

不论选用哪种方式,都可以写出针对一组类型的抽象代码,而不是针对某一个具体类型 的。在consume_iter_static 版本中,每次调用的时候,编译器都会为不同的实参类型 实例化不同版本的函数。在consume_iter_dynamic 版本中,每次调用的时候,实参的 具体类型隐藏在了trait object的后面,通过虚函数表,在执行阶段选择调用正确的函数版本。

这两种方式都可以在函数参数中正常使用。但是,如果我们考虑函数的返回值,目前只 有这样一种方式是合法的:

fn produce_iter_dynamic()->Box<dyn Iterator<Item=u8>>

以下这种方式是不合法的:

fn  produce_iter_static()->Iterator<Item=u8>

目前版本中,Rust 只支持返回“具体类型”,而不能返回一个 trait 。 由于缺少了“不装箱的抽象返回类型”这样一种机制,导致了以下这些问题。

  • 我们返回一个复杂的迭代器的时候,会让返回类型过于复杂,而且泄漏了具体实现。 比如,如果我们需要返回一个栈上的迭代器,可能需要为函数写复杂的返回类型:
Chain<Map<'a,(int,u8),u16,Enumerate<Filter<'a,u8,vec::MoveItems<u8>>>>, 
SkipWhile<'a,u16,Map<'a,&u16,u16,slice::Items<u16>>>>

函数内部的逻辑稍微有点变化,这个返回类型就要跟着改变,远不如泛型函数参数T: Iterator 的抽象程度好。
口函数无法直接返回一个闭包。因为闭包的类型是编译器自动生成的一个匿名类型,我 们没办法在函数的返回类型中手工指定,所以返回一个闭包一定要“装箱”到堆内存 中,然后把胖指针返回回去,这样是有性能开销的。

fn multiply(m: i32) - >Box < dyn Fn(i32) - >i32 > {
    Box: :new(move | x | x * m)
}
fn main() {
    let f = multiply(5);
    println ! ("{}", f(2));
}

请注意,这种时候引入一个泛型参数代表这个闭包是行不通的:

fn multiply < T > (m: i32) - >T where T: Fn(i32) - >i32 {
    move | x | x * m
}
fn main() {
    let f = multiply(5);
    println ! ("{}", f(2));
}

编译出错 , 编译错误为 :

note: expected type‘T found type[closure@test.rs: 3 : 5 : 3 : 16 m:`

因 为 泛 型 这 种 语 法 实 际 的 意 思 是 , 泛 型 参 数T由“调用者”决定。比如 std::iter::Iterator::collect 这个函数就非常适合这样实现:

let a = [1, 2, 3];
let doubled = a.iter().map( | &x | x * 2).collect: :<???>::();

使用者可以在这个地方填充不同的类型,如Vec 、VecDeque、LinkedList 等。这个collect 方法的返回类型是一个抽象的类型集合,调用者可 以随意选择这个集合中的任意一个具体类型。

这 跟 我 们 上 面 想 返 回 一 个 内 部 的 闭 包 情 况 不 同 , 上 面 的 程 序 想 表 达 的 是 返 回 一 个 “ 具 体类型”,这个类型是由被调用的函数自行决定的,只是调用者不知道它的名字而已。

为了解决上面的问题, aturon 提出了impl trait 这个方案。此方案引入了一个新的语法, 可以表达一个不用装箱的匿名类型,以及它所满足的基本接口。
示例如下:

# ! [feature(conservative_impl_trait)] fn multiply(m: i32) - >impl Fn(i32) - >i32 {
    move | x | x * m
}
fn main() {
    let f = multiply(5);
    println ! ("{}", f(2));
}

这里的impl Fn(i32)->i32 表示,这个返回类型,虽然我们不知道它的具体名字, 但是知道它满足 Fn(size)->isize 这 个trait 的约束。因此,它解决了“返回不装箱的抽 象类型”问题。

它跟泛型函数的主要区别是:泛型函数的类型参数是函数的调用者指定的; impl trait的 具体类型是函数的实现体指定的。

为什么这个功能开关名称是#![feature(conservative_impl_trait)] 呢 ? 因 为 目前为止,它的使用场景非常保守,只允许这个语法用于普通函数的返回类型,不能用于参 数类型等其他地方。实际上设计组已经通过了另外一个RFC, 将这个功能扩展到了更多的场 景,但是这些功能目前在编译器中还没有实现。

  • 让impl trait用在函数参数中:
fn test(f:impl Fn(i32)->i32){}
  • 让impl trait用在类型别名中:
type    MyIter     =impl    Iterator<Item=i32>;
  • 让impl trait用在trait 中的方法参数或返回值中:
trait Test{
   fn test()->impl MyTrait;
}
  • 让impl Trait 用在trait 中的关联类型中:
trait Test{
  type  AT  =impl  MyTrait;
}

在某些场景下,impl trait这个语法具有明显的优势,因为它可以提高语言的表达能力。 但是,要把它推广到各个场景下使用,还需要大量的设计和实现工作。目前的这个RFC 将目 标缩小为了:先推进这个语法在函数参数和返回值场景下使用,其他的情况后面再考虑。

最后需要跟各位读者提醒一点的是,不要过于激进地使用这个功能,如在每个可以使用 impl trait 的地方都用它替换原来的具体类型。它更多地倾向于简洁性,而牺牲了一部分表达 能力。比如拿前文那个复杂的迭代器类型来说,

fn test()->Chain<Map<...>>

我们可能希望将函数返回类型写成下面这样:

fn test()->impl Iterator<Item=u16>

在绝大多数应用场景下,这样写更精简、更清晰。但是,这样写实际上是降低了表达能 力。因为,使用前一种写法,用户可以拿到这个迭代器之后再调用clone() 方法,而使用 后一种写法,就不可以了。如果希望支持clone, 那么需要像下面这样写

fn test()->impl Iterator<Item=u16>+Clone

而这两个trait 依然不是原来那个具体类型的所有对外接口。在某些场景下,需要罗列出 各种接口才能完整替代原来的写法,类似下面这样:

fn test() - >impl Iterator < Item = u16 >
 +Clone + ExactSizeIterator + TrustedLen

先不管这种写法是否可行,单说这个复杂程度,就已经完全失去了impl trait功能的意义 了。所以,什么时候该用这个功能,什么时候不该用,应该仔细权衡一下。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐