Impl trait
Syntax
ImplTraitType → impl Bounds
ImplTraitTypeOneBound → 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 {
}
}
匿名类型参数
Note
这通常被称为“参数位置的 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 列表中。
Note
对于函数参数,泛型类型参数和
impl Trait并不完全等效。使用泛型参数(如<T: Trait>),调用者可以选择使用 GenericArgs 在调用站点显式指定T的泛型参数,例如foo::<usize>(1)。将参数从一种更改为另一种可能构成对函数调用者的破坏性更改,因为这会更改泛型参数的数量。
抽象返回类型
Note
这通常被称为“返回位置的 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 Edition differences
在 2024 版本之前,在自由函数以及固有实现的关联函数和方法上,未出现在抽象返回类型约束中的泛型生命周期参数不会被自动捕获。
精确捕获
返回位置 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<..> 约束,必须包含所有在作用域内的类型和 const 泛型参数,并且必须包含出现在抽象类型其他约束中的所有生命周期参数。
在 use<..> 约束内,任何存在的生命周期参数必须出现在所有类型和 const 泛型参数之前,并且如果省略生命周期('_)被允许出现在 impl Trait 返回类型中,则可以存在。
由于必须按名称包含所有在作用域内的类型参数,因此 use<..> 约束不能在使用参数位置 impl Trait 的项的签名中使用,因为这些项在作用域内具有匿名类型参数。
Trait 定义中关联函数中存在的任何 use<..> 约束必须包含 trait 的所有泛型参数,包括 trait 的隐式 Self 泛型类型参数。
泛型和返回位置 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 绑定的类型、字段类型,也不能出现在类型别名中。