这个问题是在旺旺群里讨论的,讨论的时候思路比较发散,过程中有些说法也是错误的,重新总结一下。
问题的由来是聚石在使用actor接受消息时发现模式匹配的奇怪问题,他把问题简化后,大致是这样的(忽略代码风格问题,仅分析为何是这样的匹配结果):
trait A {
case class B(i: Int)
}
class C extends A {
def !(a:Any) = a match {
case B(0) => println("never be here") // 注1
case b: B => println("never be here2") // 注2
case B => println("never be here3") // 注3
case x => println(s"Oops, received $x")
}
}
class D extends A {
new C ! B(0)
}
new D
执行过程,不会匹配到case B(0)
和 case b:B
以及 case B
这3行,只能到 case x
这一句。
如果不是因为把case类定义成了内部类的话,就没有这些诡异问题,这里的问题正是内部类、内部半生对象的限定问题。
第1个问题,在不同的类中用相同的参数创建的内部case类实例,是否equals为true?
编译器帮我们对case类实现了equals
方法,关于case类参考这篇:话说模式匹配(6) case类的细节。编译器默认实现的equals
在比较时,主要对比其构造参数是否相等:
scala> case class X(i:Int)
scala> new X(0) == new X(0) // 参数相同就相等
res0: Boolean = true
但对于内部case类,就不仅仅是参数了:
scala> trait A { case class X(i:Int) }
scala> class C extends A; class D extends A ;
scala> val x1 = new c.X(0)
x1: c.X = X(0)
scala> val x2 = new d.X(0)
x2: d.X = X(0)
scala> x1 == x2
res1: Boolean = false
scala> val x3 = new d.X(0)
x3: d.X = X(0)
scala> x2 == x3
res2: Boolean = true
之前我们已经提到过c.X
与d.X
是不同的类型(参考这里),两个内部case类实例要equals
为true的话,必须类型相同,即得是同一个外部实例才行。
第2个问题,因为case类同时产生一个伴生对象,那么c实例中拥有的单例B与d实例中拥有的单例B是不是同一个单例对象?
scala> trait A { case class B(i:Int) }
scala> class C extends A; class D extends A;
scala> val b1 = (new C).B
b1: C#B.type = B
scala> val b2 = (new D).B
b2: D#B.type = B
scala> b1 == b2
res0: Boolean = false
可看到不同的外部实例,其内部的case类伴生对象也是不同的。
回到最初的问题,正因为 B(0)
这个构造方法在不同子类里,含义是有所不同的,假如在同一个上下文里:
trait A { case class B(i: Int) }
class C extends A {
def foo(a:Any) = a match {
case B(0) => println("never be here")
case b: B => println("never be here2")
case B => println("never be here3")
case x => println("Oops, received ")
}
foo( B(0) ) // foo执行与foo定义在同一个上下文中,B实例类型是一致的
}
new C
上面是能够匹配到 case B(0)
的。 如果把 case B(0)
这行删掉,也是能匹配到case b:B
的。
但当B是在另一个子类D中构造时,case B(0)
和 case b:B
都匹配不到了,因为B的类型不同!回顾这篇文章,内部类B,是一个路径依赖类型,所以在D中创建的B实例,它的类型是 D.this.B
这与在C中的C.this.B
不是同一个类型,所以造成了匹配失败。 要类型匹配成功的话,需要改用类型投影:
case b : A#B => … // ok 可以匹配
那第一句 case B(0)
呢?它一样也是类型不符合,让这一句匹配上需要修改在D里构造B的方式:
class D extends A {
val c = new C
val b = new c.B(0)
c ! b
}
这样构造出来的 B实例b 与 C里面的 B 类型就一致了。
这篇文章好!
好文章!非常感谢!