本文由 Prefert 發表在 ScalaCool 團隊博客。html
不管在靜態語言仍是動態語言中,「類型系統」都起到了相當重要的做用。編程
在計算機科學中,類型系統用於定義如何將編程語言中的數值和表達式歸類爲許多不一樣的類型,如何操做這些類型,這些類型如何互相做用。安全
類型能夠確認一個值或者一組值具備特定的意義和目的(雖然某些類型,如抽象類型和函數類型,在程序運行中,可能不表示爲值)。閉包
類型系統在各類語言之間存在比較大的差別。最主要的差別存在於編譯時期的語法,以及運行時期的操做實現方式。咱們能夠簡單理解爲兩個部分:jvm
可是他們的目的都是一致的:編程語言
1. 安全。有了類型系統之後就能夠實現類型安全,這時候程序就變成了一個嚴格的數學證實過程,編譯器能夠機械地驗證程序某種程度的正確性,從而杜絕不少錯誤的發生。好比:Scala、Java。可是 JavaScript 等動態語言/弱類型語言就要藉助其餘插件(如 ESLint)來提示語法等錯誤。ide
2. 抽象能力。在安全的前提下,一個強大的類型系統的標準是抽象能力,能將程序中的不少東西歸入安全的類型系統中進行抽象,這在安全性的前提下又不損耗靈活性,甚至性能也能很優化。動態語言的抽象能力能夠很強,但安全性和性能就不行了。泛型、高階函數(閉包)、類型類、Monad
、Lifetime
(Rust) 屬於這一塊。函數
3. 工程能力。一個強類型的編程語言比動態類型的語言更適合大規模軟件的構建,哪怕不存在性能問題,可是一樣取決於前兩點。性能
Hint: 想深刻了解類型系統的朋友能夠參考 《Type Systems》和 《Types and Programming》學習
Kotlin 做爲一門靜態類型編程語言,一樣擁有着強大的類型系統。
你可能會對類型後面的 ?
產生疑問,那咱們就先來看看 Kotlin 中的可空類型。
Int?
Boolean?
及其餘許多編程語言中最多見的陷阱之一是訪問空引用的成員,致使空引用異常。在 Java 中,這被稱做 NullPointerException
或簡稱 NPE
。
Kotlin 的類型系統旨在從咱們的代碼中消除 NullPointerException
。
NPE 發生的緣由多是
throw NullPointerException();
!!
操做符(要求拋出 NullPointerException
)與 Java 不一樣,Kotlin 區分非空(non-null)和可空(nullable)類型。到目前爲止,咱們看到的類型都是非空類型,Kotlin 不容許 null 做爲這些類型的值。訪問非空類型的變量將永遠不會拋出空指針異常。
因爲 null
只能被存儲在 Java 的引用類型的變量中,因此在 Kotlin 中基本數據的可空版本都會使用該類型的包裝形式。
一樣的,若是你用基本數據類型做爲泛型類的類型參數,Kotlin 一樣會使用該類型的包裝形式。
咱們能夠在任何類型後面加上?
,好比Int?
,實際上等同於Int? = Int or null
,經過合理的使用,咱們可以簡化不少判空代碼。而且咱們可以有效規避 NullPointerException
致使的崩潰。
接下去讓咱們看看,非空的原理到底怎麼樣的。
對於如下一段 Kotlin 代碼:
fun testNullable1(x: String, y: String?): Int {
return x.length
}
fun testNullable2(x: String, y: String?): Int? {
return y?.length
}
fun testNullable3(x: String, y: String?): Int? {
return y!!.length
}
複製代碼
咱們利用 Idea 反編譯後,產生的 Java 代碼以下:
public final class NullableTypesKt {
public static final int testNullable1(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x"); // 若是爲 null, 拋出異常
return x.length();
}
@Nullable
public static final Integer testNullable2(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
return y != null?Integer.valueOf(y.length()):null;
}
@Nullable
public static final Integer testNullable3(@NotNull String x, @Nullable String y) {
Intrinsics.checkParameterIsNotNull(x, "x");
if(y == null) {
Intrinsics.throwNpe();
}
return Integer.valueOf(y.length());
}
}
複製代碼
能夠看到,在不可空變量調用函數以前,都使用 kotlin.jvm.internal.Intrinsics
類裏面的 checkParameterIsNotNull
方法檢查是否爲 null
,若是是 null
則拋出異常:
public static void checkParameterIsNotNull(Object value, String paramName) {
if (value == null) {
throwParameterIsNullException(paramName);
}
}
複製代碼
基於可空類型,Kotlin 才擁有不少促使安全的運算符。
?.
—— 安全調用?.
容許咱們把一次 null
檢查和一次方法的調用合併成一個操做,好比:
str?.toUpperCase()
等同於 if (str != null) str.toUpperCase() else null
固然,?.
一樣能夠處理屬性:
class User(val nickname: String, val master: User?)
fun masterInfo(user: User): String? = user.master?.nickname
// test
val ceo = User("boss", null)
val employee = User("employee-1", ceo)
println(masterInfo(employee)) // boss
println(masterInfo(ceo)) // null
複製代碼
?:
—— Elvis 運算符剛開始我也不知道爲何稱之爲「Elvis 」運算符——直到我看到了這張圖...
若是你不喜歡這個名字,咱們也能夠叫它——「null 合併運算符」。若是你學習過 Scala,這相似於 getOrElse
:
fun getOrElse(str: String?) {
val result: String = str ?: "" // 等價於 str == null ? "" : str
}
複製代碼
另外還有as?
(安全轉換)、!!
(非空斷言)、let
、lateinit
(延遲初始化屬性)等此處就不詳細介紹。
Int
, Boolean
及其餘咱們都知道,Java 將 基本數據類型 和 引用類型 作了區分:
在 Kotlin 中,並不區分基本數據類型和包裝類型 —— 你使用的永遠是同一個類型。
Kotlin 中咱們必須使用 顯示轉換 來對數字進行轉換,例:
fun main(args: Array<String>) {
val z = 13
println(z.toLong() in list(9L, 5L, 2L))
}
複製代碼
若是以爲這種方式不夠簡便,你也能夠嘗試使用 Kotlin 中的字面量:
L
表示 Long
: 123L
F
表示 Float
: .123f
、1e3f
0x
/ 0X
表示十六進制:0xadcL
當你使用字面量去初始化一個類型已知的變量,或是把字面量做爲實參傳給函數時 ,會發生隱式轉換,而且算數運算符會被重載。 例:
fun long(l: Long) = println(1)
fun main(args: Array<String>) {
val b: Byte = 1 // Int -> Byte
val l = b + 1L // 重載 plus 運算符
foo(234)
}
複製代碼
Any
, Any?
和 Object
做爲 Java 類層級結構的頂層相似,Any
類型是 Kotlin 中 全部非空類型(ex: String
, Int
) 的頂級類型——超類。
與 Java 不一樣的是: Kotlin 不區分「原始類型」(primitive type)和其它的類型。它們都是同一類型層級結構的一部分。
若是定義了一個沒有指定父類型的類型,則該類型將是 Any
的直接子類型:
class Fruit(val weight: Double)
複製代碼
若是你爲定義的類型指定了父類型,則該父類型將是新類型的直接父類型,可是新類型的最終祖先爲 Any
。
abstract class Fruit(val weight: Double)
class Banana(weight: Double, val size: Double): Fruit(weight)
class Peach(weight: Double, val color: String): Fruit(weight)
複製代碼
若是你的類型實現了多個接口,那麼它將具備多個直接的父類型,而 Any
一樣是最終的祖先。
interface ICanGoInASalad
interface ICanBeSunDried
class Tomato(weight: Double): Fruit(weight), ICanGoInASalad, ICanBeSunDried
複製代碼
Kotlin 的 Type Checker 強制執行父子關係。
例如: 你能夠將子類型值存儲到父類型變量中:
var f: Fruit = Banana(weight = 0.1)
f = Peach(weight = 0.15)
複製代碼
可是你不能將父類型值存儲到子類型變量中:
val b = Banana(weight=0.1)
val f: Fruit = b
val b2: Banana = f
// Error: Type mismatch: inferred type is Fruit but Banana was expected
複製代碼
正好也符合咱們的平常理解:「香蕉是水果,水果不是香蕉。」
另外,Kotlin 把 Java 方法參數和返回類型中用到的 Object
類型看做 Any
(更確切地是當作「平臺類型」)。當 Kotlin 函數函數中使用 Any
時,它會被編譯成 Java 字節碼中的 Object
。
Hint: 平臺類型本質上就是 Kotlin 不知道可控性信息的類型 —— 全部 Java 引用類型在 Kotlin 中都表現爲平臺類型。
上面提到:在 Kotlin 中, Any
是全部 非空類型 的超類。
你可能會有疑問: null
類型的父類是什麼呢?
Kotlin 是一種表達式導向的語言,全部流程控制語句都是表達式。它沒有 Java 和 C 中的 void
函數,函數老是會返回一個值。有時候函數並無計算任何東西 —— 這被咱們稱做他們的反作用(side effect),這時將會返回 Unit
——具備單一值的類型。
大多數狀況下,你不須要明確指定 Unit
做爲返回類型或從函數返回 Unit
。若是編寫的函數具備塊代碼體,而且不指定返回類型,則編譯器會將其視爲返回 Unit
類型,不然編譯器會使用推斷的類型。
fun example() {
println("block body and no explicit return type, so returns Unit")
}
val u: Unit = example()
複製代碼
Unit
並沒什麼特別之處。就像任何其餘類型同樣,它是 Any
的子類型,而 Unit?
是 Any?
的子類型。
然而 Unit?
類型倒是一個奇怪的特殊例子,這是 Kotlin 的類型系統一致性的結果。Unit?
類型只有兩個值:Unit
單例和 null
。我暫時還沒發現使用 Unit?
類型的地方,可是在類型系統中沒有特殊的 void 這一事實,使得處理各類函數泛型變得更加容易。
在 Kotlin 類型層級結構的最底層是 Nothing
類型。
顧名思義,Nothing
是沒有實例的類型。Nothing
類型的表達式不會產生任何值。
注意 Unit
和 Nothing
之間的區別,對 Unit
類型的表達式求值將返回 Unit
的單例,而對 Nothing
類型的表達式求值則永遠都不會返回。
這意味着任何類型爲 Nothing
的表達式以後的全部代碼都是沒法獲得執行的(unreachable code),編譯器和 IDE 會向你發出警告。
什麼樣的表達式類型爲 Nothing
呢?流程控制中與跳轉相關的表達式。
例如 throw
關鍵字會中斷表達式的計算,並從函數中拋出異常。所以 throw
就是 Nothing
類型的表達式。
經過將 Nothing
做爲全部類型的子類型,類型系統容許程序中的任何表達求值失敗。例如: JVM 在計算表達式時內存不足,或者是有人拔掉了計算機的電源插頭。這也意味着咱們能夠從任何表達式中拋出異常。
fun formatCell(value: Double): String =
if (value.isNaN())
throw IllegalArgumentException("$value is not a number")
else
value.toString()
複製代碼
你可能會驚奇地發現,return
語句的類型也爲 Nothing
。return
是一個流程控制語句,它當即從函數中返回一個值,打斷其所在表達式的求值。
fun formatCellRounded(value: Double): String =
val rounded: Long = if (value.isNaN()) return "#ERROR" else Math.round(value)
rounded.toString()
複製代碼
進入無限循環或殺死當前進程的函數返回類型也爲 Nothing。例如 Kotlin 標準庫將 exitProcess
函數聲明爲:
fun exitProcess(status: Int): Nothing
複製代碼
若是你編寫返回 Nothing
的自定義函數,編譯器一樣能檢查出調用函數後沒法獲得執行的代碼,就像使用語言自己的流程控制語句同樣。
inline fun forever(action: ()->Unit): Nothing {
while(true) action()
}
fun example() {
forever {
println("doing...")
}
println("done") // Warning: Unreachable code
}
複製代碼
與空安全同樣,不可達代碼分析是類型系統的一個特性。無需像 Java 同樣在編譯器和 IDE 中使用一些手段進行特殊處理。
Nothing
像任何其餘類型同樣,若是容許其爲空則能夠獲得對應的類型 Nothing?
。Nothing?
只能包含一個值:null
。事實上 Nothing?
就是 null
的類型。
Nothing?
是全部可空類型的最終子類型,因此咱們可使用 null 做爲任何可空類型的值。
若是你仍是對 Kotlin 類型系統不夠清晰,下面這張圖可能會對你有所幫助:
做爲「Better Java」,Kotlin 的類型系統更加簡潔,同時爲了提升代碼的安全性、可靠性,引入了一些新的特性(ex. Nullable Types 和 Immutable Collection)。
咱們將在下一篇詳細介紹 Kotlin 中的集合。
參考: