在筆者的Kotlin交流群里,不少同學反復遇到了一些相似的問題。這些問題大都比較基礎,但又容易產生誤解。因此,我決定寫一篇文章,整理群里同學遇到的一些問題
變量和常量的使用
在Kotlin語言中,我們使用var聲明變量,使用val聲明常量。由于來自Java語言中沒有區分常量變量的影響,一些同學對這兩個關鍵字的理解有問題。為了理解這兩個變量的區別,我們可以用兩個等式來說明一下:
var str: String = "abc" => public String str = "abc"val str: String = "abc" => public final String str = "abc"
復制代碼
=>符號后面是對應的Java代碼,Java語言使用final關鍵字聲明常量。很明顯,使用明確的變量和常量聲明更有助于理解。
注:一些Java程序員很少使用final關鍵字,這說明這部分同學對于常量的使用不太理解。事實上,JVM中有一個常量池,如果發現常量池中存在該值就直接使用;反之,則創建并存入常量池。從這個層面來說,使用常量比使用變量效率更高。更重要的是,如果你聲明一個不會被改動的變量,使用final修飾將更準確,也更安全。
lateinit
其實,在使用Kotlin語言的這兩年里,我從來沒有用過這個關鍵詞。但剛剛接觸Kotlin語言的同學似乎很喜歡使用這個修飾符修飾變量。
這個關鍵詞是做什么的呢?這很有意思!
在Kotlin語言中,我們必須嚴格區分可選值和非可選值。而無論是可選值還是非可選值,在聲明的時候你都必須首先初始化。
那么,如果本身是一個非可選值,但在初始化的時候我們并不知道應該賦什么初始值。或者說,我們壓根就不想賦初始值,該怎么辦?lateinit就是用于解決這個問題的。
其實這個場景的確廣泛存在,比如這個變量是一個對象類型的數據。很明顯,給一個對象變量賦予一個初始值的意義不大。因此,你可以選擇使用lateinit修飾這個變量。可是,與此同時,你的災難也降臨了!
群里同學反饋多次的一個問題就是:提示變量沒有初始化。
其實,本身這個問題并不難,但難的是你要完全弄清楚使用lateinit的前提。如果你決定使用lateinit,你至少應該記住下面兩個規則:
- lateinit只能用于修飾非可選值。因此,必須確保你的這個變量在任何時候都不會被賦值為空。
- lateinit表示這個變量的初始化可能發生在任何時候。因此。使用lateinit之前,問一問自己。你是否非常清楚你一定會在使用這個變量之前將其進行初始化。
為了避免因為未初始化引起的異常問題,Kotlin語言為每一個lateini屬性實例提供了一個判斷是否已經初始化的屬性值isInitialized。因此,為了避免出現初始化問題,你最好判斷一下這個變量是否已經完成初始化:
private lateinit var dog: Dog
if (::dog.isInitialized) {....
}
復制代碼
非可選值中的空指針陷阱
部分同學喜歡這樣聲明數據類:
data class Ticket(var id: Long, var name: String ...)
復制代碼
對于客戶端類應用,數據類通常對應后臺返回的一段Json字符串。那么,悲劇又誕生了!如果后臺沒有返回name字段,Json框架在進行數據解析的時候認為name為空值,嘗試將其賦值為空。不可預料地,臭名昭著的空指針異常又出現了。
因此,記住一個原則:除非你確定這個變量一定不會被賦值為空。否則,請盡量使用可選值。
可選值中的空指針陷阱
類似地,在可選值中也存在著空指針陷阱。而因為受到Java語言的影響,這個部分出現空指針異常的概率更高。看下面的例子:
var isRight: Boolean? = nullif (isRight!!) {...
}
復制代碼
對于上面的代碼,Kotlin將毫不留情地拋給你一個空指針異常。比Java空指針異常更溫柔的是,這個空指針異常的名稱叫做KotlinNullPointerException。
因此,記住一個原則,如果使用可選值需要進行解包的時候。一定要確定這個可選值此刻是有值的。針對上面這個例子,更好的處理方式應該是這樣:
var isRight: Boolean? = nullif (isRight ?: false) {...
}
復制代碼
不要誤會,我沒有基本數據類型
Kotlin認為所謂的基本數據類型,所謂的拆包,封包是沒有意義的。因此,在Kotlin語言中所有的基本數據類型變量也是對象,擁有與變量一樣的行為。
所以,記住一個原則,從Java轉換到Kotlin,在使用基本數據類型變量的時候同樣需要注意合理地選擇可選值和非可選值,慎用lateinit。
雙冒號到底是個什么東西
雙冒號(::)操作符是Kotlin語言特有的操作符。它主要有以下幾個作用:
- 獲取KClass引用
- 獲取函數引用
- 獲取屬性引用
- 獲取構造函數引用
獲取KClass引用
這是很常用的表達式,不過通常用于獲取java的Class實例:
val javaClass = Person::class.java
復制代碼
注:這在Android開發中比較常用,通常用于獲取Activity的Java class實例。
獲取函數引用
在Kotlin語言中,你可以使用函數作為某個高階函數的參數。使用雙冒號操作符可以用于獲取具體的函數引用作為參數傳入目標函數:
fun cdn(x: Int): Boolean {return x >= 3
}fun filter(x: Int, condition: (x: Int)->Boolean): Boolean {return condition(x)
}filter(5, ::cdn)
復制代碼
獲取屬性引用
Kotlin類中每一個成員變量對應一個Property實例,使用雙冒號操作符可以用于獲取該屬性實例。在lateinit場景中,這很有用!
class Dog {var name: String? = null
}
// 注意:這里獲取的是Property實例,而非屬性本身
val property = Dog::nameval receiver = Dog()
println(property.get(receiver))
復制代碼
注:類對象變量本身并沒有isInitialized屬性,要判斷lateinit變量是否已經完成初始化,需要通過雙冒號獲取該變量對應的Property實例才能判斷。
獲取構造函數引用
雙冒號操作符也可以用于獲取某個對象的構造函數實例,具體的用法是:在類名稱前面使用雙冒號。看下面的例子:
class Dog {var name: String? = null
}val init = ::Dog
val dog = init()
println(dog.name)
復制代碼
注:該構造函數實例同樣可以作為參數傳入某個高階函數中。
PS:雙冒號操作符其實就是用于簡化Kotlin反射而創造的一種操作符。
簡單總結
你在日常使用Kotlin語言的過程中還有遇到其它問題嗎?如果有,請留言告訴我!
歡迎加入Kotlin交流群
如果你也喜歡Kotlin語言,歡迎加入我的Kotlin交流群: 329673958 ,一起來參與Kotlin語言的推廣工作。