scala bug系列:2.10编译器把单例当作类型的bug

下面的内容也是那天在旺旺群里讨论的,当时对2.10下这种奇怪的行为很疑惑,以为是2.10因macro或什么其他原因允许编译时这么写呢,现在证实它是2.10编译器的一个bug,对那天在群里的错误言论澄清一下。

对于这段代码:

object A ;
A match { case a:A => println("ok") } 

为什么在2.10版本里能够编译通过?在运行时才报错。

scala> object A

scala> A match { case a:A => println(a) }
<console>:9: warning: fruitless type test: a value of type A.type cannot also be a A
          A match { case a:A => println(a) }
                           ^
<console>:9: warning: match may not be exhaustive.
It would fail on the following input: A
          A match { case a:A => println(a) }
          ^
scala.MatchError: A$@232b0a52 (of class A$)
    at .<init>(<console>:9)
    at .<clinit>(<console>)
    at .<init>(<console>:7)
    at .<clinit>(<console>)
    at $print(<console>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...

在2.92下编译会报错:

scala> object A 
scala> A  match { case a:A => println("OK") }
<console>:9: error: not found: type A
        A  match { case a:A => println("OK") }
                          ^

按理说 A 是单例,不是类型,要匹配应该写 case Acase a:A.type 才对。2.92编译报错应该是合理的。2.10下居然允许把单例对象当类型使用,很不合理啊?

scala> object A
defined module A

scala> val x: A = null  //在scala2.10.2下编译通过
x: A = null

在 2.11 下又验证了一下,MD原来确实是个bug,瞎猜测一堆其他的因素,实际验证是最靠谱的:

scala> object A
defined object A

scala> val x:A = null  // scala2.11.0-M4 版本下
<console>:7: error: not found: type A
    val x:A = null

新版本里已经修复了这个bug。

scala雾中风景(6): 内部类与模式匹配

这个问题是在旺旺群里讨论的,讨论的时候思路比较发散,过程中有些说法也是错误的,重新总结一下。

问题的由来是聚石在使用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.Xd.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 类型就一致了。

scala类型系统:4) 内部类,路径依赖类型&类型投影

先回顾一下java的内部类

class Outter{

    public class Inner {}

    public void foo(Inner c){
        System.out.println(c);
    }
}

public class Main {
    public static void main(String[] args)throws Exception{
        Outter o1 = new Outter();
        Outter o2 = new Outter();
        Outter.Inner i1 = o1.new Inner();
        Outter.Inner i2 = o2.new Inner();
        o1.foo(i2);
    }
}

上面在Outter类内部定义了Inner类,在后边main里创建了两个Inner实例,注意创建内部类的时候

Outter.Inner i1 = o1.new Inner();

在用new创建内部类时,前边必须限定外部对象(内部类实例必须要访问到外部对象引用):o1;如果是在 Outter类内部这个外部引用可以省略,它默认会用传递外部this引用。

class Outter {

    public class Inner{}

    public void test() {
        new Inner(); // 相当于this.new Inner(); 也可以写为Outter.this.new Inner();
    }
} 

同样的事情翻译为scala代码:

scala> class A { 
            class B; 
            def foo(b:B) = println(b) 
        }

scala> val a1 = new A
scala> val a2 = new A

scala> val b1 = new a1.B
scala> val b2 = new a2.B

在创建内部类的时候,语法上与java有所不同,不是 outter.new Inner() 而是 new outter.Inner(),看上去只是表象不同么?实际上,scala有很大差异,不同于java里 i1 和 i2 类型是相同的,否则 o1.foo(i2) 就不能执行了,scala里的 b1 和 b2 是不同的类型:

scala> a1.foo(b2)
<console>:12: error: type mismatch;
 found   : a2.B
 required: a1.B 

按照方法的提示: a1.foo方法接受的参数类型为:a1.B,而传入的b2 类型是 a2.B,两者不匹配。
验证一下:

scala> typeOf[a1.B] == typeOf[a2.B]
res2: Boolean = false

确实是不一样的类型,它跟外部的实例相关,那个foo方法参数类型B的写法是缩写,省略了路径:

def foo(b: B) // 相当于 this.B 或 A.this.B

这里要引入一个概念:路径依赖类型;比如上面的 A.this.B 就是一个路径依赖类型,B 前面的路径 A.this 随着不同的实例而不同,比如 a1 和 a2 就是两个不同的路径,所以a1.Ba2.B也是不同的类型。路径依赖类型的路径完整写法:

1) 内部类定义在object里面,路径:package.object.Inner

object Singleton {
    class Inner
}

val x = new p1.p2.p3.Singleton.Inner

2) 内部类定义在class/trait 里

//2.1) 直接在外部类中使用内部类型,路径:this 或 Outter.this 

class A {
    class B
    val b = new B // 相当于 A.this.B
}   

//2.2) 在子类中使用父类的内部类型,路径:super 或 Child.super

class A  { class B }
class C extends A { val x = new super.B } // 相当于 C.super.B

//2.3) 在其他类中使用,路径:outter(外部类实例)

class A  { class B }
class C { 
    val a = new A
    val x = new a.B  
}

那现在的问题来了,怎么让 a1.foo 方法可以接收 b2 参数 ?

class A { 
    class B; 
    def foo(b:B)  // 接收所有的B类型实例,而不只是foo的调用者实例(a1)路径下B类型的对象
         println(b) 
}

这又引出一个概念:类型投影(type projection)

在scala里,内部类型(排除定义在object内部的),想要表达所有的外部类A实例路径下的B类型,即对 a1.Ba2.B及所有的 an.B类型找一个共同的父类型,这就是类型投影,用 A#B的形式表示。

        A#B
        / \
       /   \
     a1.B  a2.B

这样,我们只要修改一下 foo 方法里的参数类型

def foo(b: A#B)

就可以调用 a1.foo(b2) 了。

我们回头来对比一下scala里的类型投影与java里的内部类型的概念,java里的内部类型在写法上是 Outter.Inner 它其实等同于scala里的投影类型 Outter#Inner,java里没有路径依赖类型的概念,比较简化。

scala类型系统:3) 单例类型与this.type

对于单例类型(singleton type),在《scala impatient》这本书(中文版:快学scala)里有提到过,当时读的时候,扫了一下,以为就是指scala里用object定义的这种单例对象呢,没有仔细看,最近才发现其实singleton type 是所有实例都可以有

scala> object A

scala> A.getClass
res2: Class[_ <: A.type] = class A$

scala> typeOf[A.type]
res0: reflect.runtime.universe.Type = A.type

对于这种单例,它的类型与它的类不同,要用 A.type 来表示。
这有点怪,通常我们不会用它,比如下面的方式都多此一举:

scala> val a : A.type = A

scala> def foo() : A.type = A

一方面因为scala有类型推导的功能,另一方面,因为单例是唯一的,A.type类型只有唯一的实例A(排除null),我需要的话直接用A就好了。

不过我们讨论的话题重点是 singleton type,想象一下A是一个对象实例,是否对任何实例x都存在一个x.type这样的类型呢?

scala> class A

scala> val a = new A

scala> typeOf[a.type]
res0: reflect.runtime.universe.Type = a.type

wow,真的存在。再用这个类型声明一个变量看看:

scala> val x:a.type = a
x: a.type = A@6738694b

灵的,如果赋一个非a的实例呢?

scala> val x:a.type = a2
<console>:13: error: type mismatch;
     found   : a2.type (with underlying type A)
     required: a.type

scala> typeOf[a.type] == typeOf[A]  // a.type 与 A 不是同一个类型
res2: Boolean = false

scala> typeOf[a.type] == typeOf[a2.type] // a.type 与 a2.type 也不同
res1: Boolean = false

scala> typeOf[a.type] <:< typeOf[A] // a.type 是 A 类型的子类型
res5: Boolean = true

看到了,a.typea2.type 是不同的类型!a.type也是单例类型,它也只有唯一的实例: a (排除null)

所有的对象实例都有一个x.type的单例类型,它只对应当前对象实例。这么做有什么意义呢?

这里看到一种情况,在“链式”调用风格下,有适用的场景:

class A {def method1: A = this }
class B extends A {def method2: B = this}

val b = new B
b.method2.method1  // 可以工作
b.method1.method2  // 不行,提示:error: value method2 is not a member of A

有些人很喜欢用 x.foo.bar 这样的方式连续的去操作,这种风格也成为”链式调用”风格,它要求方法返回的必须是当前对象类型,以便连贯的调用方法。不过上面,因为父类中声明的method1方法返回类型限制死了就是A类型(不写返回值类型,用类型推导也一样),导致子类对象调用完method1之后,类型已经变成了父类型,无法再调用子类型中的方法了。解决方法是:

class A { def method1: this.type = this } 
class B extends A { def method2 : this.type = this } 

val b = new B
b.method1.method2  // ok

把返回类型都改为了 this.type 单例类型,就灵了。它利用了this关键字的动态特性来实现的,在执行b.method1 的时候,method1返回值类型this.type 被翻译成了B.this.type

scala> b.method1
res0: b.type = B@ca5bdb6

这样不同的对象实例在执行该方法的时候,返回的类型也是不同的(都是当前实例的单例类型)。

小结,单例类型是个特殊的类型,单例类型绑定(依赖)在某个对象实例上,每个对象实例都有它的单例类型。不过它的场景并不多见。