嘿,把你的委派委派給 Kotlin 吧

Connie Lin
11 min readJan 11, 2021
本文譯自 Murat Yener 於 2020 年 10 月 8 日在 Android Developers 社群上發表的 Delegating Delegates to Kotlin ,以下將介紹使用 Kotlin 進行類別委派與屬性委派的方法,以及在底層的運作方式。如有任何問題或勘誤,歡迎在底下留言告知。

▍Kotlin Vocabulary: Delegates

你知道嗎?有一種做事的方法,就是把任務交付給其他人完成 ⋯嗯、不是指甩鍋給朋友啦。我是指我們可以把某個物件要做的事情,委派給另外一個物件去完成。

委派(Delegation)這概念並不新鮮了。委派是一種軟體設計模式,可以讓物件將外部需求轉交給另一個物件,而代為處理需求的受委派方,會負責將結果提供給委派方。

Kotlin 支援 類別(class) 與 屬性(property) 的委派,並提供一些原生方法,使委派設計模式的運用更加容易上手。

善用 Kotlin Delegation ,把委派甩鍋給 Kotlin 吧。

▍類別委派 (Class Delegates)

假設有個需求是要讓 ArrayList 可以復原最後一個移除的 item (以下簡稱 trash),那麼只要讓 ArrayList 可以引用到 trash 就可以達成這個需求了。而實際上,要怎麼做到這件事呢?

有一種做法是去擴充 ArrayList 的類別。但這麼做是去擴充 ArrayList 的實體而非實作 MutableList 的介面,會與實體 ArrayList 的實作產生耦合。而要是可以直接去修改 MutableList.remove() 的方法,加入與最後刪除項目的 reference,並且將剩下有關 MutableList 的空實作都委派給其他物件,這樣會乾淨許多。

Kotlin 讓我們可以把其餘任務委派給內部產生的 ArrayList 實體,讓你客製你想要的行為。要做到第二種做法,可以使用強大的 by

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class ListWithTrash <T>(
private val innerList: MutableList<T> = ArrayList<T>()
) : MutableCollection<T> by innerList {
var deletedItem : T? = null override fun remove(element: T): Boolean {
deletedItem = element
return innerList.remove(element)
}
fun recover(): T? {
return deletedItem
}
}

innerList 是接受 MutableList 介面實作委派的內部 ArrayList 實體 。藉由 by 這個方法,Kotlin 會產生 delegation code 並讓 innerList 作為被委派方,藉此將其餘的介面實作直接轉介給 innerList,使得 ListWithTrash 能夠完整實作 MutableList 介面的功能。這麼一來就可以輕易地客製化預期的行為了。

▍Under the hood

來看看底層是如何運作的。如果你看過由 ListWithTrash byte code 反編譯的 Java code,會發現 Kotlin 編譯器實際上創建了包裝函式(wrapper functions),來呼叫內部 ArrayList 物件的相應函式。

public final class ListWithTrash implements Collection, KMutableCollection {
@Nullable
private Object deletedItem;
private final List innerList;
@Nullable
public final Object getDeletedItem() {
return this.deletedItem;
}
public final void setDeletedItem(@Nullable Object var1) {
this.deletedItem = var1;
}
public boolean remove(Object element) {
this.deletedItem = element;
return this.innerList.remove(element);
}
@Nullable
public final Object recover() {
return this.deletedItem;
}
public ListWithTrash() {
this((List)null, 1, (DefaultConstructorMarker)null);
}
public int getSize() {
return this.innerList.size();
}
// $FF: bridge method
public final int size() {
return this.getSize();
}
//...and so on
}

註:Kotlin 編譯器是採用裝飾者模式(Decorator Pattern)來進行類別委派。裝飾者類別 會與 被裝飾者類別 共享同個介面。裝飾者類別 持有目標類別的引用,並且包裝(或裝飾)該介面的所有公開方法。

當你無法繼承某些特定類別的時候,改採委派就是很好的方法。藉由類別委派,你的類別不會隸屬於其他的類別層次,但卻可以共享相同的介面,並且修飾內部物件的原生方法。這意味著你不需要冒著破壞公開 API 的風險,就可以簡單地修改實作。

▍屬性委派(Delegating properties)

除了委派類別, by 也可以用來委派屬性。受委派方會負責處理該屬性的 getset 的方法,非常適合不同物件需要重用 getter 與 setter 邏輯的情況,讓你可以簡易擴充功能而不會使 backing field 太過複雜。

來看看這樣的使用情境:

class Person(var name: String, var lastname: String)

假設我們今天有個有關於 Person 的類別,想要對於 name 的格式有些限制,我們希望在 set name 的時候去確保除了英文字首是大寫之外,其餘字母都要是小寫。此外,當 name 被更新的時候,也要自動增加修改次數的計數。

你可以這樣寫:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class Person(name: String, var lastName: String) {
var name: String = name
set(value) {
name = value.toLowerCase().capitalize()
updateCount++
}
var updateCount = 0
}

這樣寫也沒什麼錯,但若需求一改,也得在 lastName 變化時更新 updateCount 又該怎麼辦呢?雖然也可以將邏輯複製到 lastName 的 setter,但這樣 namelastName 的 setter 基本上是做一樣的事情,就不該重複程式碼。

這種情況,轉而透過屬性委派將 getter 與 setter 委派給另一方,就可以解決了。如同委派類別,你也可以使用 by 來讓 Kotlin 幫你委派屬性:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class Person(name: String, lastname: String) {
var name: String by FormatDelegate()
var lastname: String by FormatDelegate()
var updateCount = 0
}

現在已經把 namelastname 這兩個屬性委派給 FormatDelegate 類別了,那就來看看 FormatDelegate 是怎麼寫的:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class FormatDelegate : ReadWriteProperty<Any?, String> {
private var formattedString: String = ""
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
return formattedString
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
formattedString = value.toLowerCase().capitalize()
}
}

如果你只想委派 getter ,需要去實作 ReadProperty<Any?, String>。在我們的情況是需要委派 getter 與 setter ,因此實作了 ReadWriteProperty<Any?, String>

你可能注意到了,getter 和 setter 方法裡面有兩個額外的參數,thisRefKProperty<*>thisRef 代表擁有屬性的物件,目的在需要檢查其他屬性或是呼叫其他類別的方法時,可以用來訪問物件本身。KProperty<*> 則用來訪問受委派方的屬性的 metadata 。回到需求,我們使用 thisRef 來訪問 updateCount,並且在這邊來增加它的數值。

▍Under the hood

來看看反編譯的 Java code 。Kotlin 編譯器讓 namelastName 個別引用 FormatDelegate 物件,便能訪問到 getter/setter 的邏輯。編譯器產生 KProperty[] 來儲存受委託的屬性,你可以發現 index 0 和 1 分別為 namelastname 所產生的 getter 和 setter。

public final class Person {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Person.class), "lastname", "getlastname()Ljava/lang/String;"))};
@NotNull
private final FormatDelegate name$delegate;
@NotNull
private final FormatDelegate lastname$delegate;
private int updateCount;
@NotNull
public final String getName() {
return this.name$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
}
//...
}

這麼一來,任何的呼叫者透過一般屬性語法,就可以使用到受委派的屬性了。

person.lastname = “Smith” // calls generated setter, increments countprintln(“Update count is $person.count”)

Kotlin 不僅支援委派,還提供原生委派方法,未來 我們 會在其他文章中進一步分享。

委派可以讓你將任務交給其他物件,減少重複程式碼。而透過 by 這個關鍵字來委派類別或是屬性,Kotlin 編譯器會在底層產生所需的程式碼,幫助你無縫地完成委派,也完全不會有影響公開 API 的風險。

簡單地說,Kotlin 官方會負責維護委派的程式碼,你只要把你的委派任務委派給 Kotlin 就好了

--

--