理解 Dart mixin 機制
在 Dart 語言中,我們經常可以看到對 mixin
關鍵字的使用,根據字面理解,就是混合的意思。那麼,mixin
如何使用,它的使用場景是什麼呢。
從一個例項說起
我們假設一個需求,我們需要用多個物件表示一些 動物, 諸如 狗、鳥、魚、青蛙。其中
- 狗會跑
- 鳥會飛
- 魚會游泳
- 青蛙是兩棲動物,會跑,並且會游泳
基於如下一些考慮
- 動物特性可能會繼續增多,並且一個動物可能具備多種技能
- 動物種類很多,但是可以歸大類。例如 鳥禽、哺乳類
我們使用如下設計
- 動物繼承自 Animal 抽象類
- 跑、飛、遊 抽象為介面
程式碼如下:
abstract class Animal {
}
class Run {
run() {
print('run');
}
}
class Fly {
fly() {
print('fly');
}
}
class Swim {
swim(){
print('swim');
}
}
class Bird extends Animal implements Fly {
@override
fly() {
super.fly();
}
}
class Dog extends Animal implements Run {
@override
run() {
super.run();
}
}
class Fish extends Animal implements Swim {
@override
swim() {
super.swim();
}
}
class Frog extends Animal implements Run,Swim {
@override
run() {
super.run();
}
@override
swim() {
super.swim();
}
}
複製程式碼
這個時候,我們會發現編輯器報了個錯
原來這個方法 Dart 會一直認為 super
呼叫是在呼叫一個 abstract 的函式,所以我們這時候需要把這裡面整合的函式實現一一實現。
這時候問題來了,Frog 和 Fish 都實現了 Swim 介面,這時候 swim 函式的內容我們需要重複的寫 2 遍!
回想一下我們當初在 Android 中寫 Java 或者 Kotlin 的時候,其實也有類似問題,同一個 interface 內的 method, 我們可能需要重寫 n 次,非常明顯的程式碼冗餘。
Java8 和 Kotlin 選擇使用介面的 default 實現來解決這個問題:
interface IXX {
default void xmethod() {
/// do sth...
}
}
複製程式碼
而 Dart, 選擇使用 mixin
修改上面的程式碼:
abstract class Animal {
}
mixin Run {
run() {
print('run');
}
}
mixin Fly {
fly() {
print('fly');
}
}
mixin Swim {
swim(){
print('swim');
}
}
class Bird extends Animal with Flym {}
class Dog extends Animal with Run {}
class Fish extends Animal with Swim {}
class Frog extends Animal with Run,Swim {}
複製程式碼
我們執行如下程式碼
Bird bird = Bird();
bird.fly();
Frog frog = Frog();
frog.run();
frog.swim();
複製程式碼
輸出如下:
fly
run
swim
複製程式碼
這裡我們可以意識到,mixin
被混入到了具體的類中,實際也起到了實現具體特性的作用。但是相比實現介面來說,更加的便捷一點。
這裡類的繼承關係我們可以梳理成下圖
當函式一樣的時候
上述的例子結束了 mixin
的基本用法。我們可以看到每個類都可以通過 with
關鍵字,把 mixin
中定義的特性 “混入” 到自己這裡來。但是這時候如果每個 mixin
的函式名是一樣的,會發生什麼呢?我們不妨重新寫一個簡單的例子。
class S {
fun()=>print('A');
}
mixin MA {
fun()=>print('MA');
}
mixin MB {
fun()=>print('MB');
}
class A extends S with MA,MB {}
class B extends S with MB,MA {}
複製程式碼
執行如下程式碼
main() {
A a = A();
a.fun();
B b = B();
b.fun();
}
複製程式碼
我們得到下面這個輸出
MB
MA
複製程式碼
這個時候我們會發現,最後混入的 mixin
的函式,被呼叫了。這說明最後一個混入的 mixins
會覆蓋前面一個 mixins
的特性。為了驗證這個工作流程,我們稍微修改一下這個例子,給 mixins
的函式加上 super 呼叫。
mixin MA on S {
fun() {
super.fun();
print('MA');
}
}
mixin MB on S {
fun() {
super.fun();
print('MB');
}
}
複製程式碼
繼續執行上面的程式,輸出結果如下
A
MA
MB
A
MB
MA
複製程式碼
第一個 A#fun
為例子。我們發現實際的呼叫順序為 MB -> MA -> A,這裡我們可以看出來 mixin
的工作方式,是具有線性化的。
mixin的線性化
上面的示例,我們可以畫一個圖來表示 mixin
是如何線性化的
Dart 中的 mixin
通過建立一個類來實現,該類將 mixin
的實現層疊在一個超類之上以建立一個新類 ,它不是“在超類中”,而是在超類的“頂部”。
我們可以得到以下幾個結論:
mixin
可以實現類似多重繼承的功能,但是實際上和多重繼承又不一樣。多重繼承中相同的函式執行並不會存在 ”父子“ 關係mixin
可以抽象和重用一系列特性mixin
實際上實現了一條繼承鏈
最終我們可以得出一個很重要的結論
宣告 mixin 的順序代表了繼承鏈的繼承順序,宣告在後面的 mixin,一般會最先執行
這裡再提出一個假設,如果 MA 和 MB 都有一個函式叫 log
, 如果在先宣告的 mixin
中執行 log
函式,會發生宣告事情呢?
程式碼如下
mixin MA on S {
fun() {
super.fun();
log();
print('MA');
}
log() {
print('log MA');
}
}
mixin MB on S {
fun() {
super.fun();
print('MB');
}
log() {
print('log MB');
}
}
class A extends S with MA,MB {}
A a = A();
a.fun();
複製程式碼
這裡按照習慣性的思維,我們可能會得到
A
log MA
MA
MB
複製程式碼
的結果。實際上,我們的輸出是
A
log MB
MA
MB
複製程式碼
仔細思考一下,按照上面的工作原理,在 mixin
的繼承鏈建立的時候,最後宣告的 mixin
會把後宣告的 minxin
的函式覆蓋掉。這時候即使我們從程式碼邏輯中認為在 MA 中呼叫了 log
函式,實際上這時候 A 類中的 log
函式已經被 MB 給覆蓋了。所以最終,log
函式呼叫的是 MB 中的 log
函式邏輯。
型別
根據 mixin
的工作原理,我們完全可以大膽猜想,最終的子類型別和這個繼承鏈上所有父類和混入的 mixin
的型別都可以匹配上。我們來驗證一下這個猜想:
A a = A();
print(a is A);
print(a is S);
print(a is MA);
print(a is MB);
複製程式碼
輸出結果
true
true
true
true
複製程式碼
推論完全正確。
mixin 的使用場景
我們應該在什麼時候使用 mixin
呢?很簡單,在我們編寫 Java 的時候,感覺需要實現多個 interface
的時候。
那麼,這個和多重繼承相比,在某些場景有什麼好處嗎?答案是有。
在 Flutter 中,framework 的執行依賴多個 Binding
,我們檢視最外層 WidgetsFlutterBinding
的定義:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {}
複製程式碼
在 WidgetsBinding
和 RendererBinding
中,都有一個叫做 drawFrame
的函式。在 WidgetsBinding
的 drawFrame
中,也有 super.drawFrame()
的呼叫。
這裡 mixin
的優點就體現了出來,我們可以看到這個邏輯有如下2點
- 保證了 widget 等的
drawFrame
先於 render 層的呼叫,保證了 Flutter 在佈局和渲染處理中 widgets -> render 的處理順序 - 保證順序的同時,Widgets 和 Render 仍然屬於 2 個不同的物件定義,職責分割的非常的清晰。
具體的細節,感興趣的同學可以閱讀 Flutter 的 flutter package 的原始碼。
小結
這篇文,我對 Dart 的 mixin
的使用、工作機制、使用場景做了一個大致的總結。mixin
是一個強大的概念,我們可以跨越類的層次結構重用程式碼。
文中一些優勢和工作機制是我的個人理解。在初次接觸 Dart 的這個機制的時候,也需要很多的思維轉變。如果文中我有理解的不對的地方,或者您有不同的理解。歡迎評論討論交流。