Scala中通配符与显式forSome定义的存在类型为何存在编译行为差异?
通配符与显式
forSome的作用域不对称,是导致编译错误的核心原因 你会觉得b和c的类型完全等价,本质上是基于“通配符只是显式forSome语法糖”的常规认知,但这个认知在带路径依赖的投影类型场景下完全不成立——两者生成的类型语法树从根源上就不一样。
逐段拆解两个类型的真实语义
我们把两个写法完全展开,差异会非常直观:
- 通配符版本的
val b: Model[_ <: Aux[_ <: Test]]
按照Scala的通配符解析规则,每个_对应的存在量词会从自身出现的位置开始,逐层捕获最近范围内的类型绑定,最终展开的等价结构是严格嵌套的:
这里内层的U完全隶属于外层T的上界定义,Aux的类型参数和自身的投影类型是强绑定的,类型系统可以正确识别:这个类型的参数可以是任意一个Tst成员满足Model[T forSome { type T <: Aux[U forSome { type U <: Test }] }]<: Test的Type子类型。 - 显式forSome版本的
val c: Model[T forSome { type T <: Aux[_ <: Test]}]
很多人没意识到的是,显式forSome的绑定规则和通配符完全不同:写在type T <: ...上界表达式里的通配符,不会被当前forSome块捕获,它的存在量词会直接提升到整个类型表达式的最外层,最终展开的真实结构是跨层独立的:
这时候存在绑定的层级完全颠倒:最外层先绑定了一个和T完全脱钩的自由类型U,内层才绑定T,T的上界是Aux[U]。因为U是跨层的自由存在类型,类型系统不会把它和Aux的Tst成员做路径依赖关联,只会把Model[T forSome { type T <: Aux[U] }] forSome { type U <: Test }Aux[U]当成一个没有具体成员信息的抽象上界。
类型不匹配的触发逻辑
用来赋值的a是Model[Aux[A.type]],它的类型参数是具体的、Tst成员固定为A.type的投影类型。
在c的真实类型结构里,因为U是和T完全独立的跨层存在变量,类型系统做匹配时没办法证明“具体的Aux[A.type]满足那个由自由U推导出的抽象上界”——毕竟U是完全自由的,可能对应任意Test子类型,和具体的A.type之间没有建立可推导的约束关系,自然就会报类型不匹配。
正确的等价写法
如果要让显式forSome的写法和通配符版本的b完全等价,只要把存在绑定的嵌套层级对齐通配符的解析结果就行,比如:
val c: Model[Aux[U] forSome { type U <: Test }] = a // 完全复刻b的语法树结构的等价写法 val c: Model[x forSome { type x <: Aux[U forSome { type U <: Test }] }] = a
改完后代码会完全编译通过。
这里要特别提醒一个常见误区:大家常说的“Foo[_]等价于Foo[T] forSome {type T}”,只在Foo是不带路径依赖的普通泛型类时成立;一旦Foo是基于内部类型成员做的投影别名(比如这里的Aux),这个等价关系会因为存在量词的作用域提升问题直接失效。
这个踩坑本质是Scala存在类型设计的历史遗留问题:通配符的解析规则是从内向外做嵌套绑定,而显式forSome的类型绑定是作用域提升式的,两者没有做到完全的语法糖等价,碰到带路径依赖的投影类型时,就会出现这种写法看起来一样、实际编译逻辑完全不同的诡异行为。
内容的提问来源于stack exchange,提问作者Some Name
相关产品推荐
相关产品推荐

