本人scala爱好者,分享内容属于漫谈,欢迎同好指导交流~
Scala 的类型系统
跟很多语言一样,scala是 有「类型推导」的。这意味着我们可以在源码中省略一些类型声明。在不显式声明类型的前提下,我们只要书写 val 或 def 就够了。
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 可以加快编译的速度,通常我们也很乐意看到一个方法的返回类型。
-
通用类型系统 — Any, AnyRef, AnyVal
我们之所以说 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 一样,仅仅将它们作为一个分离的情况存在。
-
底类型 - 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: "
Nothingextends 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 继承了一切。
-
Scala中的型变
型变,通常可以解释成类型之间依靠彼此的「兼容性」,形成一种继承的关系。最常见的例子就是当你要处理容器或函数的时候,有时就必须要处理型变。
| 概念 | 描述 | Scala 语法 |
|---|---|---|
| 不变 | C[T’] 与 C[T] 是不相干的 | C[T] |
| 协变 | C[T’] 是 C[T] 的子类 | C[+T] |
| 逆变 | C[T] 是 C[T’] 的子类 | C [-T] |
class Box[T]
//默认泛型类是非变的
//类型B是A的子类型,Box[A]和Box[B]没有任何从属关系
//Java是一样的
class Box[+T]
//类型B是A的子类型,Box[B]可以认为是Box[A]的子类型
//参数化类型的方向和类型的方向是一致的。
class Box[-T]
//类型B是A的子类型,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的类型系统还有很多很多知识点,希望后面有机会继续扩展,也请觉得有收获的小伙伴多评论交流~