如何理解existential types

这里翻译了几段Jon Gjengset大佬的《Rust for Rustaceans》。

在Rust中,你很少需要在函数体中声明的变量或调用方法的泛型参数中指定变量类型。这是因为 Rust 支持类型推断,编译器会根据代码中使用的类型来推断变量或方法调用的参数类型。

编译器通常只会对变量闭包的参数(以及返回类型)进行类型推断;而顶层定义,如函数、类型、trait 和 trait 实现块,都需要显式命名所有类型。

有几个原因导致了这种情况,但主要原因是在你至少有一些已知的点可以启动类型推断时,类型推断会更加容易。

然而,并不总是容易或者可能完全给一个类型命名!例如,如果你从一个函数中返回一个闭包,或者从一个trait方法中返回一个async块,它的类型没有一个你可以在代码中输入的名称。

1
为了处理这种情况,Rust 支持存在类型(rust-existential-type)。

可能你已经在实际代码中看到过存在类型的使用。

所有标记为 async fn 或返回 impl Trait 的函数都具有存在类型的返回类型:函数签名并没有给出返回值的真实类型,只是给出了一个提示,即函数返回实现某些特定 trait 的一些类型,调用者可以依赖这些 trait。而且关键是,调用者只能依赖于返回类型实现这些特性,仅此而已。

在技术上,严格来说,调用者不仅依赖于返回类型,而且还依赖于其他一些自动 trait,比如 Send 和 Sync。这些 trait 在 impl Trait 作为返回值时也会被编译器推导出来。我们将在下一章中更详细地介绍这个问题。

这种行为是使存在类型(existential types)得名的原因:我们断言存在某些具体类型与该签名匹配,我们将寻找该类型的任务留给编译器。通常情况下,编译器会在函数体上应用类型推断来找到匹配签名的具体类型。

将impl Trait用于函数参数位置时,它实际上只是该函数的一个未命名泛型参数的简写形式,而并非使用了存在类型。例如,fn foo(s: impl ToString) 在大多数情况下只是 fn foo<S: ToString>(s: S) 的语法糖。其中,使用 impl Trait 作为参数类型时,它其实是一个未命名的泛型参数。

当你实现具有关联类型的 trait 时,存在类型特别有用。例如,想象一下你正在实现 IntoIterator trait。它有一个关联类型 IntoIter,它保存了该类型可以转换为的迭代器的类型。使用存在类型,您不需要定义一个单独的迭代器类型来用于IntoIter。相反,您可以将关联类型给出为 impl Iterator<Item = Self::Item>,并在 fn into_iter(self) 中编写一个表达式,该表达式计算为一个迭代器,例如通过使用一些现有迭代器类型进行映射和过滤。

Existential types还提供了一个超越纯粹便利的功能:它们允许您执行零成本的类型擦除。您可以使用existential types来隐藏底层的具体类型,而不是仅仅因为它们出现在公共签名中而导出辅助类型——迭代器和futures就是常见的例子。

使用您的接口的用户只会看到相关类型实现的特征,而具体类型则作为实现细节隐藏起来。这不仅简化了接口,还使您能够随意更改实现而不会破坏未来的下游代码。