Rust 不透明类型:Defining Use 与 Typing Mode
记录 impl Trait 的 defining use、non-defining use,以及 TypingEnv 和 rigid alias 对类型展开的影响。
defining use and non-defining use
defining use 是指向编译器reveal 不透明类型背后的具体类型,也就是impl Trait背后的底层是什么具体的类型。
当编译器遇到 impl Trait 时,它需要知道这个类型在内存中到底长什么样(具体类型是什么)。Defining use 就是编译器用来收集线索、推断并最终问题确定这个具体类型的地方。
fn get_number() -> impl std::fmt::Display { 42 // <--- 这是 Defining Use}看到42之后就可以推断出来std::fmt::Display背后的具体类型是i32
Non-defining use 是指那些使用了该 impl Trait 类型,但不会让编译器去推断其底层具体类型的代码,在这些位置,编译器只检查你的操作是否符合 impl Trait 声明的 Trait 边界(Bounds),而把它的具体类型视为未知。
比如
fn foo() -> impl Iterator<Item = i32> { vec![1, 2, 3].into_iter()}rust故意让调用者不知道这个 具体的类型,对于调用者来说它不知道是一个具体的Vec<i32>::Iterator,而是一个impl Iterator<Item = i32>
在定义 opaque type 的上下文中,是可以确定它背后的类型具体是啥,但是对于调用者来说保持隐藏。
fn get_number() -> impl std::fmt::Display { 42 // Defining use (决定了类型是 i32)}
fn use_number() { let x = get_number(); // <--- Non-defining use println!("{}", x); // <--- Non-defining use}在 use_number 中,编译器不关心 x 到底是 i32 还是 String,它只知道 x 实现了 Display,所以可以传给 println!。这里不会去“定义”或改变 get_number 返回的具体类型。
TAIT 中经典Non-defining use的例子:
type MyOpaque = impl Copy;
fn define_it() -> MyOpaque { 1 // Defining use: 决定 MyOpaque 是 i32}
fn pass_it_through() -> MyOpaque { define_it() // <--- Non-defining use!}Rust 这么设计的目的是为了防止类型推断的时候过于全局性,增加编译速度,防止出现一些循环依赖和难以理解的报错。所以具体的类型只能由Defining use单项决定;impl Trait的设计的目的是为了隐藏具体的实现,如果允许调用方去反推底层类型,破坏了不透明类型的封装意义.
- #156742:引入回归的改动 ——
Add rigid alias marker - #158993:真正修正逻辑的改动 ——
rerun in original typing mode if we meet any opaques in post analysis - #161555:把 #158993 backport 到 Rust 1.98 stable,形成 1.98.1
这个156742 PR 之前大致相当于Alias(AliasTy),这个PR之后改成了Alias(Isrigid,AliasTy)
并增加了一个 enum IsRigid,包括yes和no两个变体。
它表示一个 alias 是 rigid 的,还是仍然 potentially normalizeable;如果在当前 scope 中已经确定它不能继续 normalize,就标成 IsRigid::Yes,以后不用再重复尝试 normalization。
normalization意思是把一个alias变成一个更加明确类型的过程
当然只是表示当前的scope里面确定不能继续normalize,不过如果在其他scope内可以继续reveal opaque,就可能可以继续normalize了
IsRigid 从设计上就不是 alias 的永久属性,而是一个对于当前 TypingEnv 成立的属性。如果进入另一个 TypingEnv,因为 ParamEnv 或 TypingMode 发生变化,需要强制把 rigid aliases 恢复成 non-rigid。
rustc 会有不同 TypingMode,比如现在能看到:
TypingMode::TypeckTypingMode::PostBorrowckTypingMode::PostAnalysisTypingMode::CodegenTypingMode::CoherenceTypingMode::ErasedNotCoherence...不同 context 下,Rust 的 typing rules 有一些差异。
比如 Typeck 主要负责正常用户代码类型检查,而 PostAnalysis / Codegen 已经处于后期阶段,可以 reveal 更多 opaque 信息。文档也明确说,在 analysis 之后可以 reveal opaque hidden type,只是不应该让这种具体信息泄露成用户可观察的类型错误。
GPT做的流程示意
TypingMode │ │ 决定当前允许使用什么类型信息 ▼ TypingEnv │ │ 尝试 normalization ▼ Alias ┌────────┴────────┐ │ │ Opaque Projection impl Trait <T as Trait>::Assoc │ │ └────────┬────────┘ ▼ 当前还能继续展开吗? / \ 能 不能 │ │ ▼ ▼ 继续 normalize IsRigid::YesOpaque 能否被 reveal 是依赖于不同的 Typing Mode
rust-lang/rust #158993
这个PR是rerun in original typing mode if we meet any opaques in post analysis
修改的代码就这一块:
( RerunCondition::OpaqueInStorageOrAnyOpaqueHasInferAsHidden(_), TypingMode::PostAnalysis | TypingMode::Codegen,) => RerunDecision::No,) => RerunDecision::Yes,把原先已经推断为No的改为Yes,也就是需要一次Rerun;普通的 OpaqueInStorage 已经会在 PostAnalysis/Codegen 下重跑,可它的组合版本却不会,这是一个逻辑bug