本文由 Yison 發表在 ScalaCool 團隊博客。javascript
ktoso.github.io/scala-types…java
2013 年在幾場 「JavaOne 大會」以後,掀起了一些關於 「Scala 類型」方面的熱議,這篇博文也應運而生。 git
在這些討論聲中,我發現不一樣的人在學習 Scala 的過程當中,常常重複提出相同的問題。我想咱們缺乏一個詳盡的清單,來指明跟 Scala 類型打交道的方法,因此我決定總結下本身已有的經驗,分享在 Scala 中爲何咱們須要這些類型。github
儘管我寫這篇文章已經有段時間了,但始終還有不少內容未完成。好比說「高階類型」部分須要從新梳理,「Self Type」還得補充更多細節,等等等等。詳情參見計劃清單。安全
此外,若是你看到某個部分被打上了 ❌ ,則表示該部分須要修改或者是未完成。oop
Scala 有「類型推導」,這意味着咱們能夠在源碼中省略一些類型聲明。在不顯式聲明類型的前提下,咱們只要書寫 val
或 def
就夠了。學習
這種顯式指定類型的行爲,被稱爲 Type Ascription(有時候,也有叫做 Type Annotation,但這個名字很容易形成混淆,在 Scala 文檔中並不這麼使用)。測試
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複製代碼
在此類狀況下,咱們能夠不使用 Type Ascription 。固然你也能夠針對每一個公有的方法顯示聲明返回類型(一個很是好的習慣),這能使讓代碼可讀性更好。ui
你能夠根據如下的提示問題,來決定是否使用 Type Ascription :spa
Q: 若是它是一個參數?
A: 必須使用。
Q: 若是它是一個公有方法的返回值?
A: 爲了更好的代碼可讀性,及輸出類型的可控性,須要使用。
Q: 若是它是一個遞歸或重載的方法?
A: 必須使用。
Q: 當你須要返回一個比隱式推導結果更通用的接口?
A: 除非你願意暴露實現細節,不然必須使用。
除上述狀況以外,則能夠沒必要顯式聲明類型。
補充說明:
使用 Type Ascription 能夠加快編譯的速度,一般咱們也很樂意看到一個方法的返回類型。
好了,咱們如今明白了 Type Ascription 大概是怎麼一回事。講完這個以後,咱們繼續接下來的話題,類型隨之也會變得愈來愈有趣。
咱們之因此說 Scala 的類型系統是通用的,是由於有一個「頂類型」— Any
。這與 Java 很不同,後者存在叫作「原始類型」 ( int
, long
, float
, double
, byte
, char
, short
, boolean
) 的特例,它們並不繼承 Java 中相似頂類型的東西 java.lang.Object
。
Scala 引入了 Any
做爲全部類型共同的頂類型。Any
是 AnyRef
和 AnyVal
的超類。
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 // Int, kept as low-level `int` during runtime
allThings += myInt // Int (extends AnyVal)
// has to be boxed (!) -> becomes java.lang.Integer in the collection (!)
allThings += new Person() // Person (extends AnyRef), no magic here複製代碼
雖然在 JVM 層一旦遭遇 ArrayBuffer[Any]
,咱們的 Int 實例就會被打包成對象。對於類型系統而言,這一切還算是透明的。咱們能夠經過 Scala REPL 和 :javap
來調查下上述的例子,這樣子能夠找到咱們的測試類產生的代碼。
35: invokevirtual #47 // Method myInt:()I
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;複製代碼
你將注意到 myInt
起初仍是攜帶一個原始 int
類型的值。而後,在它即將被添加到 ArrayBuffer
的時候,scalac 植入了一個方法 BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer
(提醒下不是常常跟「字節碼」打交道的讀者,這個方法就是 public Integer boxToInteger(i: int)
)。
經過這麼一個智能的編譯器,以及在這套公共繼承體系中將全部東西都當成一個對象來處理,咱們就可以擺脫「原始類型」這種邊緣狀況的糾纏,至少在咱們的 Scala 源碼中,編譯器會爲咱們處理它。
固然在 JVM 層面,這種差別依舊存在。因爲「原始類型」的操做更安全,同時佔用更少的內存(對象明顯要佔用更多),scalac 會在儘量的狀況下使用原始類型。
另外一方面,咱們也能夠限制一個方法只能採用輕量級的值類型:
def check(in: AnyVal) = ()
check(42) // Int -> AnyVal
check(13.37) // Double -> AnyVal
check(new Object) // -> AnyRef = fails to compile複製代碼
在上述例子中,咱們使用了一個 TypeClass Checker[T]
與類型邊界 (type bound)(後續會詳談)。整體思路就是這個方法只能採用 Value Classes ,如 Int 或咱們本身的值類型。雖然這不是慣用的方法,但這展現了 Scala 的類型系統如何擁抱 Java 的原始類型,把它們引入到 「真正的」 類型系統裏面,而不是像 Java 同樣,僅僅將它們做爲一個分離的狀況存在。
在 Scala 中,一切皆有類型…… 但你是否想過,當遇到一些非正常的狀況,好比拋出異常的時候,類型推導是如何保持正常運轉,推斷出合理的類型。
讓咱們經過如下的 if/else throw
的例子來一探究竟:
val thing: Int =
if (test)
42 // : Int
else
throw new Exception("Whoops!") // : Nothing複製代碼
正如你在註釋裏所看到的,if
塊的返回類型是 Int
(很明顯),else
代碼塊的類型是 Nothing
(有點意思)。推導器之因此可以推斷 thing
的類型將永遠是 Int
,主要是 Nothing
類型的「底類型」性質在起做用。
一個關於「底類型」如何運做的準確直覺是:Nothing 繼承了全部類型。
類型推導老是會尋找 if
語句兩個邏輯分支的「共同類型」。所以若是 else
分支這裏是一個繼承全部類型的子類型,那麼最終推斷出來的結果天然會是第一個分支的類型。
Types visualized:
[Int] -> ... -> AnyVal -> Any
Nothing -> [Int] -> ... -> AnyVal -> Any複製代碼
一樣的道理也適用於 Scala 中的第二個底類型 - Null
。
val thing: String =
if (test)
"Yay!" // : String
else
null // : Null複製代碼
thing
的類型是預期的 String
。 Null
遵循着跟 Nothing
幾乎同樣的規則。我將經過這個例子先探討下 — 類型推導中 AnyVal
與 AnyRef
之間的區別。
Types visualized:
[String] -> AnyRef -> Any
Null -> [String] -> AnyRef -> Any
infered type: String複製代碼
讓咱們考慮下 Int
及其它不能兼容 Null
值的原始類型。咱們在 REPL 中使用 :type
命令來調查這個狀況(這樣能夠返回一個表達式的類型)。
scala> :type if (false) 23 else null
Any複製代碼
這跟上面一個分支對象爲 String
類型的例子不一樣。由於 Null
不像 Nothing
同樣繼承任何類型,咱們來詳細研究一下這裏的類型。讓咱們再次使用 :type
命令來看看 Int
到底繼承了什麼:
scala> :type -v 12
// Type signature
Int
// Internal Type structure
TypeRef(TypeSymbol(final abstract class Int extends AnyVal))複製代碼
verbose
參數在這裏新增了一些信息,如今咱們知道了 Int
是 一個 AnyVal
,後者是個特殊的用於表示值類型的 class
,它不能兼容 Null
。若是咱們看 AnyVal
的源碼,咱們將發現:
abstract class AnyVal extends Any with NotNull複製代碼
我之因此要講是這裏,是由於 AnyVal
的核心功能在這裏經過類型很好地表示出來了。注意那個 NotNull
特質(trait)。
回到主題,爲何上面 if
語句(兩個邏輯分支的類型分別是 AnyVal
和 null
)的公共類型是 Any
,而不是其它。
用一句話來總結就是:
Null 繼承全部的 AnyRefs,而 Nothing 繼承了一切。
因爲 AnyVals (例如數字)跟 AnyRefs 並不在一個繼承樹中,一個數字與一個 null
值惟一的公共類型就是 Any
,這就解釋了咱們的例子。
Types visualized:
Int -> NotNull -> AnyVal -> [Any]
Null -> AnyRef -> [Any]
infered type: Any an object複製代碼