理解 Dart mixin 機制

e網通大前端專欄發表於2019-06-02

在 Dart 語言中,我們經常可以看到對 mixin 關鍵字的使用,根據字面理解,就是混合的意思。那麼,mixin 如何使用,它的使用場景是什麼呢。

從一個例項說起

我們假設一個需求,我們需要用多個物件表示一些 動物, 諸如 狗、鳥、魚、青蛙。其中

  1. 狗會跑
  2. 鳥會飛
  3. 魚會游泳
  4. 青蛙是兩棲動物,會跑,並且會游泳

基於如下一些考慮

  • 動物特性可能會繼續增多,並且一個動物可能具備多種技能
  • 動物種類很多,但是可以歸大類。例如 鳥禽、哺乳類

我們使用如下設計

  • 動物繼承自 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 mixin 機制

原來這個方法 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 被混入到了具體的類中,實際也起到了實現具體特性的作用。但是相比實現介面來說,更加的便捷一點。

這裡類的繼承關係我們可以梳理成下圖

理解 Dart 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 機制

Dart 中的 mixin 通過建立一個類來實現,該類將 mixin的實現層疊在一個超類之上以建立一個新類 ,它不是“在超類中”,而是在超類的“頂部”。

我們可以得到以下幾個結論:

  1. mixin 可以實現類似多重繼承的功能,但是實際上和多重繼承又不一樣。多重繼承中相同的函式執行並不會存在 ”父子“ 關係
  2. mixin 可以抽象和重用一系列特性
  3. 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 {}
複製程式碼

WidgetsBindingRendererBinding 中,都有一個叫做 drawFrame 的函式。在 WidgetsBindingdrawFrame中,也有 super.drawFrame() 的呼叫。

這裡 mixin 的優點就體現了出來,我們可以看到這個邏輯有如下2點

  1. 保證了 widget 等的 drawFrame 先於 render 層的呼叫,保證了 Flutter 在佈局和渲染處理中 widgets -> render 的處理順序
  2. 保證順序的同時,Widgets 和 Render 仍然屬於 2 個不同的物件定義,職責分割的非常的清晰。

具體的細節,感興趣的同學可以閱讀 Flutter 的 flutter package 的原始碼。

小結

這篇文,我對 Dart 的 mixin 的使用、工作機制、使用場景做了一個大致的總結。mixin 是一個強大的概念,我們可以跨越類的層次結構重用程式碼。

文中一些優勢和工作機制是我的個人理解。在初次接觸 Dart 的這個機制的時候,也需要很多的思維轉變。如果文中我有理解的不對的地方,或者您有不同的理解。歡迎評論討論交流。


相關文章