Skip to content

23.3 impl trait

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


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

下面我们来讲解impl Iterator<Item=u32>

我们注意到,在写泛型函数的时候,参数传递方式可以有:静态分派或动态分派两种选择:


rust
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 的后面,通过虚函数表,在执行阶段选择调用正确的函数版本。

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


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

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


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

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

  • 我们返回一个复杂的迭代器的时候,会让返回类型过于复杂,而且泄漏了具体实现。比如,如果我们需要返回一个栈上的迭代器,可能需要为函数写复杂的返回类型:

rust
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的抽象程度好。

  • 函数无法直接返回一个闭包。因为闭包的类型是编译器自动生成的一个匿名类型,我们没办法在函数的返回类型中手工指定,所以返回一个闭包一定要“装箱”到堆内存中,然后把胖指针返回回去,这样是有性能开销的。

rust
fn multiply(m: i32) -> Box<dyn Fn(i32)->i32> {
    Box::new(move |x|x*m)
}

fn main() {
    let f = multiply(5);
    println!("{}", f(2));
}

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


rust
fn multiply<T>(m: i32) -> T where T:Fn(i32)->i32 {
    move |x|x*m
}

fn main() {
    let f = multiply(5);
    println!("{}", f(2));
}

编译出错,编译错误为:


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

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


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

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

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

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

示例如下:


rust
#![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 用在函数参数中:

rust
fn test(f: impl Fn(i32)->i32){}

  • 让 impl trait 用在类型别名中:

rust
type MyIter = impl Iterator<Item=i32>;

  • 让 impl trait 用在 trait 中的方法参数或返回值中:

rust
trait Test {
    fn test() -> impl MyTrait;
}

  • 让 impl Trait 用在 trait 中的关联类型中:

rust
trait Test {
    type AT = impl MyTrait;
}

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

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


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

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


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

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


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

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


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

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

Released under the MIT License