Impl trait
语法
ImplTraitType :impl
TypeParamBoundsImplTraitTypeOneBound :
impl
TraitBound
impl Trait
提供了一种指定未命名但具体的类型的方法,这些类型实现了特定的 trait。它可以出现在两种位置:参数位置(在参数位置它可以充当函数的匿名类型参数),以及返回位置(在返回位置它可以充当抽象返回类型)。
#![allow(unused)] fn main() { trait Trait {} impl Trait for () {} // argument position: anonymous type parameter fn foo(arg: impl Trait) { } // return position: abstract return type fn bar() -> impl Trait { } }
匿名类型参数
注意:这通常被称为 “参数位置的 impl Trait”。(术语 “参数” 在这里更正确,但 “参数位置的 impl Trait” 是此特性开发期间使用的措辞,并且仍然保留在部分实现中。)
函数可以使用 impl
后跟一组 trait 约束来声明一个参数具有匿名类型。调用者必须提供一个满足匿名类型参数声明的约束的类型,并且函数只能使用通过匿名类型参数的 trait 约束可用的方法。
例如,以下两种形式几乎等效
#![allow(unused)] fn main() { trait Trait {} // generic type parameter fn with_generic_type<T: Trait>(arg: T) { } // impl Trait in argument position fn with_impl_trait(arg: impl Trait) { } }
也就是说,参数位置的 impl Trait
是泛型类型参数(如 <T: Trait>
)的语法糖,只不过该类型是匿名的,并且不会出现在 GenericParams 列表中。
注意: 对于函数参数,泛型类型参数和
impl Trait
并不完全等效。对于诸如<T: Trait>
之类的泛型参数,调用者可以选择在调用点使用 GenericArgs 显式指定T
的泛型实参,例如,foo::<usize>(1)
。将参数从其中一种更改为另一种可能会对函数的调用者构成破坏性更改,因为这会更改泛型实参的数量。
抽象返回类型
注意:这通常被称为 “返回位置的 impl Trait”。
函数可以使用 impl Trait
返回抽象返回类型。这些类型代表另一个具体类型,调用者只能使用指定的 Trait
声明的方法。
函数返回的每个可能值都必须解析为相同的具体类型。
返回位置的 impl Trait
允许函数返回一个非装箱的抽象类型。这对于 闭包 和迭代器特别有用。例如,闭包具有唯一的、不可写的类型。以前,从函数返回闭包的唯一方法是使用 trait 对象
#![allow(unused)] fn main() { fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } }
这可能会因堆分配和动态分发而导致性能损失。无法完全指定闭包的类型,只能使用 Fn
trait。这意味着 trait 对象是必要的。但是,使用 impl Trait
,可以更简单地编写此代码
#![allow(unused)] fn main() { fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } }
这也避免了使用装箱 trait 对象的缺点。
类似地,迭代器的具体类型可能变得非常复杂,包含链中所有先前迭代器的类型。返回 impl Iterator
意味着函数仅公开 Iterator
trait 作为其返回类型的约束,而不是显式指定涉及的所有其他迭代器类型。
trait 和 trait 实现中的返回位置 impl Trait
trait 中的函数也可以使用 impl Trait
作为匿名关联类型的语法。
trait 中关联函数的返回类型中的每个 impl Trait
都被解糖为匿名关联类型。实现函数签名中出现的返回类型用于确定关联类型的值。
捕获
每个返回位置的 impl Trait
抽象类型背后都隐藏着一些具体的类型。为了使此具体类型使用泛型参数,该泛型参数必须被抽象类型捕获。
自动捕获
返回位置的 impl Trait
抽象类型自动捕获所有作用域内的泛型参数,包括泛型类型、const 和生命周期参数(包括高阶生命周期参数)。
版本差异:在 2024 版本之前,在自由函数以及固有 impl 的关联函数和方法上,未出现在抽象返回类型的约束中的泛型生命周期参数不会被自动捕获。
精确捕获
返回位置的 impl Trait
抽象类型捕获的泛型参数集可以使用 use<..>
约束 显式控制。如果存在,则只会捕获 use<..>
约束中列出的泛型参数。例如:
#![allow(unused)] fn main() { fn capture<'a, 'b, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> { // ~~~~~~~~~~~~~~~~~~~~~~~ // Captures `'a` and `T` only. (x, y) } }
目前,约束列表中只能存在一个 use<..>
约束,trait 定义的项的签名中不允许使用此类约束,所有作用域内的类型和 const 泛型参数都必须包含在内,并且出现在抽象类型的其他约束中的所有生命周期参数都必须包含在内。
在 use<..>
约束中,任何存在的生命周期参数都必须出现在所有类型和 const 泛型参数之前,并且如果允许在 impl Trait
返回类型中出现省略的生命周期 ('_
),则可以使用它。
由于所有作用域内的类型参数都必须按名称包含在内,因此 use<..>
约束不能用于使用参数位置 impl Trait
的项的签名中,因为这些项具有作用域内的匿名类型参数。
泛型和返回位置 impl Trait
之间的区别
在参数位置,impl Trait
在语义上与泛型类型参数非常相似。但是,两者在返回位置存在显着差异。使用 impl Trait
,与泛型类型参数不同,函数选择返回类型,而调用者不能选择返回类型。
函数
#![allow(unused)] fn main() { trait Trait {} fn foo<T: Trait>() -> T { // ... panic!() } }
允许调用者确定返回类型 T
,并且函数返回该类型。
函数
#![allow(unused)] fn main() { trait Trait {} impl Trait for () {} fn foo() -> impl Trait { // ... } }
不允许调用者确定返回类型。相反,函数选择返回类型,但仅承诺它将实现 Trait
。
局限性
impl Trait
只能作为非 extern
函数的参数或返回类型出现。它不能是 let
绑定的类型、字段类型或出现在类型别名内部。