[譯] Scala 類型的類型(一)

本文由 Yison 發表在 ScalaCool 團隊博客。javascript

原文

ktoso.github.io/scala-types…java

目錄

1. Scala 類型的不一樣類型

2013 年在幾場 「JavaOne 大會」以後,掀起了一些關於 「Scala 類型」方面的熱議,這篇博文也應運而生。 git

在這些討論聲中,我發現不一樣的人在學習 Scala 的過程當中,常常重複提出相同的問題。我想咱們缺乏一個詳盡的清單,來指明跟 Scala 類型打交道的方法,因此我決定總結下本身已有的經驗,分享在 Scala 中爲何咱們須要這些類型。github

2. 寫做進度

儘管我寫這篇文章已經有段時間了,但始終還有不少內容未完成。好比說「高階類型」部分須要從新梳理,「Self Type」還得補充更多細節,等等等等。詳情參見計劃清單。安全

此外,若是你看到某個部分被打上了 ❌ ,則表示該部分須要修改或者是未完成。oop

3. Type Ascription

Scala 有「類型推導」,這意味着咱們能夠在源碼中省略一些類型聲明。在不顯式聲明類型的前提下,咱們只要書寫 valdef 就夠了。學習

這種顯式指定類型的行爲,被稱爲 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 大概是怎麼一回事。講完這個以後,咱們繼續接下來的話題,類型隨之也會變得愈來愈有趣。

4. 通用類型系統 — Any, AnyRef, AnyVal

咱們之因此說 Scala 的類型系統是通用的,是由於有一個「頂類型」— Any 。這與 Java 很不同,後者存在叫作「原始類型」 ( int , long , float , double , byte , char , short , boolean ) 的特例,它們並不繼承 Java 中相似頂類型的東西 java.lang.Object


Scala's Unified Types

Scala 引入了 Any 做爲全部類型共同的頂類型。AnyAnyRefAnyVal 的超類。

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 同樣,僅僅將它們做爲一個分離的狀況存在。

5. 底類型 - Nothing 與 Null

在 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 的類型是預期的 StringNull 遵循着跟 Nothing 幾乎同樣的規則。我將經過這個例子先探討下 — 類型推導中 AnyValAnyRef 之間的區別。

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 語句(兩個邏輯分支的類型分別是 AnyValnull)的公共類型是 Any,而不是其它。

用一句話來總結就是:

Null 繼承全部的 AnyRefs,而 Nothing 繼承了一切。

因爲 AnyVals (例如數字)跟 AnyRefs 並不在一個繼承樹中,一個數字與一個 null 值惟一的公共類型就是 Any ,這就解釋了咱們的例子。

Types visualized:

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

infered type: Any an object複製代碼
相關文章
相關標籤/搜索