Scala类型系统

Scala类型系统使得编译器能进行很多编译时优化和约束,从而提高运行速度和避免程序错误。通过让编译器来跟踪变量、方法和类的信息,类型系统能帮助我们避免不小心写出的错误代码。

1. 类型

一个类型就是编译器知道的一组信息。这可以是任何信息。信息可以由用户明确提供的,也可以是由编译器在检查其他代码时自动推断出来的。

在Scala里可以用以下两种方式定义类型:1. 定义类(class)、特质(trait)或对象(object);2. 直接用type关键字定义类型。

在Scala里,标注类型的时候可以直接用类或特质的名字来引用其类型。要引用对象的类型,需要用对象的type成员来引用其类型。


In [1]:
class ClassName
trait TraitName
object ObjectName


defined class ClassName
defined trait TraitName
defined object ObjectName

In [2]:
def foo(x: ClassName) = x
def bar(x: TraitName) = x
def baz(x: ObjectName.type) = x


defined function foo
defined function bar
defined function baz

2. 型变(Variance)

型变是指A[T]这样的高阶类型的类型参数可以改变或变化的能力。型变的规则决定了参数化类型的顺应性。型变有三种形式:不变(Invariance)、协变(Covariance)、逆变(Contravariance)。

Java中参数化的类型在定义时并未声明继承转化关系,而是在使用该类型时,也就是声明变量时,才指定参数化类型的转化行为。

型变有以下说明:

  • 用+T表示某个泛型类的继承关系和参数类型T的集成关系“方向一致”,用-T来表示方向相反。
  • 协变适用于表示输出的类型参数,比如不可变集合中的元素。
  • 逆变适用于表示输入的类型参数,比如函数参数。

2.1 逆变的例子

逆变不容易理解。逆变的最好例子是一组trait FunctionN,N是介于0到22之间的数字(如scala.Function2),并且与函数所带的参数个数相对应。

Scala使用这些trait来实现匿名函数。比如函数表达式i => i+3是一个语法糖,编译器将其转换为scala.Function1的匿名子类。其实现为:

val f: Int => Int = new Function1[Int, Int] {
  def apply(i: Int): Int = i + 3
}

编译器使用匿名函数的函数体来定义FunctionN抽象特质的apply方法。

tips: 当对象后面跟有参数列表时,就会调用默认的apply函数。比如,一旦定义了f,我们就通过制定参数列表的方式调用它,如f(1)实际上就是f.apply(1)。

从scala.Function2的声明可以看出:

trait Function2[-T1, -T2, +R] extends AnyRef

最后一个类型参数+R是返回类型,它是协变的。开头的两个类型参数分别是函数的第一个和第二个参数,它们是逆变的。FunctionN特质中,函数参数的类型参数都是逆变的。

你可以拿一个函数的返回值并转换为其超类。对于参数来说,你可以传入参数类型的子类。

你应该能够接受一个Any => String 类型的函数并强制转换为String => Any,但反过来不行。


In [3]:
// 方法的隐式型变
def foo(x: Any): String = "Hello, I received a " + x
def bar(x: String): Any = foo(x)


defined function foo
defined function bar

In [4]:
bar("test")


res3: Any = Hello, I received a test

In [5]:
foo("test")


res4: String = "Hello, I received a test"

创建特质来构造函数对象


In [6]:
trait Function[-Arg, +Return] {
    def apply(arg: Arg): Return
}


defined trait Function

In [7]:
val foo = new Function[Any, String] {
    override def apply(arg: Any): String =
        "Hello, I received a " + arg
}


foo: AnyRef with Function[Any, String] = cmd6$$user$$anonfun$1$$anon$1@6734be

In [8]:
val bar: Function[String, Any] = foo


bar: Function[String, Any] = cmd6$$user$$anonfun$1$$anon$1@6734be

In [9]:
bar("test")


res8: Any = Hello, I received a test

2.2 为何函数参数必须是逆变,而返回值必须是协变的


In [10]:
class CSuper {
    def msuper() = println("CSuper")
}

class C extends CSuper {
    def m() = println("C")
}

class CSub extends C {
    def msub() = println("CSub")
}


defined class CSuper
defined class C
defined class CSub

定义一组匿名函数,形式为Function1[C, C],查看函数继承编译规则。


In [11]:
var f: C => C = (c: C) => new C


f: C => C = <function1>

In [12]:
f = (c: CSuper) => new CSub




In [13]:
f = (c: CSuper) => new C




In [14]:
f = (c: C) => new CSub




In [15]:
// 因为参数是逆变的,返回值是协变的,故参数无效
f = (c: CSub) => new CSuper


Compilation Failed
Main.scala:91: type mismatch;
 found   : cmd14.this.$ref$cmd9.CSuper
 required: cmd10.INSTANCE.$ref$cmd9.C
f = (c: CSub) => new CSuper
                 ^

当我们约定f的类型是C => C时,其实定义了一个契约。这样,任何有效的C类值都可以传给f,f也永远不会返回除C类值意外的任何值。

话说输入类型逆变: 如果实际函数类型为(x: CSuper) => Csub,该函数不仅可以接受任何C类值作为参数,也可以处理C的父类型实例,或其父类型的其他子类型的实例。所以,由于传入C的实例,我们永远不会传入超出f允许范围外的参数。从某种意义上说,f比我们需要的更加“宽容”。

话说输出类型协变: 当它只返回Csub时,这也是安全的。因为调用方可以处理C的实例,所以也一定可以处理CSub的实例。在这个意义上说,f比我们需要的更加“严格”。

从调用者的角度来理解,在使用函数的时候,首先看到的是函数的类型声明(上面的例子中,函数类型是C => C)。那么,函数的输入参数类型应该更加抽象(超类),因为如果函数定义中能够处理具体的,那么传入的对象是超类也能够处理;反之,如果传入的对象是更加具体的,函数体就不知道如何处理具体类的个性成分了。函数的输出类型应该更加具体(子类),不然将超出调用者的预期范围,返回值的内容不应该是所赋变量的超类,而应该是子类。

2.3 方法没有型变标记

型变标记值有在类型声明中的类型参数里才有意义,对参数化方法没有意义。因为该标记影响的是子类继承行为,而方法没有子类。如果试图在方法中加入+或-标记的话,编译器将报错。

2.4 可变(var)类型变异

通常情况下,对于某个对象消费的值适合逆变,而对于它产出的值适合协变。 如果一个对象同时消费和产出某值,则类型应该保持不变。这通常适用于可变数据结构,比如Scala的数组Array类型就不支持型变。

对于class中的可变字段,由于存在公有的读写访问方法,将会出现可变类型变异的情况。


In [16]:
// 错误信息指出,我们在使用逆变类型的位置使用了协变类型A
class ContainerPlus[+A](var value: A)


Compilation Failed
Main.scala:84: covariant type A occurs in contravariant position in type A of value value_=

                class ContainerPlus[+A](var value: A)
                      ^

In [17]:
// 错误信息指出,我们在返回类型处使用了逆变类型A
class ContainerMinus[-A](var value: A)


Compilation Failed
Main.scala:84: contravariant type A occurs in covariant position in type => A of method value

                class ContainerMinus[-A](var value: A)
                                             ^

In [18]:
// 这是上面ContainerPlus的显式方法声明重写,与上式等同
class ContainerPlus[+A](var a: A) {
    private var _value: A = a
    def value_=(newA: A): Unit = _value = newA
    def value: A = _value
}


Compilation Failed
Main.scala:84: covariant type A occurs in contravariant position in type A of value a_=

                class ContainerPlus[+A](var a: A) {
                      ^
Main.scala:86: covariant type A occurs in contravariant position in type A of value newA

    def value_=(newA: A): Unit = _value = newA
                ^

对于getter和setter方法中的可变字段而言,它在读方法中处于协变的位置,而在写方法中又处于逆变的位置。不存在既协变又逆变的类型参数,所以对于可变字段A的唯一选择就是不变

3. 型变的例子

3.1 可变集合型变类型为不变

可变数据结构(比如Array、ArrayBuffer、ListBuffer)不支持型变,因为这样做会不安全。

val students = new Array[Student](length)
val people: Array[Person] = students //非法,假设可以
people(0) = new Person("Jason") //这样使得students(0)不再是Student了

In [19]:
trait Animal {
    def speak
}

class Dog(var name: String) extends Animal {
    def speak = println("woof")
    override def toString = name
}

class SuperDog(name: String) extends Dog(name) {
    def useSuperPower = println("Using my superpower!")
}


defined trait Animal
defined class Dog
defined class SuperDog

In [20]:
val fido = new Dog("Fido")
val wonderDog = new SuperDog("Wonder Dog")
val shaggy = new SuperDog("Shaggy")


fido: Dog = Fido
wonderDog: SuperDog = Wonder Dog
shaggy: SuperDog = Shaggy

In [21]:
import collection.mutable.ArrayBuffer

val dogs = ArrayBuffer[Dog]()
dogs += fido
dogs += wonderDog


import collection.mutable.ArrayBuffer
dogs: collection.mutable.ArrayBuffer[Dog] = ArrayBuffer(Fido, Wonder Dog)
res16_2: collection.mutable.ArrayBuffer[Dog] = ArrayBuffer(Fido, Wonder Dog)
res16_3: collection.mutable.ArrayBuffer[Dog] = ArrayBuffer(Fido, Wonder Dog)

In [22]:
def makeDogsSpeak(dogs: ArrayBuffer[Dog]) {
    dogs.foreach(_.speak)
}


defined function makeDogsSpeak

In [23]:
makeDogsSpeak(dogs)


woof
woof

如果使用makeDogsSpeak(superDogs)将编译无法通过


In [24]:
val superDogs = ArrayBuffer[SuperDog]()
superDogs += shaggy
superDogs += wonderDog
makeDogsSpeak(superDogs) // ERROR: won't compile


Compilation Failed
Main.scala:126: type mismatch;
 found   : scala.collection.mutable.ArrayBuffer[cmd19.this.$ref$cmd14.SuperDog]
 required: scala.collection.mutable.ArrayBuffer[cmd17.INSTANCE.$ref$cmd14.Dog]
Note: cmd19.this.$ref$cmd14.SuperDog <: cmd17.INSTANCE.$ref$cmd14.Dog, but class ArrayBuffer is invariant in type A.
You may wish to investigate a wildcard type such as `_ <: cmd17.INSTANCE.$ref$cmd14.Dog`. (SLS 3.2.10)
makeDogsSpeak(superDogs)
              ^

3.2 不可变集合型变类型为协变

像是List、Vector、Seq这样的不可变集合都支持协变


In [25]:
def makeDogsSpeak2(dogs: Seq[Dog]) {
    dogs.foreach(_.speak)
}


defined function makeDogsSpeak2

In [26]:
val dogs2 = Seq(new Dog("Fido"), new Dog("Tanner"))
makeDogsSpeak2(dogs2)


woof
woof
dogs2: Seq[Dog] = List(Fido, Tanner)

In [27]:
// this works too
val superDogs2 = Seq(new SuperDog("Wonder Dog"), new SuperDog("Scooby"))
makeDogsSpeak2(superDogs2)


woof
woof
superDogs2: Seq[SuperDog] = List(Wonder Dog, Scooby)