[KotlinConf 2023] 那些令人期待的 Kotlin 2.0 新功能

Andy Lu
11 min readApr 17, 2023
https://www.youtube.com/watch?v=c4f4SCEYA5Q&t=292s

2023年的 KotlinConf 在阿姆斯特丹舉辦了三天的研討會,在會中也公布了在 Kotlin 2.0 中預計會推出的新功能。在這些功能當中,有些項目已經能夠上線了,而有些需要更多的討論才能做出最後的結論。那麼是哪些功能會在 Kotlin 2.0 推出呢?下面我們將針對 Keynote 上介紹的內容,一一來介紹給各位:

Static extensions

Collection literals

Name-based destructuring

Context receivers

Explicit fields

Static extensions — 靜態擴充

KT-11968

目前在 Kotlin 當中,如果需要定義一個全域使用的方法,需要在 companion object內宣告,如:

//file.kt

class File {
companion object {
fun load(fileName: String) {
println("load $fileName")
}
}
}

而在其他 Kotlin 檔案中要呼叫這個函式就只要使用 類別名稱.函式名稱 即可。

fun main() {
File.load(“sample.txt”)
}

不過若是在 Java 內要呼叫這個函式,呼叫時的類別就會需要包含 Companion 類,類別名稱.Companion.函式名稱,如下 :

//readfile.java
public class ReadFile {
public static void main(String[] args) {
File.Companion.load("sample.txt");
}
}

為什麼會這樣呢?將 file.txt 轉成 bytecode 看看:

public final class File {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
...

public static final class Companion {
public final void load(@NotNull String fileName) {
Intrinsics.checkNotNullParameter(fileName, "fileName");
String var2 = "load " + fileName;
System.out.println(var2);
}
...
}
}

從 bytecode 可以發現, companion object會在原類別中建立一個 public static final class Companion的類別,也就是說這個類別是依附在該類別的裡面,所以在裡面定義的物件才叫做共伴物件,在 Java 中,必須要把完整的類別填入,才能夠使用到這個函式,所以才會如上方所看到的

File.Companion.load("sample.txt");

那麼,如果能夠類似 Java 的用法,將方法宣告成靜態方法,如此就不需要在 companion object中定義了。

如在 Java 中使用 static宣告。

//filej.java
public class FileJ {
public static void load(String fileName){
System.out.println("load: "+fileName);
}
}

🧚🏼‍♀️

無論是在 Java 或是 Kotlin 中,都能夠直接使用 類別名稱.函式名稱 來呼叫該函式:

FileJ.load(“sample.txt”);

期待指數 ⭐️⭐️⭐️⭐️⭐️

Collection Literals — 集合字面值

KT-433871

若打算在 Kotlin 中建立一個列表 (List),我們可以使用 listOf 來產生一個不可修改(Immuttable)的列表,如下:

val myList = listOf(“a”, “b”, “c”)

同樣的,要建立 Map ,可以使用 `mapOf` 來建立一個不可變的 Map:

val myMap = mapOf(“a” to 1, “b” to 2, “c” to 3)

雖然這種方式可以很明確的表示要建立一個 List 或 Map,但是某些時候卻有點多餘,假如我們可以使用更簡潔的方式來宣告一個列表,也就是說不使用 xxxOf,這樣是不是更方便呢?
在 Keynote 中,Roman Elizarov 提到使用中括弧來表示要定義一個列表,而宣告什麼樣的列表就由 Kotlin 自動型別推斷,而這樣子的方式其實已經在許多語言中採用。

如果改成使用中括弧來定義,上面兩個列表的宣告,就可以改成:

val myList = [“a”, “b”, “c”]
val myMap = [“a” to 1, “b” to 2, “c” to 3]

這樣子的宣告,會不會更簡潔呢?

我個人是覺得有,所以我也相當期待這個新功能的推出。但是在 Kotlin 中會使用 xxxOf 其中一個目的是:當我們宣告可變列表時,會使用與其相對的 mutableXXXOf 函式,所以如果採用中括弧來宣告列表時,要怎麼清楚的定義可變列表就是一個問題了(或許不讓你宣告也是一個解法XD)

而在目前的註解中,有部分已經採用中括弧來宣告列表,所以廣義來說,使用中括弧宣告列表已經加入至 Kotlin 當中了,只是目前只侷限在註解的部分,希望能在 Kotlin 2.0 看到這個功能。

期待指數 ⭐️⭐️⭐️⭐️⭐️

Name-Based Destructuring

KT-19627

當我們在 List 中存入元素時,所在意的是其順序而不是其名稱,輸出時可以按照輸入的順序或是經過排列後的順序。
但如果是一個資料類別(data class),我們將元素存進類別時除了可以按照建構子屬性的順序依序將內容塞進類別中,或者可以指定存入的建構子的屬性名稱,而不需要與建構子順序綁在一起。

如下方的 Person資料類別,在其建構子中包含了兩個型別為 String 的屬性 — firstNamelastName

data class Person(
val firstName: String,
val lastName: String
)

如果我們要將 Andy Lu存進 Person 這個資料類別中,我們可以這樣做:

Person(“Andy”, “Lu”) // firstName = Andy lastName = Lu

直接按照建構式的順序來將內容存入,這種方式所要注意的是,當建構式的屬性型別都相同時,存入的資料就必須由呼叫端注意,一不小心就有可能塞錯屬性。如下面示範的,當兩個字串的位置放錯時,資料類別就會存到錯誤的內容。

Person(“Lu”, “Andy”) // firstName = Lu, lastName = Andy 

若要避免塞錯屬性的問題,我們可以在建立時指定屬性名稱,如此就可以避免因為型別相同而將錯誤的內容存入類別內。

Person(firstName=”Andy”, lastName=”Lu”) // firstName= Andy, lastName= Lu
Person(lastName=”Lu”, firstName=”Andy”) // firstName= Andy, lastName= Lu

在資料類別中,我們在意的是資料內容的本身,而不是其資料存入的順序,所以上面兩種使用指定屬性名稱的方式,都可以得到相同的結果。

如上所述,我們可以使用指定屬性名稱的方式將內容正確的傳入建構式中。那麼,我們若要將資料類別的內容取出,有什麼方法可以使用呢?我們可以使用解構式,如下:

val person = Person(firstName=”Andy”, lastName=”Lu”)
val (firstName, lastName) = person

使用解構式,就可以自動將資料類別的內容取出,而不需使用多餘的 getter,如下:

val firstName = person.firstName
val lastName = person.lastName

不過,使用解構式有個問題,那就是其輸出的屬性是依照建構式的順序,而不是其名稱,換句話說,如果我們使用錯誤的變數來接這個內容,那麼還是會得到錯誤的值,如下:

val (lastName, firstName) = person

🧐 這項還在討論中,期待可以很快看到這條的結果。

期待指數 ⭐️⭐️⭐️⭐️

Context Receivers

KT-10468

Context Receiver 可以用來指定函式所能使用的 Context,也就是說只能使用特定的 Context,如下, processRequest 函式包含兩個參數,第一個是 ServiceContext,第二個是 ServiceRequest,而從它的內容來看,呼叫這個函式時,必須要使用這個 Context 帶入 ServiceRequest 的 loadData 函式中。

fun processRequest(
context: ServiceContext,
request: ServiceRequest
){
val data = request.loadData(context)
}

若改使用 Context Receiver 的方式來顯式聲明這個方法實際使用的 Context,如此一來就能更清楚的知道這個函式的使用範圍。

context(ServiceContext)
fun processRequest(request: ServiceRequest){
val data = request.loadData()
}

除了一般的函式外,也可以將 Context Receiver 使用在 Extension Function 上。如下:

context(ServiceContext)
fun ServiceRequest.loadData(): Data

而在這個 YouTracker issue 中,提出可以指定多個 Context Receiver,類似下方的寫法:

context(Context1, Context2)
fun bar() {}

🧐 這條就讓我們靜觀其變吧 XD

期待指數 ⭐️⭐️⭐️⭐️

Explicit Fields (顯式屬性)

KT-14663

在許多 ViewModel 的 best practice 中都告訴我們:只能將不可變的屬性讓 View 觀察(observe)。更新這個屬性的任務就只能交由 ViewModel,不過,因為不可變的屬性是不可以更改其內容的,所以我們需要更新在可變屬性上,而對外的不可變屬性則是讀取這個可變屬性的值。如下:

private val _applicationState = 
MutableStateFlow(State())
val applicationState: StateFlow<State>
get() = _applicationState

Kotlin 希望提供的是一種簡潔的語法,當程式中大量出現這種程式碼的時候,就變成樣板程式碼了,在這條 YouTrack Issue 中,提出了一個解決方法,可以用更少的程式碼來宣告一個對外是不可變的屬性,但是在屬性存在的類別內又有存取權,這種方法目前被命名為 Explicit Fields,如下:

val applicationState: StateFlow<State>
field = MutableStateFlow(State())

這個 field 只有屬性存在的類別才能更新,對外是無法使用的,如此就可以讓對外的部分與內部更新的部分分開。

這樣子是不是更簡單了呢?程式碼從四行直接變成一半的數量,一個私有的可變屬性直接就不需要被建立了。

期待指數 ⭐️⭐️⭐️⭐️⭐️

結論

在這幾版的更新中,有可能是更新時間間距固定的關係,所以並沒有什麼 Breaking Change 在裡頭,所以新功能就會放在 Kotlin 2.0上(畢竟跳了一個大版號),所以在這次的 KotlinConf 的中聽到這些新功能,讓我很期待 Kotlin 2.0。
那麼,你最期待的是哪些功能呢?或者目前的哪些功能還有待加強呢?歡迎留下你的意見,讓我們一起來討論。

如果這篇文章對你有幫助,請拍手👏🏻👏🏻👏🏻鼓勵我。

如內容有問題,請留言告訴我,讓我有改進的空間。

謝謝。🫡

--

--

Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.