Scala泛型函数协变类型输出不符问题:是类型系统Bug还是预期行为?
Scala泛型函数协变类型输出不符问题:是类型系统Bug还是预期行为?
嘿,这个问题我刚碰到的时候也懵了一下——怎么同样是过滤类型,泛型函数写出来的结果跟直接写collect差这么多?别担心,这绝对不是Scala类型系统的Bug,完全是预期之内的行为,咱们一步步拆解来看。
先把问题里的代码完整列出来,方便对照:
trait Fruit { val name: String } case class Apple(override val name: String) extends Fruit case class Banana(override val name: String) extends Fruit // 不带ClassTag的泛型过滤函数 def onlyT[T](list: List[Fruit]): List[T] = list.collect { case x: T => x } // 带ClassTag的泛型过滤函数 def onlyT2[T: ClassTag](list: List[Fruit]): List[T] = list.collect { case x: T => x } val fruitList: List[Fruit] = List( Apple("app1"), Apple("app2"), Apple("app3"), Apple("app4"), Banana("ban1"), Banana("ban2"), Banana("ban3") ) // 问题核心:a的类型是List[Banana],但实际装的是所有Apple和Banana val a: List[Banana] = onlyT[Banana](fruitList) // 正确过滤Banana的写法 val b: List[Banana] = fruitList.collect { case x: Banana => x } // 带ClassTag的泛型函数,结果正确 val c: List[Banana] = onlyT2[Banana](fruitList) println(a) // 输出: List(Apple(app1), Apple(app2), Apple(app3), Apple(app4), Banana(ban1), Banana(ban2), Banana(ban3)) println(b) // 输出: List(Banana(ban1), Banana(ban2), Banana(ban3)) println(c) // 输出: List(Banana(ban1), Banana(ban2), Banana(ban3))
为啥会出现a这种“名不副实”的情况?核心原因就是JVM的类型擦除,咱们慢慢说:
先看
onlyT函数:你写了case x: T => x,但因为T是泛型参数,Scala编译成JVM字节码的时候,T的具体类型信息会被擦除——JVM运行时根本不知道你调用的是onlyT[Banana],它只知道这个T是Fruit的子类型,所以这个case匹配其实退化成了case x: Fruit => x,所有Fruit实例都被捞了进来。那为啥编译器允许你把
List[Fruit]赋值给List[Banana]?因为你显式指定了val a: List[Banana],编译器就做了一个编译时的强制类型转换,它默认你知道自己在做什么,不会在运行时做额外的类型检查——于是就出现了“List[Banana]里装着Apple”的诡异情况,本质上是编译时的类型承诺,跟运行时的实际类型脱节了。
再看另外两个正确的例子:
- 直接写
collect { case x: Banana => x }的时候,Banana是具体类型,编译器能保留它的类型信息,运行时能准确判断实例是不是Banana,自然过滤正确。 onlyT2加了[T: ClassTag]这个上下文边界,相当于告诉编译器“帮我把T的类型信息存下来,运行时要用”,所以运行时能真正检查每个实例是不是T(也就是Banana)类型,结果自然和直接写collect一致。
总结一下:这绝对不是Scala的Bug,是JVM类型擦除机制带来的必然结果。如果你的泛型函数需要在运行时做类型匹配/过滤,必须用ClassTag(或者更复杂的TypeTag)来保留泛型参数的类型信息,否则运行时根本不知道你要匹配的是什么类型,只能做最宽泛的匹配。
备注:内容来源于stack exchange,提问作者fineThanksAndYou
相关产品推荐
相关产品推荐

