Kotlin的其他技術
目錄
一、解構宣告
二、區間
三、型別檢查與轉換
四、this表示式
五、相等性
六、操作符過載
七、空安全
八、異常
九、型別別名
一、解構宣告
解構宣告能同時建立多個變數,將物件中的資料解析成相對的變數。舉個例子:
//建立一個資料類User
data class User(var name: String, var age: Int)
//獲得User的例項
var user = User("Czh", 22)
//宣告變數 name 和 age
var (name, age) = user
println("name:$name age:$age")
//輸出結果為:name:Czh age:22
複製程式碼
上面程式碼中用解構宣告同時建立兩個變數的時候,會被編譯成以下程式碼:
//指定變數name的值為user第一個引數的值
var name = user.component1()
//指定變數name的值為user第二個引數的值
var age = user.component2()
println("name:$name age:$age")
//輸出結果為:name:Czh age:22
複製程式碼
- 解構宣告和Map
Map可以儲存一組key-value鍵值對,通過解構宣告可以把這些值解構出來。如下所示:
var map = mutableMapOf<String, Any>()
map.put("name", "Czh")
map.put("age", 22)
for ((key, value) in map) {
println("$key:$value")
}
複製程式碼
執行程式碼,輸出結果:
二、區間
1.in
假如現在要判斷 i 是否在 1-5 內,可以這樣寫:
if (i in 1..5) {
println("i 在 1-5 內")
}
複製程式碼
上面程式碼中,1..5
指的是 1-5,in
指的是在…範圍內,如果 i 在範圍 1-5 之內,將會執行後面的程式碼塊,輸出結果。如果想判斷 i 是否不在 1-5 內,可以這樣寫:
//!in表示不在...範圍內
if (i !in 1..5) {
println("i 不在 1-5 內")
}
複製程式碼
上面兩段程式碼等同於:
if (i >= 1 && i <= 5) {
println("i 在 1-5 內")
}
if (i <= 1 && i >= 5) {
println("i 不在 1-5 內")
}
複製程式碼
2.downTo
如果想輸出 1-5 ,可以這樣寫:
for (i in 1..5) println(i)
//輸出12345
複製程式碼
如果倒著來:
for (i in 5..1) println(i)
//什麼也不輸出
複製程式碼
這個時候可以用downTo
函式倒序輸出 5-1
for (i in 5 downTo 1) println(i)
複製程式碼
3.step
上面的程式碼順序輸出12345或倒序54321,按順序+1或者-1,也就是步長為1。如果要修改步長,可以用step
函式,如下所示:
for (i in 1..5 step 2) println(i)
//輸出135
//倒序
for (i in 1 downTo 5 step 2) println(i)
//輸出531
複製程式碼
4.until
上面的程式碼中,使用的範圍都是閉區間,例如1..5
的區間是[1,5],如果要建立一個不包括其結束元素的區間,即區間是[1,5),可以使用until
函式,如下所示:
for (i in 1 until 5) println(i)
//輸出1234
複製程式碼
三、型別檢查與轉換
1.is操作符
在Kotlin中,可以通過is
操作符判斷一個物件與指定的型別是否一致,還可以使用is
操作符的否定形式!is
,舉個例子:
var a: Any = "a"
if (a is String) {
println("a是String型別")
}
if (a !is Int) {
println("a不是Int型別")
}
複製程式碼
執行程式碼,輸出結果為:
2.智慧轉換
在Kotlin中不必使用顯式型別轉換操作,因為編譯器會跟蹤不可變值的is
檢查以及顯式轉換,並在需要時自動插入(安全的)轉換。舉個例子:
var a: Any = "a"
if (a is String) {
println("a是String型別")
println(a.length) // a 自動轉換為String型別
//輸出結果為:1
}
複製程式碼
還可以反向檢查,如下所示:
if (a !is String) return
print(a.length) // a 自動轉換為String型別
複製程式碼
在 && 和 || 的右側也可以智慧轉換:
// `&&` 右側的 a 自動轉換為String
if (a is String && a.length > 0)
// `||` 右側的 a 自動轉換為String
if (a is String || a.length > 0)
複製程式碼
在when表示式和while迴圈裡也能智慧轉換:
when(a){
is String -> a.length
is Int -> a + 1
}
複製程式碼
需要注意的是,當編譯器不能保證變數在檢查和使用之間不可改變時,智慧轉換不能用。智慧轉換能否適用根據以下規則:
- val 區域性變數——總是可以,區域性委託屬性除外;
- val 屬性——如果屬性是 private 或 internal,或者該檢查在宣告屬性的同一模組中執行。智慧轉換不適用於 open 的屬性或者具有自定義 getter 的屬性;
- var 區域性變數——如果變數在檢查和使用之間沒有修改、沒有在會修改它的 lambda 中捕獲、並且不是區域性委託屬性;
- var 屬性——決不可能(因為該變數可以隨時被其他程式碼修改)
3.強制型別轉換
在Kotlin中,用操作符as
進行強制型別轉換,如下所示:
var any: Any = "abc"
var str: String = any as String
複製程式碼
但強制型別轉換是不安全的,如果型別不相容,會丟擲一個異常,如下所示:
var int: Int = 123
var str: String = int as String
//丟擲ClassCastException
複製程式碼
4.可空轉換操作符
null
不能轉換為 String
,因該型別不是可空的。舉個例子:
var str = null
var str2 = str as String
//丟擲TypeCastException
複製程式碼
解決這個問題可以使用可空轉換操作符as?
,如下所示:
var str = null
var str2 = str as? String
println(str2) //輸出結果為:null
複製程式碼
使用安全轉換操作符as?
可以在轉換失敗時返回null
,避免了丟擲異常。
四、this表示式
為了表示當前的接收者我們使用this表示式。當this
在類的成員中,this
指的是該類的當前物件;當this
在擴充套件函式或者帶接收者的函式字面值中,this
表示在點左側傳遞的接收者引數。
- 限定的this
如果this
沒有限定符,它指的是最內層的包含它的作用域。如果要訪問來自外部作用域的this
(一個類或者擴充套件函式, 或者帶標籤的帶接收者的函式字面值)我們使用this@label
,其中@label
是一個代指this
來源的標籤。舉個例子:
class A { // 隱式標籤 @A
inner class B { // 隱式標籤 @B
fun Int.foo() { // 隱式標籤 @foo
val a = this@A // A 的 this
val b = this@B // B 的 this
val c = this // foo() 的接收者,一個 Int
val c1 = this@foo // foo() 的接收者,一個 Int
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者
}
val funLit2 = { s: String ->
// foo() 的接收者,因為它包含的 lambda 表示式
// 沒有任何接收者
val d1 = this
}
}
}
}
複製程式碼
五、相等性
在Kotlin中存在結構相等和引用相等兩中相等判斷。
1.結構相等
使用equals()
或==
判斷,如下所示:
var a = "1"
var b = "1"
if (a.equals(b)) {
println("a 和 b 結構相等")
//輸出結果為:a 和 b 結構相等
}
var a = 1
var b = 1
if (a == b) {
println("a 和 b 結構相等")
//輸出結果為:a 和 b 結構相等
}
複製程式碼
2.引用相等
引用相等指兩個引用指向同一物件,用===
判斷,如下所示:
data class User(var name: String, var age: Int)
var a = User("Czh", 22)
var b = User("Czh", 22)
var c = b
var d = a
if (c == d) {
println("a 和 b 結構相等")
} else {
println("a 和 b 結構不相等")
}
if (c === d) {
println("a 和 b 引用相等")
} else {
println("a 和 b 引用不相等")
}
複製程式碼
執行程式碼,輸出結果為:
六、操作符過載
Kotlin允許對自己的型別提供預定義的一組操作符的實現,這些操作符具有固定的符號表示 (如 +
或 *
)和固定的優先順序。為實現這樣的操作符,我們為相應的型別(即二元操作符左側的型別和一元操作符的引數型別)提供了一個固定名字的成員函式或擴充套件函式。 過載操作符的函式需要用 operator
修飾符標記。
過載操作符
+
是一個一元操作符,下面來對一元操作符進行過載:
//用 operator 修飾符標記
operator fun String.unaryPlus(): String {
return this + this
}
//呼叫
var a = "a"
println(+a) //輸出結果為:aa
複製程式碼
當編譯器處理例如表示式 +a 時,它執行以下步驟:
- 確定 a 的型別,令其為 T;
- 為接收者 T 查詢一個帶有 operator 修飾符的無參函式 unaryPlus(),即成員函式或擴充套件函式;
- 如果函式不存在或不明確,則導致編譯錯誤;
- 如果函式存在且其返回型別為 R,那就表示式 +a 具有型別 R;
除對一元操作符進行過載外,還可以對其他操作符進行過載,其過載方式和原理大致相同。下面來一一列舉:
1.一元操作符
表示式 | 對應的函式 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a– | a.dec() |
2.二元操作符
表示式 | 對應的函式 |
---|---|
a+b | a.plus(b) |
a-b | a.minus(b) |
a*b | a.times(b) |
a/b | a.div(b) |
a%b | a.mod(b) |
a..b | a.rangeTo(b) |
3.in操作符
表示式 | 對應的函式 |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
4.索引訪問操作符
表示式 | 對應的函式 |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ……, i_n] | a.get(i_1, ……, i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ……, i_n] = b | a.set(i_1, ……, i_n, b) |
5.呼叫操作符
表示式 | 對應的函式 |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ……, i_n) | a.invoke(i_1, ……, i_n) |
6.廣義賦值
表示式 | 對應的函式 |
---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b), a.modAssign(b)(已棄用) |
7.相等與不等操作符
表示式 | 對應的函式 |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
8.比較操作符
表示式 | 對應的函式 |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
七、空安全
在Java中,NullPointerException 可能是最常見的異常之一,而Kotlin的型別系統旨在消除來自程式碼空引用的危險。
1.可空型別與非空型別
在Kotlin中,只有下列情況可能導致出現NullPointerException:
- 顯式呼叫 throw NullPointerException();
- 使用了下文描述的 !! 操作符;
- 有些資料在初始化時不一致;
- 外部 Java 程式碼引發的問題。
在 Kotlin 中,型別系統區分一個引用可以容納 null (可空引用)還是不能容納(非空引用)。 例如,String 型別的常規變數不能容納 null:
如果要允許為空,我們可以宣告一個變數為可空字串,在字串型別後面加一個問號?
,寫作 String?
,如下所示:
var b: String? = "b"
b = null
複製程式碼
2.安全呼叫操作符
接著上面的程式碼,如果你呼叫a
的方法或者訪問它的屬性,不會出現NullPointerException,但如果呼叫b
的方法或者訪問它的屬性,編譯器會報告一個錯誤,如下所示:
這個時候可以使用安全呼叫操作符,寫作?.
,在b
後面加安全呼叫操作符,表示如果b
不為null則呼叫b.length
,如下所示:
b?.length
複製程式碼
安全呼叫操作符還能鏈式呼叫,例如一個員工 Bob 可能會(或者不會)分配給一個部門, 並且可能有另外一個員工是該部門的負責人,那麼獲取 Bob 所在部門負責人(如果有的話)的名字,我們寫作:
Bob?.department?.head?.name
//如果Bob分配給一個部門
//執行Bob.department.head?獲取該部門的負責人
//如果該部門有一個負責人
//執行Bob.department.head.name獲取該負責人的名字
複製程式碼
如果該鏈式呼叫中任何一個屬性為null,整個表示式都會返回null。
如果要只對非空值執行某個操作,安全呼叫操作符可以與let
一起使用,如下所示:
val listWithNulls: List<String?> = listOf("A", null, "B")
for (item in listWithNulls) {
item?.let { println(it) }
}
複製程式碼
執行程式碼,輸出結果為:
- 安全的型別轉換
如果物件不是目標型別,那麼常規型別轉換可能會導致ClassCastException
。 另一個選擇是使用安全的型別轉換,如果嘗試轉換不成功則返回null,如下所示:
val i: Int? = i as? Int
複製程式碼
- 可空型別的集合
如果你有一個可空型別元素的集合,並且想要過濾非空元素,你可以使用filterNotNull
來實現。如下所示:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
複製程式碼
3.Elvis 操作符
先看一段程式碼:
val i: Int = if (b != null) b.length else -1
val i = b?.length ?: -1
複製程式碼
這兩行程式碼表達的都是“如果b
不等於null,i = b.length
;如果b
等於null,i = -1
”。第一行程式碼用的是if
表示式,而第二行程式碼使用了Elvis
操作符,寫作?:
。Elvis
操作符表示如果?:
左側表示式非空,就使用左側表示式,否則使用右側表示式。
請注意,因為throw
和return
在Kotlin中都是表示式,所以它們也可以用在Elvis
操作符右側。如下所示:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}
複製程式碼
4. !! 操作符
!!
操作符將任何值轉換為非空型別,若該值為空則丟擲異常。如下所示:
var a = null
a!!
//執行程式碼,丟擲KotlinNullPointerException
複製程式碼
八、異常
Kotlin中所有異常類都是Throwable
類的子類。每個異常都有訊息、堆疊回溯資訊和可選的原因。
使用throw
表示式可以丟擲異常。舉個例子:
throw NullPointerException("NPE")
複製程式碼
使用try
表示式可以捕獲異常。一個try
表示式可以有多個catch
程式碼段;finally
程式碼段可以省略。舉個例子:
try {
//捕獲異常
} catch (e: NullPointerException) {
//異常處理
} catch (e: ClassNotFoundException) {
//異常處理
} finally {
//可選的finally程式碼段
}
複製程式碼
因為Try
是一個表示式,所以它可以有一個返回值。舉個例子:
val a: Int? = try {
parseInt(input)
} catch (e: NumberFormatException) {
null
}
複製程式碼
try
表示式的返回值是 try
塊中的最後一個表示式或者是catch
塊中的最後一個表示式。finally
塊中的內容不會影響表示式的結果。
九、型別別名
Kotlin提供型別別名來代替過長的型別名稱,這些型別別名不會引入新型別,且等效於相應的底層型別。可以通過使用關鍵字typealias
修改型別別名,如下所示:
//使用關鍵字typealias修改型別別名Length
//相當於 Length 就是一個 (String) -> Int 型別
typealias Length = (String) -> Int
//呼叫
fun getLength(l: Length) = l("Czh")
//編譯器把 Length 擴充套件為 (String) -> Int 型別
val l: Length = { it.length }
println(getLength(l)) //輸出結果為:3
複製程式碼
使用型別別名能讓那些看起來很長的型別在使用起來變得簡潔,如下所示:
typealias MyType = (String, Int, Any, MutableList<String> ) -> Unit
//當我們使用的時候
var myType:MyType
//而不需要寫他原來的型別
//var myType:(String, Int, Any, MutableList<String> ) -> Unit
複製程式碼
總結
相對於Java來說,Kotlin有很多新的技術和語法糖,這也是為什麼使用Kotlin來開發Android要優於Java。運用好這些新的東西,能大大加快開發速度。
參考文獻:
Kotlin語言中文站、《Kotlin程式開發入門精要》
推薦閱讀:
從Java到Kotlin(一)為什麼使用Kotlin
從Java到Kotlin(二)基本語法
從Java到Kotlin(三)類和介面
從Java到Kotlin(四)物件與泛型
從Java到Kotlin(五)函式與Lambda表示式
從Java到Kotlin(六)擴充套件與委託
從Java到Kotlin(七)反射和註解
從Java到Kotlin(八)Kotlin的其他技術
Kotlin學習資料總彙
更多精彩文章請掃描下方二維碼關注微信公眾號”AndroidCzh“:這裡將長期為您分享原創文章、Android開發經驗等!
QQ交流群: 705929135