Kotlin研發第四彈——基礎類型

基礎類型

在 Kotlin 中,所有東西都是對象,在這個意義上講我們可以在任何變量上調用成員函數與屬性。 一些類型可以有特殊的內部表示——例如,數字、字符以及布爾值可以在運行時表示爲原生類型值,但是對於用戶來說,它們看起來就像普通的類。 在本節中,我們會描述 Kotlin 中使用的基本類型:數字、字符、布爾值、數組與字符串。

數字

Kotlin 處理數字在某種程度上接近 Java,但是並不完全相同。例如,對於數字沒有隱式拓寬轉換(如 Java 中 int可以隱式轉換爲long——譯者注),另外有些情況的字面值略有不同。

Kotlin 提供瞭如下的內置類型來表示數字(與 Java 很相近):

Type Bit width
Double 64
Float 32
Long 64
Int 32
Short 16
Byte 8

注意在 Kotlin 中字符不是數字

字面常量

數值常量字面值有以下幾種:

  • 十進制:

    123
    
    • Long 類型用大寫 L 標記: 123L
  • 十六進制: 0x0F

  • 二進制: 0b00001011

注意: 不支持八進制

Kotlin 同樣支持浮點數的常規表示方法:

  • 默認 double:123.5123.5e10
  • Float 用 f 或者 F 標記: 123.5f

數字字面值中的下劃線(自 1.1 起)

你可以使用下劃線使數字常量更易讀:

val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

表示方式

在 Java 平臺數字是物理存儲爲 JVM 的原生類型,除非我們需要一個可空的引用(如 Int?)或泛型。 後者情況下會把數字裝箱。

注意數字裝箱不一定保留同一性:

val a: Int = 10000
println(a === a) // 輸出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA === anotherBoxedA) // !!!輸出“false”!!!

Target platform: JVMRunning on kotlin v. 1.3.21

另一方面,它保留了相等性:

val a: Int = 10000
println(a == a) // 輸出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA == anotherBoxedA) // 輸出“true”

Target platform: JVMRunning on kotlin v. 1.3.21

顯式轉換

由於不同的表示方式,較小類型並不是較大類型的子類型。 如果它們是的話,就會出現下述問題:

// 假想的代碼,實際上並不能編譯:
val a: Int? = 1 // 一個裝箱的 Int (java.lang.Integer)
val b: Long? = a // 隱式轉換產生一個裝箱的 Long (java.lang.Long)
print(b == a) // 驚!這將輸出“false”鑑於 Long 的 equals() 會檢測另一個是否也爲 Long

所以相等性會在所有地方悄無聲息地失去,更別說同一性了。

因此較小的類型不能隱式轉換爲較大的類型。 這意味着在不進行顯式轉換的情況下我們不能把 Byte 型值賦給一個 Int 變量。

val b: Byte = 1 // OK, 字面值是靜態檢測的
val i: Int = b // 錯誤

Target platform: JVMRunning on kotlin v. 1.3.21

我們可以顯式轉換來拓寬數字

val i: Int = b.toInt() // OK:顯式拓寬
print(i)

Target platform: JVMRunning on kotlin v. 1.3.21

每個數字類型支持如下的轉換:

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double
  • toChar(): Char

缺乏隱式類型轉換很少會引起注意,因爲類型會從上下文推斷出來,而算術運算會有重載做適當轉換,例如:

val l = 1L + 3 // Long + Int => Long

運算

Kotlin支持數字運算的標準集,運算被定義爲相應的類成員(但編譯器會將函數調用優化爲相應的指令)。 參見運算符重載

對於位運算,沒有特殊字符來表示,而只可用中綴方式調用命名函數,例如:

val x = (1 shl 2) and 0x000FF000

這是完整的位運算列表(只用於 IntLong):

  • shl(bits) – 有符號左移 (Java 的 <<)
  • shr(bits) – 有符號右移 (Java 的 >>)
  • ushr(bits) – 無符號右移 (Java 的 >>>)
  • and(bits) – 位與
  • or(bits) – 位或
  • xor(bits) – 位異或
  • inv() – 位非

浮點數比較

本節討論的浮點數操作如下:

  • 相等性檢測:a == ba != b
  • 比較操作符:a < ba > ba <= ba >= b
  • 區間實例以及區間檢測:a..bx in a..bx !in a..b

當其中的操作數 ab 都是靜態已知的 FloatDouble 或者它們對應的可空類型(聲明爲該類型,或者推斷爲該類型,或者智能類型轉換的結果是該類型),兩數字所形成的操作或者區間遵循 IEEE 754 浮點運算標準。

然而,爲了支持泛型場景並提供全序支持,當這些操作數並非靜態類型爲浮點數(例如是 AnyComparable<……>、 類型參數)時,這些操作使用爲 FloatDouble 實現的不符合標準的 equalscompareTo,這會出現:

  • 認爲 NaN 與其自身相等
  • 認爲 NaN 比包括正無窮大(POSITIVE_INFINITY)在內的任何其他元素都大
  • 認爲 -0.0 小於 0.0

字符

字符用 Char 類型表示。它們不能直接當作數字

fun check(c: Char) {
    if (c == 1) { // 錯誤:類型不兼容
        // ……
    }
}

字符字面值用單引號括起來: '1'。 特殊字符可以用反斜槓轉義。 支持這幾個轉義序列:\t\b\n\r\'\"\\\$。 編碼其他字符要用 Unicode 轉義序列語法:'\uFF00'

我們可以顯式把字符轉換爲 Int 數字:

fun decimalDigitValue(c: Char): Int {
    if (c !in '0'..'9')
        throw IllegalArgumentException("Out of range")
    return c.toInt() - '0'.toInt() // 顯式轉換爲數字
}

當需要可空引用時,像數字、字符會被裝箱。裝箱操作不會保留同一性。

布爾

布爾用 Boolean 類型表示,它有兩個值:truefalse

若需要可空引用布爾會被裝箱。

內置的布爾運算有:

  • || – 短路邏輯或
  • && – 短路邏輯與
  • ! - 邏輯非

數組

數組在 Kotlin 中使用 Array 類來表示,它定義了 getset 函數(按照運算符重載約定這會轉變爲 [])以及 size 屬性,以及一些其他有用的成員函數:

class Array<T> private constructor() {
    val size: Int
    operator fun get(index: Int): T
    operator fun set(index: Int, value: T): Unit

    operator fun iterator(): Iterator<T>
    // ……
}

我們可以使用庫函數 arrayOf() 來創建一個數組並傳遞元素值給它,這樣 arrayOf(1, 2, 3) 創建了 array [1, 2, 3]。 或者,庫函數 arrayOfNulls() 可以用於創建一個指定大小的、所有元素都爲空的數組。

另一個選項是用接受數組大小以及一個函數參數的 Array 構造函數,用作參數的函數能夠返回給定索引的每個元素初始值:

// 創建一個 Array<String> 初始化爲 ["0", "1", "4", "9", "16"]
val asc = Array(5, { i -> (i * i).toString() })
asc.forEach { println(it) }

Target platform: JVMRunning on kotlin v. 1.3.21

如上所述,[] 運算符代表調用成員函數 get()set()

注意: 與 Java 不同的是,Kotlin 中數組是不型變的(invariant)。這意味着 Kotlin 不讓我們把 Array<String>賦值給 Array<Any>,以防止可能的運行時失敗(但是你可以使用 Array<out Any>, 參見類型投影)。

Kotlin 也有無裝箱開銷的專門的類來表示原生類型數組: ByteArrayShortArrayIntArray 等等。這些類與 Array 並沒有繼承關係,但是它們有同樣的方法屬性集。它們也都有相應的工廠方法:

val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]

無符號整型

無符號類型自 Kotlin 1.3 起纔可用,並且目前是實驗性的。詳見下文

Kotlin 爲無符號整數引入了以下類型:

  • kotlin.UByte: 無符號 8 比特整數,範圍是 0 到 255
  • kotlin.UShort: 無符號 16 比特整數,範圍是 0 到 65535
  • kotlin.UInt: 無符號 32 比特整數,範圍是 0 到 2^32 - 1
  • kotlin.ULong: 無符號 64 比特整數,範圍是 0 到 2^64 - 1

無符號類型支持其對應有符號類型的大多數操作。

請注意,將類型從無符號類型更改爲對應的有符號類型(反之亦然)是二進制不兼容變更

無符號類型是使用另一個實驗性特性(即內聯類)實現的。

特化的類

與原生類型相同,每個無符號類型都有相應的爲該類型特化的表示數組的類型:

  • kotlin.UByteArray: 無符號字節數組
  • kotlin.UShortArray: 無符號短整型數組
  • kotlin.UIntArray: 無符號整型數組
  • kotlin.ULongArray: 無符號長整型數組

與有符號整型數組一樣,它們提供了類似於 Array 類的 API 而沒有裝箱開銷。

此外,區間與數列也支持 UIntULong(通過這些類 kotlin.ranges.UIntRangekotlin.ranges.UIntProgressionkotlin.ranges.ULongRangekotlin.ranges.ULongProgression

字面值

爲使無符號整型更易於使用,Kotlin 提供了用後綴標記整型字面值來表示指定無符號類型(類似於 Float/Long):

  • 後綴 uU 將字面值標記爲無符號。確切類型會根據預期類型確定。如果沒有提供預期的類型,會根據字面值大小選擇 UInt 或者 ULong
val b: UByte = 1u  // UByte,已提供預期類型
val s: UShort = 1u // UShort,已提供預期類型
val l: ULong = 1u  // ULong,已提供預期類型

val a1 = 42u // UInt:未提供預期類型,常量適於 UInt
val a2 = 0xFFFF_FFFF_FFFFu // ULong:未提供預期類型,常量不適於 UInt
  • 後綴 uLUL 顯式將字面值標記爲無符號長整型。
val a = 1UL // ULong,即使未提供預期類型並且常量適於 UInt

無符號整型的實驗性狀態

無符號類型的設計是實驗性的,這意味着這個特性改進很快並且沒有給出兼容性保證。當在 Kotlin 1.3+ 中使用無符號算術時,會報出警告表明這個特性是實驗性的。如需移除警告,必須選擇加入(opt-in)無符號類型的實驗性使用。

選擇加入無符號整型有兩種可行的方式:將 API 標記爲實驗性的,或者無需標記。

  • 如需傳播實驗性,要麼使用 @ExperimentalUnsignedTypes 標註使用了無符號整型的聲明,要麼將 -Xexperimental=kotlin.ExperimentalUnsignedTypes 傳給編譯器(請注意,後者會使所編譯的模塊內的所有聲明都具實驗性)
  • 如需選擇加入而不傳播實驗性,要麼使用 @UseExperimental(ExperimentalUnsignedTypes::class) 註解標註聲明,要麼將 -Xuse-experimental=kotlin.ExperimentalUnsignedTypes 傳給編譯器

你的客戶是否必須選擇使用你的 API 取決於你,不過請記住,無符號整型是一個實驗性特性,因此使用它們的 API 可能會因語言的變更而發生突然破壞。

技術細節也參見實驗性 API KEEP

深入探討

關於技術細節與深入探討請參見無符號類型的語言提案

字符串

字符串用 String 類型表示。字符串是不可變的。 字符串的元素——字符可以使用索引運算符訪問: s[i]。 可以用 for 循環迭代字符串:

for (c in str) {
    println(c)
}

Target platform: JVMRunning on kotlin v. 1.3.21

可以用 + 操作符連接字符串。這也適用於連接字符串與其他類型的值, 只要表達式中的第一個元素是字符串:

val s = "abc" + 1
println(s + "def")

Target platform: JVMRunning on kotlin v. 1.3.21

請注意,在大多數情況下,優先使用字符串模板或原始字符串而不是字符串連接。

字符串字面值

Kotlin 有兩種類型的字符串字面值: 轉義字符串可以有轉義字符,以及原始字符串可以包含換行以及任意文本。轉義字符串很像 Java 字符串:

val s = "Hello, world!\n"

轉義採用傳統的反斜槓方式。參見上面的 字符 查看支持的轉義序列。

原始字符串 使用三個引號(""")分界符括起來,內部沒有轉義並且可以包含換行以及任何其他字符:

val text = """
    for (c in "foo")
        print(c)
"""

你可以通過 trimMargin() 函數去除前導空格:

val text = """
    |Tell me and I forget.
    |Teach me and I remember.
    |Involve me and I learn.
    |(Benjamin Franklin)
    """.trimMargin()

默認 | 用作邊界前綴,但你可以選擇其他字符並作爲參數傳入,比如 trimMargin(">")

字符串模板

字符串可以包含模板表達式 ,即一些小段代碼,會求值並把結果合併到字符串中。 模板表達式以美元符($)開頭,由一個簡單的名字構成:

val i = 10
println("i = $i") // 輸出“i = 10”

Target platform: JVMRunning on kotlin v. 1.3.21

或者用花括號括起來的任意表達式:

val s = "abc"
println("$s.length is ${s.length}") // 輸出“abc.length is 3”

Target platform: JVMRunning on kotlin v. 1.3.21

原始字符串與轉義字符串內部都支持模板。 如果你需要在原始字符串中表示字面值 $ 字符(它不支持反斜槓轉義),你可以用下列語法:

val price = """
${'$'}9.99
"""

源文件通常以包聲明開頭:

package foo.bar

fun baz() { ... }
class Goo { ... }

// ……

源文件所有內容(無論是類還是函數)都包含在聲明的包內。 所以上例中 baz() 的全名是 foo.bar.bazGoo 的全名是 foo.bar.Goo

如果沒有指明包,該文件的內容屬於無名字的默認包。

默認導入

有多個包會默認導入到每個 Kotlin 文件中:

根據目標平臺還會導入額外的包:

導入

除了默認導入之外,每個文件可以包含它自己的導入指令。 導入語法在語法中講述。

可以導入一個單獨的名字,如.

import foo.Bar // 現在 Bar 可以不用限定符訪問

也可以導入一個作用域下的所有內容(包、類、對象等):

import foo.* // “foo”中的一切都可訪問

如果出現名字衝突,可以使用 as 關鍵字在本地重命名衝突項來消歧義:

import foo.Bar // Bar 可訪問
import bar.Bar as bBar // bBar 代表“bar.Bar”

關鍵字 import 並不僅限於導入類;也可用它來導入其他聲明:

與 Java 不同,Kotlin 沒有單獨的“import static”語法; 所有這些聲明都用 import 關鍵字導入。

頂層聲明的可見性

如果頂層聲明是 private 的,它是聲明它的文件所私有的(參見 可見性修飾符)。

跳轉上一章 Kotlin研發第三彈——編碼風格

跳轉下一章 Kotlin研發第五彈——控制流:if、when、for、while

發佈了34 篇原創文章 · 獲贊 20 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章