常見Kotlin高頻問題解惑

歐陽鋒發表於2018-04-19

文 | 歐陽鋒

在筆者的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,你至少應該記住下面兩個規則:

  1. lateinit只能用於修飾非可選值。因此,必須確保你的這個變數在任何時候都不會被賦值為空。
  2. 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? = null

if (isRight!!) {
   ...
}
複製程式碼

對於上面的程式碼,Kotlin將毫不留情地拋給你一個空指標異常。比Java空指標異常更溫柔的是,這個空指標異常的名稱叫做KotlinNullPointerException

因此,記住一個原則,如果使用可選值需要進行解包的時候。一定要確定這個可選值此刻是有值的。針對上面這個例子,更好的處理方式應該是這樣:

var isRight: Boolean? = null

if (isRight ?: false) {
   ...
}
複製程式碼

不要誤會,我沒有基本資料型別

Kotlin認為所謂的基本資料型別,所謂的拆包,封包是沒有意義的。因此,在Kotlin語言中所有的基本資料型別變數也是物件,擁有與變數一樣的行為。

所以,記住一個原則,從Java轉換到Kotlin,在使用基本資料型別變數的時候同樣需要注意合理地選擇可選值和非可選值,慎用lateinit。

雙冒號到底是個什麼東西

雙冒號(::)操作符是Kotlin語言特有的操作符。它主要有以下幾個作用:

  1. 獲取KClass引用
  2. 獲取函式引用
  3. 獲取屬性引用
  4. 獲取建構函式引用

獲取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::name

val 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語言的推廣工作。

程式設計,我們是認真的!

關注歐陽鋒工作室,與歐陽鋒同行!

歐陽鋒工作室