Scala 类型的类型(一)
目錄
- 1. Scala 類型的不同類型
- 2. 寫作進度
- 3. Type Ascription
- 4. 通用類型系統 — Any, AnyRef, AnyVal
- 5. 底類型 - Nothing 與 Null
1. Scala 類型的不同類型
2013 年在幾場 「JavaOne 大會」之后,掀起了一些關于 「Scala 類型」方面的熱議,這篇博文也應運而生。
在這些討論聲中,我發現不同的人在學習 Scala 的過程中,經常重復提出相同的問題。我想我們缺少一個詳盡的清單,來指明跟 Scala 類型打交道的方法,所以我決定總結下自己的經驗,分享在 Scala 中為什么我們需要這些類型。
2. 寫作進度
盡管我寫這篇文章已經有段時間了,但始終還有很多內容未完成。比如說「高階類型」部分需要重新梳理,「Self Type」還得補充更多細節,等等等等。詳情參見計劃清單。
此外,如果你看到某個部分被打上了 ? ,則表示該部分需要修改或者是未完成。
3. Type Ascription
Scala 有「類型推導」,這意味著我們可以在源碼中省略一些類型聲明。在不顯式聲明類型的前提下,我們只要書寫?val或?def?就夠了。
這種顯式指定類型的行為,被稱為 Type Ascription(有時候,也有叫作 Type Annotation,但這個名字很容易造成混淆,在 Scala 文檔中并不這么使用)。
1 2 3 4 5 6 7 8 | 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 。當然你也可以針對每個公有的方法顯示聲明返回類型(一個非常好的習慣),這能使讓代碼可讀性更好。
你可以根據以下的提示問題,來決定是否使用 Type Ascription :
Q: 如果它是一個參數?
A: 必須使用。
Q: 如果它是一個公有方法的返回值?
A: 為了更好的代碼可讀性,及輸出類型的可控性,需要使用。
Q: 如果它是一個遞歸或重載的方法?
A: 必須使用。
Q: 當你需要返回一個比隱式推導結果更通用的接口?
A: 除非你愿意暴露實現細節,否則必須使用。
除上述情況之外,則可以不必顯式聲明類型。
補充說明:
使用 Type Ascription 可以加快編譯的速度,通常我們也很樂意看到一個方法的返回類型。
好了,我們現在明白了 Type Ascription 大概是怎么一回事。講完這個之后,我們繼續接下來的話題,類型隨之也會變得越來越有趣。
4. 通用類型系統 — Any, AnyRef, AnyVal
我們之所以說 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?的實例。
1 2 3 4 5 6 7 8 9 10 | 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?來調查下上述的例子,這樣子可以找到我們的測試類產生的代碼。
1 2 3 | 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 會在盡可能的情況下使用原始類型。
另一方面,我們也可以限制一個方法只能采用輕量級的值類型:
1 2 3 4 5 6 | 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 一樣,僅僅將它們作為一個分離的情況存在。
5. 底類型 - Nothing 與 Null
在 Scala 中,一切皆有類型…… 但你是否想過,當遇到一些非正常的情況,比如拋出異常的時候,類型推導是如何保持正常運轉,推斷出合理的類型。
讓我們通過以下的?if/else throw?的例子來一探究竟:
1 2 3 4 5 | val thing: Int = if (test) 42 // : Int else throw new Exception("Whoops!") // : Nothing |
正如你在注釋里所看到的,if?塊的返回類型是?Int(很明顯),else?代碼塊的類型是?Nothing(有點意思)。推導器之所以能夠推斷?thing?的類型將永遠是?Int,主要是?Nothing?類型的「底類型」性質在起作用。
一個關于「底類型」如何運作的準確直覺是:Nothing 繼承了所有類型。
類型推導總是會尋找?if?語句兩個邏輯分支的「共同類型」。因此如果?else?分支這里是一個繼承所有類型的子類型,那么最終推斷出來的結果自然會是第一個分支的類型。
1 2 3 4 | Types visualized: [Int] -> ... -> AnyVal -> Any Nothing -> [Int] -> ... -> AnyVal -> Any |
同樣的道理也適用于 Scala 中的第二個底類型 -?Null?。
1 2 3 4 5 | val thing: String = if (test) "Yay!" // : String else ????null // : Null |
thing?的類型是預期的?String。?Null?遵循著跟?Nothing?幾乎一樣的規則。我將通過這個例子先探討下 — 類型推導中?AnyVal?與?AnyRef?之間的區別。
1 2 3 4 5 6 | Types visualized: [String] -> AnyRef -> Any Null -> [String] -> AnyRef -> Any infered type: String |
讓我們考慮下?Int?及其它不能兼容?Null?值的原始類型。我們在 REPL 中使用?:type?命令來調查這個情況(這樣可以返回一個表達式的類型)。
1 2 | scala> :type if (false) 23 else null Any |
這跟上面一個分支對象為?String?類型的例子不同。因為?Null?不像?Nothing?一樣繼承任何類型,我們來詳細研究一下這里的類型。讓我們再次使用?:type?命令來看看?Int?到底繼承了什么:
1 2 3 4 5 6 | scala> :type -v 12 // Type signature Int // Internal Type structure TypeRef(TypeSymbol(final abstract class Int extends AnyVal)) |
verbose?參數在這里新增了一些信息,現在我們知道了?Int?是 一個?AnyVal,后者是個特殊的用于表示值類型的?class,它不能兼容?Null。如果我們看?AnyVal?的源碼,我們將發現:
1 | abstract class AnyVal extends Any with NotNull |
我之所以要講是這里,是因為?AnyVal?的核心功能在這里通過類型很好地表示出來了。注意那個?NotNull?特質(trait)。
回到主題,為什么上面?if?語句(兩個邏輯分支的類型分別是?AnyVal?和?null)的公共類型是?Any,而不是其它。
用一句話來總結就是:
Null 繼承所有的 AnyRefs,而 Nothing 繼承了一切。
由于 AnyVals (例如數字)跟 AnyRefs 并不在一個繼承樹中,一個數字與一個?null?值唯一的公共類型就是?Any?,這就解釋了我們的例子。
1 2 3 4 5 6 | Types visualized: Int -> NotNull -> AnyVal -> [Any] Null -> AnyRef -> [Any] infered type: Any an object |
總結
以上是生活随笔為你收集整理的Scala 类型的类型(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从 Java 到 Scala(一):面向
- 下一篇: play!framework框架概述