解釋一下 Kotlin 的 var、val、const

昨天公衆號後臺收到一位小夥伴的留言詢問,他對於 Kotlin 爲什麼沒有 Java 的 final 關鍵字感到困惑,這應該是不少初學者都會遇到的問題,因此我就寫了這篇博文從更底層的角度來解析 Kotlin 聲明變量時用到的三個關鍵字:varvalconsthtml

其實,Java 的 final 就等價於 Kotlin 的 val, 雖然經過 javap 反編譯能夠看到二者的底層實現不同,可是從語義上講,它們二者的確是等價的。具體緣由,咱們來逐一分析。java

什麼是屬性

咱們知道,在 Kotlin 的世界中,class 已經再也不是惟一的一等公民,咱們能夠直接在代碼文件的最頂層(top-level)聲明類、函數和變量。git

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 那樣依附於類。github

首先,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。spa

  • 屬性的名就是就是咱們用來引用屬性的方式;
  • 屬性的類型能夠顯示聲明,由於 Kotlin 支持類型推導,若是類型可以從上下文推導得出,那麼它也能夠省略;
  • initializer 是類型推導的線索之一,例如 val author = "liangfei",根據 = "liangfei" 能夠得出它是一個 String 類型;
  • getter 也是類型推導的線索之一,全部使用屬性名獲取值的操做,都是經過 getter 來完成的;
  • setter 用於給屬性賦值。

以上只是聲明瞭一個屬性,若是咱們要賦值,它的值會存儲在哪裏呢?其實,編譯器還會自動爲屬性生成一個用於存儲值的字段(field),由於寫代碼時感知不到到它的存在,因此稱爲幕後字段(backing field)。具體能夠參考幕後字段,由於與本文關係不大,因此此處不作介紹。code

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

例如,上面 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 屬性,並且它的值必須在編譯時肯定。

參考資料

相關文章
相關標籤/搜索