Scala漫谈系列(1)——scala的类型系统

92 阅读4分钟

本人scala爱好者,分享内容属于漫谈,欢迎同好指导交流~

Scala 的类型系统

跟很多语言一样,scala是 有「类型推导」的。这意味着我们可以在源码中省略一些类型声明。在不显式声明类型的前提下,我们只要书写 valdef 就够了。

trait Thing
def getThing = new Thing { }

// without Type Ascription, the type is infered to be `Thing`
val infered = getThing

// with Type Ascription
val thing: Thing = getThing

Q: 如果它是一个参数?

A: 必须使用。

Q: 如果它是一个公有方法的返回值?

A: 为了更好的代码可读性,及输出类型的可控性,需要使用。

Q: 如果它是一个递归或重载的方法?

A: 必须使用。

Q: 如果类型推断器可能会推断出比你想要的更具体的类型?

A: 除非愿意暴露实现细节,否则必须使用。

trait Animal
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal

def getAnimal(name: String): Animal = {
  if (name == "Fido") {
    Dog("Fido")
  } else {
    Cat("Whiskers")
  }
}

def getAnimal(name: String): Animal = {
  if (name == "Fido") {
    Dog("Fido"): Animal
  } else {
    Cat("Whiskers"): Animal
  }
}

除上述情况之外,则可以不必显式声明类型。

当然,使用 Type Ascription 可以加快编译的速度,通常我们也很乐意看到一个方法的返回类型。

  1. 通用类型系统 — Any, AnyRef, AnyVal

image.png 我们之所以说 Scala 的类型系统是通用的,是因为有一个「顶类型」— Any

AnyRef 面向 Java(JVM)的对象世界,它对应 java.lang.Object ,是所有对象的超类。

AnyVal 则代表了 Java 的值世界,例如 int 以及其它 JVM 原始类型。

由于这个层次结构,我们能够定义采用 Any 的方法 - 从而与 scala.Int 实例以及 兼容java.lang.String

class Person

val allThings = ArrayBuffer [ Any ]()
val myInt = 42             
allThings += myInt                                    
allThings += new Person ()  

查看一下反编译文件:

35: invokevirtual #47  // Method myInt:()
38: invokestatic  #53  // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
41: invokevirtual #57  // Method scala/collection/mutable/ArrayBuffer.$plus$eq:(Ljava/lang/Object;)Lscala/collection/mutable/ArrayBuffer;

这也展示了 Scala 的类型系统如何拥抱 Java 的原始类型,把它们引入到 “真正的” 类型系统里面,而不是像 Java 一样,仅仅将它们作为一个分离的情况存在。

  1. 底类型 - Nothing 与 Null

当遇到一些非正常的情况,比如抛出异常的时候,类型推导是如何保持正常运转,推断出合理的类型?

val thing: Int =
  if (test)
    42                             // : Int
  else
    throw new Exception("Whoops!") // : Nothing

A very nice intuition about how bottom types work is: " Nothing extends everything."

类型推导总是会寻找 if 语句两个逻辑分支的「共同类型」。因此如果 else 分支这里是一个继承所有类型的子类型,那么最终推断出来的结果自然会是第一个分支的类型。

Types visualized:

           [Int] -> ... -> AnyVal -> Any
Nothing -> [Int] -> ... -> AnyVal -> Any

同样的道理也适用于 Scala 中的第二个底类型 - Null

val thing: String =
  if (test)
    "Yes!"  // : String
  else
    null    // : Null

由于thing 的类型是预期的 String可以看出 Null 遵循着跟 Nothing 几乎一样的规则。

Types visualized:

        [String] -> AnyRef -> Any
Null -> [String] -> AnyRef -> Any

infered type: String

但是当我们将数值类型和Null类型对比时却出现:

scala> :type if (false) 23 else null
Any
Types visualized:

Int  -> NotNull -> AnyVal -> [Any]
Null            -> AnyRef -> [Any]

infered type: Any an object

用一句话来总结就是:

Null 继承所有的 AnyRefs,而 Nothing 继承了一切。

  1. Scala中的型变

型变,通常可以解释成类型之间依靠彼此的「兼容性」,形成一种继承的关系。最常见的例子就是当你要处理容器或函数的时候,有时就必须要处理型变。

概念描述Scala 语法
不变C[T’] 与 C[T] 是不相干的C[T]
协变C[T’] 是 C[T] 的子类C[+T]
逆变C[T] 是 C[T’] 的子类C [-T]
class Box[T]
//默认泛型类是非变的
//类型BA的子类型,Box[A]和Box[B]没有任何从属关系
//Java是一样的

class Box[+T]
//类型BA的子类型,Box[B]可以认为是Box[A]的子类型
//参数化类型的方向和类型的方向是一致的。

class Box[-T]
//类型BA的子类型,Box[A]反过来可以认为是Box[B]的子类型
//参数化类型的方向和类型的方向是相反的

当你每次处理 collection 的时候就遇到了 — 你必须思考这是一个协变吗? 事实上:

大部分不可变的 collection 是协变的,而大多数可变的 collection 是不变的。

以我们最常用的不可变collection scala.collection.immutable.List[+A] 为例:

class Fruit
case class Apple() extends Fruit
case class Orange() extends Fruit

val l1: List[Apple] = Apple() :: Nil
val l2: List[Fruit] = Orange() :: l1

// and also, it's safe to prepend with "anything",
// as we're building a new list - not modifying the previous instance
val l3: List[AnyRef] = "" :: l2

如果可变的collection支持协变的话,会类型不安全,例:

// won't compile
val a: Array[Any] = Array[Int](1, 2, 3)

a(0) = "" // ArrayStoreException!

以上就是scala类型系统的一些基础概念,当然scala的类型系统还有很多很多知识点,希望后面有机会继续扩展,也请觉得有收获的小伙伴多评论交流~