Kotlin開發筆記:使用委托進行拓展
導言
在OO語言(面向對象)中,我們經常會用到委托或者代理的思想。委托和代理在乍一看很相似,其實其各有各的側重點,這里我引用ChatGpt的回答:
委托(Delegation)和代理(Proxy)雖然有相似之處,但在面向對象編程中有一些區別。
- 職責分配:
委托:委托是一種將對象的一部分職責轉交給另一個對象來處理的方式。原始對象將某些任務委托給另一個對象,但是仍然保持對委托對象的控制。
代理:代理是一種通過提供一個代替對象來控制訪問。代理對象通常具有與被代理對象相同的接口,客戶端代碼可以通過代理對象來間接訪問被代理對象。
目的:- 委托:委托用于實現代碼的模塊化和職責分離,將一部分功能委托給其他對象處理,以達到更好的代碼組織和可維護性。
- 代理:代理用于控制訪問,可以用于實現懶加載、安全性控制、遠程訪問等。代理對象可以在客戶端和被代理對象之間添加額外的邏輯,如緩存、權限驗證等。
關系類型:
- 委托:委托通常涉及到兩個具體的對象,一個是原始對象,另一個是被委托的對象,它們可以屬于不同的類。
- 代理:代理通常有三個主要組成部分:客戶端、代理對象和被代理對象。代理對象扮演中間人的角色,控制客戶端訪問被代理對象。
雖然委托和代理在某些情況下可能會有重疊,但它們的重點和使用方式是不同的。
委托更關注職責分離和模塊化,而代理更關注控制訪問和添加額外的邏輯。在實際編程中,選擇使用委托還是代理取決于具體的需求和設計目標。
通過前面的介紹我們應該對代理和委托的概念和區別有了一定的認識。不過本篇文章并不是來探討委托和代理之間的關系,而是來簡單介紹Kotlin中的委托語法相關知識的。
使用Kotlin的by來進行委托
在Java中并沒有專門的語法來幫助我們實現委托,而Kotlin中則十分貼心地提供了by關鍵字,通過這個關鍵字我們可以要求編譯器生成粗略的代碼來幫助我們實現委托。接下來給出一個最簡單的例子來介紹by關鍵字的用法。
首先我們先定義一個Worker接口來定義打工人的職責:
interface Worker {fun work()fun takeVacation()
}
該接口有work和takeVacation(不存在的)兩個方法。接下來定義兩個類來實現這個接口:
class JavaProgramer : Worker{override fun work() {println("... write JavaCode ...")}override fun takeVacation() {println("JavaProgramer relax")}
}class CSharpProgramer : Worker{override fun work() {println("...writer CSharpCode ...")}override fun takeVacation() {println("CSharpProgramer relax")}
}
最后,我們還想要定義一個Manager類來管理所有的Worker,這種情況下我們就可以使用到Kotlin中的by關鍵字進行委托:
class Manager():Worker by JavaProgramer()//委托語法
是的,只需要這一行Manager就實現了委托,這種情況下我們調用Manager來調用方法最終就會路由到JavaProgramer的一個默認生成的實例中運行:
fun main() {val del = Manager()del.work()
}
最后的結果就是:
其實這樣說可能不太清楚,我們來仔細分析一下class Manager():Worker by JavaProgramer()這行代碼,首先類名后面用冒號跟上Worker接口的意思正是Manager類需要實現Worker接口,后面的JavaProgramer()代碼就會自動生成一個JavaProgramer的實例,最后通過by連接,意思就是Manager類將會委托后面生成的這個JavaProgramer實例來實現Worker接口。
上面例子的局限
上面的這個例子的局限也十分明顯,那就是由于委托的JavaProgramer實例是隱性生成的,所以我們就丟失了對委托的引用,這種情況下我們在這個Manager類就無法再次委托隱式生成的JavaProgramer來進行一些操作了。
委托給一個參數
上面我們提出了上面例子的局限性,不過只要理解了我們在上面分析的那一行代碼的語法,我們可以很簡單地避免上面的局限性。很顯然要解決這個問題需要我們保留對被委托方的引用:
class Manager2(val mWorker: Worker):Worker by mWorker{fun fun1(){mWorker.work()}
}
在這段代碼中我們保留了對被委托方的引用,通過幕后生成的mWorker字段存儲了被委托方,后面的by mWorker一句表明這個Manager2類將會委托mWorker字段來實現Worker接口。
不要用var來修飾持有委托的字段
上面的代碼中我們用val變量持有了傳入的委托實例,當然編譯器也是允許我們使用var變量來持有委托實例的,不過這樣做存在風險。具體來說class Manager2(val mWorker: Worker):Worker by mWorker 實際上存儲了兩個對mWorker的引用,一個就是通過val生成的幕后字段,還有一個就是通過by生成的包裝類中持有的委托引用。當我們用val修飾時不會有什么問題,但是如果當我們用var修飾就會存在隱患。
比如我們這樣寫:
class Manager2(var mWorker: Worker):Worker by mWorker{fun change(){if(mWorker is JavaProgramer){mWorker = CSharpProgramer()}else{mWorker = JavaProgramer()}}fun showWorker(){println(mWorker.javaClass.simpleName)}
}
里面定義的change方法將會更改成員變量中的mWorker但是無法修改by語句的委托實例,我們運行一下這段代碼查看結果:
fun main() {val del = Manager2(JavaProgramer())del.work()del.takeVacation()del.showWorker()del.change()del.work()del.takeVacation()del.showWorker()
}
最后結果為:
這里雖然成員變量中持有的Worker發生了變化,但是委托的Worker依舊是一開始創建的Worker,它無法被修改,這樣就會造成語義的不清晰,所以說我們盡量不要用var變量來持有被委托的實例。
取消部分委托
當我們的委托方類沒有和接口中的方法名一致的方法時,將不會有什么大問題,但是如果委托方中有一個方法實現了接口中的一些方法時就會和委托產生沖突。換句話說,如果我們只想要委托給一個實例實現接口中的部分方法時就需要處理掉一些沖突。比如我們在之前的例子中進行修改,如果Manager的代碼如下就會產生沖突:
class Manager():Worker by JavaProgramer(){fun takeVacation(){ println("Manager Relax...")}
}//委托語法
這種情況下被委托方中的takeVacation方法就和Manager類中的takeVacation方法有了沖突,編譯器無法決定是該調用被委托方的方法還是Manager中的方法。這時就需要在我們不需要進行代理的方法前加上override修飾符,如下:
class Manager():Worker by JavaProgramer(){override fun takeVacation(){ //解決方法沖突--取消委托println("Manager Relax...")}
}//委托語法
這種語法我們可以理解為單個方法取消委托,也可以理解為Manager類在實現接口的方法。總而言之,我們這樣表達后沖突就不復存在了,當我們調用Manager類的takeVacation方法時就會調用Manager自身的方法而不是進行委托。
實現多個委托
上面的情況我們介紹的都是一個類委托給一個類實現,實際上一個類也可以委托給多個類實現多個接口,不過這種情況下可能會產生一些沖突需要我們手動處理。接下來我們修改Worker接口并且新增一個Assistant接口:
interface Worker {fun work()fun takeVacation()fun FishingTime()
}interface Assistant{fun doChores()fun FishingTime()
}
這樣我們這兩個接口就會有一個重疊的方法,現在我們創建一個類來實現多個委托:
class Manager3(val mWorker: Worker,val mAssistant: Assistant):Worker by mWorker,
Assistant by mAssistant{}
這樣寫將會產生報錯,因為這兩個接口有一個重疊的方法,用兩個委托的話編譯器將無法確定Manager3需要委托哪一個類來實現FishingTime方法,這里解決沖突的方法就是使用取消委托的方式,用override進行修飾決定到底需要哪一個委托:
class Manager3(val mWorker: Worker,val mAssistant: Assistant):Worker by mWorker,
Assistant by mAssistant{override fun FishingTime() {mWorker.FishingTime()mAssistant.FishingTime()}
}
這樣就解決了沖突,同時實現了一個委托類委托給多個類實現的效果。
內置的標準委托
Lazy委托
在這里Lazy委托也可以被理解為懶加載,即只有在真正需要時才對一個函數進行調用,以達到節省開銷延時加載計算的效果。比如說在布爾邏輯表達式中現在的大部分語言都有短路求值的特性,如果在表達式之前對表達式的求值足以產生結果,則跳過表達式的執行。比如:
fun workwork():String{println("execute")return "work work"
}fun main() {val msg = workwork()var shouldWork = falseif(shouldWork && msg != null){println("work day")}else{println("relax")}
}
在這種情況下顯然就違反了短路原則,產生了額外的開銷,運行結果是:
雖然msg未被使用,但是還是因為msg的賦值語句產生了額外的開銷,這顯然不是我們想要的。我們當然可以將賦值移到&&運算符之后,不過kotlin針對這種情況提供了lazy委托,讓我們對上面的例子進行修改:
fun main() {val msg by lazy { workwork() } var shouldWork = falseif(shouldWork && msg != null){println("work day")}else{println("relax")}
}
這里我們用lazy委托包裝了workwork函數,現在再來看運行結果:
額外的開銷消失了,很神奇。這就是Kotlin中的lazy委托。Lazy委托后面接收一個lambda表達式,這樣在我們需要用到委托方(在這里即為msg變量)時才會執行,否則將不會被執行,也就是說它是按需執行的。一旦對lambda中表達式求值,委托將記住結果,以后對該值的請求將接受保存的值而不是重新計算lambda表達式。
在默認情況下,lazy函數同步lambda表達式的運行,因此最多只有一個線程運行它。另外,Kotlin中的lazy委托只能用于val(不可變)變量,而不能用于var(可變)變量。這是因為lazy委托的特性與惰性求值相關,適用于只需要初始化一次并且后續不會再變化的情況。
Observable委托
接下來介紹的是Observable委托,看名字就知道這個委托和觀察者模式密不可分。實際上也是這樣。Observable委托將對關聯的變量或者屬性的修改進行攔截,發生修改時委托將調用我們用observable函數注冊的事件處理程序上。
事件處理程序將接受三個類型為KProperty的參數,這些參數保存關于屬性,舊值和新值的元數據,但是不返回任何值。我們直接用例子來說明:
fun main() {var count by observable(0){property, oldValue, newValue ->println("參數是:$property,舊值是:$oldValue,新值是:$newValue")}count++count++count++
}
這里我們用observable委托將count參數給委托了,observable括號中的0代表的是初始值,也就是count一開始為0,而當我們對count進行修改時就會觸發后面的lambda表達式,我們來看看運行結果:
顯然我們對count進行修改時就觸發了這段lambda表達式,達到了觀察的效果,感覺和JetPack中的LiveData也很相似。
vetoable委托
接下來介紹的是vetoable委托,和observable委托不同的是vetoable將返回一個Boolean類的值,如果返回true代表同意修改,否則就是拒絕修改。一旦拒絕修改,被委托的變量也就將停止修改,比如說:
fun main() {var count by vetoable(0){property, oldValue, newValue ->println("參數是:$property,舊值是:$oldValue,新值是:$newValue")oldValue < newValue}count++count--count--
}
這里我們對上面的例子稍作修改,lambda表達式最后一行的oldValue < newValue就是vetoable委托的最后返回值,當新值大于舊值時才同意修改,所以可以預見的是count–將不會生效,我們來看運行結果:
可以看到,后面兩次修改果然沒有生效。