解釋一下 Kotlin 的 var、val、const

liangfei發表於2019-04-01

昨天公眾號後臺收到一位小夥伴的留言詢問,他對於 Kotlin 為何沒有 Java 的 final 關鍵字感到困惑,這應該是很多初學者都會遇到的問題,所以我就寫了這篇博文從更底層的角度來解析 Kotlin 宣告變數時用到的三個關鍵字:varvalconst

其實,Java 的 final 就等價於 Kotlin 的 val, 雖然通過 javap 反編譯可以看到兩者的底層實現不一樣,但是從語義上講,它們兩者的確是等價的。具體原因,我們來逐一分析。

什麼是屬性

我們知道,在 Kotlin 的世界中,class 已經不再是唯一的一等公民,我們可以直接在程式碼檔案的最頂層(top-level)宣告類、函式和變數。

class Address {
  // class properties
  var province = "zhejiang"
  val city = "hangzhou"
}

fun prettify(address: Address): String {
  // local variable
  val district = "xihu"
  return district + ',' + address.city + ',' + address.province
}

// top-level property
val author = "liangfei"
複製程式碼

上例中的 Address 是一個類,prettify 是一個函式,author 是一個變數,它們都是一等公民,也就是說,函式和變數可以單獨存在,不會像 Java 那樣依附於類。

首先,varval 可分為三種型別:

  • 類的屬性(class property),例如上例中的 var province = "zhejiang",它是 Address 類的一個屬性;
  • 頂層屬性(top-level property),例如上例中的 val author = "liangfei",它是檔案(module)的一個屬性;
  • 區域性變數(local variable),例如上例中的 val district = "xihu",它是函式 prettify 的一個區域性變數。

類的屬性和頂層屬性都是屬性,所以可以統一來看待,屬性本身不會儲存值,也就是說它不是一個欄位(field),那它的值是哪裡來的呢?我們先來看一下宣告一個屬性的完整語法:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
複製程式碼

可以看出,一個屬性的宣告可以分解為五個部分:屬性名、屬性型別、initializer、getter、setter。

  • 屬性的名就是就是我們用來引用屬性的方式;
  • 屬性的型別可以顯示宣告,因為 Kotlin 支援型別推導,如果型別能夠從上下文推導得出,那麼它也可以省略;
  • initializer 是型別推導的線索之一,例如 val author = "liangfei",根據 = "liangfei" 可以得出它是一個 String 型別;
  • getter 也是型別推導的線索之一,所有使用屬性名獲取值的操作,都是通過 getter 來完成的;
  • setter 用於給屬性賦值。

以上只是宣告瞭一個屬性,如果我們要賦值,它的值會儲存在哪裡呢?其實,編譯器還會自動為屬性生成一個用於儲存值的欄位(field),因為寫程式碼時感知不到到它的存在,所以稱為幕後欄位(backing field)。具體可以參考幕後欄位,因為與本文關係不大,所以此處不做介紹。

varval 所宣告的屬性,其最本質的區別就是:val 不能有 setter,這就達到了 Java 中 final 的效果。

例如,上面 Kotlin 程式碼中的 Address 類:

class Address {
  var province = "zhejiang"
  val city = "hangzhou"
}
複製程式碼

它在 JVM 平臺上的實現是下面這樣的(通過 javap 命令檢視):

public final class Address {
  public final java.lang.String getProvince();
  public final void setProvince(java.lang.String);
  public final java.lang.String getCity();
  public Address();
}
複製程式碼

可以看出,針對 var province 屬性,生成了 getProvince()setProvince(java.lang.String) 兩個函式。但是 val city 只生成了一個 getCity() 函式。

對於區域性變數來說,var 或者 val 都無法生成 getter 或 setter,所以只會在編譯階段做檢查。

看一下它的官方定義(中文版可參考屬性和欄位):

Classes in Kotlin can have properties. These can be declared as mutable, using the var keyword or read-only using the val keyword.

對於類的屬性來說:var 表示可變(mutable),val 表示只讀(read-only)。對於頂層屬性來說也是一樣的。

可變和只讀

var 表示可變,val 表示只讀,而不是不可變(immutable)。我們已經知道了 val 屬性只有 getter,但是這並不能保證它的值是不可變的。例如,下面的程式碼:

class Person {
  var name = "liangfei"
  var age = 30

  val nickname: String
    get() {
      return if (age > 30) "laoliang" else "xiaoliang"
    }

  fun grow() {
    age += 1
  }
}
複製程式碼

屬性 nickname 的值並非不可變,當呼叫 grow() 方法時,它的值會從 "laoliang" 變為 "xiaoliang",但是無法直接給 nickname 賦值,也就是說,它不能位於賦值運算的左側,只能位於右側,這就說明了為什麼它是隻讀(read-only),而不是不可變(immutable)。

其實,Kotlin 有專門的語法來定義可變和不可變的變數,後面會專門寫一篇博問來分析,這裡不再深入。

我們知道,Java 中可以使用 static final 來定義常量,這個常量會存放於全域性常量區,這樣編譯器會針對這些變數做一些優化,例如,有三個字串常量,他們的值是一樣的,那麼就可以讓這個三個變數指向同一塊空間。我們還知道,區域性變數無法宣告為 static final,因為區域性變數會存放在棧區,它會隨著呼叫的結束而銷燬。

Kotlin 引入一個新的關鍵字 const 來定義常量,但是這個常量跟 Java 的 static final 是有所區別的,如果它的值無法在編譯時確定,則編譯不過,因此 const 所定義的常量叫編譯時常量

編譯時常量

首先,const 無法定義區域性變數,除了區域性變數位於棧區這個原因之外,還因為區域性變數的值無法在編譯期間確定,因此,const 只能修飾屬性(類屬性、頂層屬性)。

因為 const 變數的值必須在編譯期間確定下來,所以它的型別只能是 String 或基本型別,並且不能有自定義的 getter。

所以,編譯時常量需要滿足如下條件:

  • 頂層或者 object 的成員(object 也是 Kotlin 的一個新特性,具體可參考物件宣告)。
  • 初始化為一個 String 或者基本型別的值
  • 沒有自定義 getter

總結

最後,總結一下:

  • varval 宣告的變數分為三種型別:頂層屬性、類屬性和區域性變數;
  • var 屬性可以生成 getter 和 setter,是可變的(mutable),val 屬性只有 getter,是隻讀的(read-only,注意不是 immutable);
  • 區域性變數只是一個普通變數,不會有 getter 和 setter,它的 val 等價於 Java 的 final,在編譯時做檢查。
  • const 只能修飾沒有自定義 getter 的 val 屬性,而且它的值必須在編譯時確定。

參考資料

相關文章