【Flutter】一文讀懂混入類Mixin

2023-11-15 06:01:31

【Flutter】一文讀懂混入類Mixin

基本介紹

Mixin是一種有利於程式碼複用,又避免了多繼承的解決方案。

Mixin 是物件導向程式設計語言中的類,提供了方法的實現,其他類可以存取 Mixin 類的方法而不必成為其子類;Mixin 為使用它的 Class 類提供額外的功能,但自身卻不單獨使用(不能單獨生成範例物件,屬於抽象類),Mixin 類通常作為功能模組使用,在需要該功能時「混入」,而且不會使類的關係變得複雜;
Mixin 有利於程式碼複用性同時又避免了多繼承的複雜性,使用 Mixin 享有單一繼承的單純性和多重繼承的共有性,interface 介面與 Mixin 相同的地方是都可以多繼承,不同的地方在於 Mixin 是可以實現的;

對應關係

繼承 混入 介面
關鍵字 extends with implements
對應數量 1:1 1:n 1:n
程式碼設定順序
耦合度

舉例學習

首先,眾所周知...Java只能單繼承,

假如我們面臨下面這一種需求:

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

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

基於如下一些考慮

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

我們使用如下設計

  • 動物繼承自 Animal 抽象類
  • 跑、飛、遊 抽象為介面

我們按照上面的需求...讓copilotX幫我寫一個類的實現...

可以看到AI生成的程式碼還是很給力的,但是我們可以發現,Frog和Dog都實現了Run的抽象方法。

假如我們現在嘗試讓程式碼複用率變高,讓Run,Fly,Swim作為實現,看看會發生什麼...

可以看到,我們的Copilit告訴了我們問題

原來這個寫法 Dart 會一直認為 super 呼叫是在呼叫一個 abstract 的函數,所以我們這時候需要把這裡面整合的函數實現一一實現。

這時候問題來了,Frog 和 Fish 都實現了 Swim 介面,這時候 swim 函數的內容我們需要重複的寫 2 遍!

(當然我們指的就是前面AI生成的程式碼)

當然,作為一篇Mixin教學,我們對這個結果肯定是不滿意的...

現在,我們完全沒學過類似Java的default關鍵字的知識點...我們只是個渴望dart的小白...

選擇使用mixin,重新定義Run,Fly,Swim方法,子類也不再是實現介面而是混入。

可以看到,mixin被混入到了類中,也實現了對應「抽象類」的特性。

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

這裡也可以增加一個新的理解:mixin並不是對子類的拓展,而是對父類別的拓展

mixin,class,interface的異同

mixin也可以使用class關鍵字定義,也可以當做普通class一樣使用。
mixin可以使用with定義,這樣定義的mixin就只能通過with關鍵字參照了。

Dart是沒有interface這種東西的,但並不意味著這門語言沒有介面,事實上,Dart任何一個類都是介面,你可以實現任何一個類,只需要重寫那個類裡面的所有具體方法。

所以,Dart中的任何一個class,既是類,又是介面,也可以當作mixin使用

這意味著:

  • 混入類可以持有成員變數,也可以宣告和實現成員方法。而混入一個類,就可以存取其中的成員屬性和方法,這點和繼承很像

  • 一個類可以混入若干個類,通過,分隔開,這個功能和介面類似,但是和介面不同的是:混入類本身可以對方法進行實現,而介面內必須是抽象方法

  • 混入類支援抽象方法,但是這要求了派生類必須實現抽象方法,這一點又和抽象類很像。

    mixin PaintAble{
      late Paint painter;
      void paint(){
        print("=====$runtimeType paint====");
      }
      void init();
    }
    
    class Shape with MoveAble,PaintAble{
      @override
      void init() {
        painter = Paint();
      }
    }
    // 這裡的Shape作為派生類,必須實現PaintAble中宣告的抽象方法init
    

mixin的限制

可以看到,在混入了之後,就可以使用mixin的所有方法了,但是有時我們並不希望所有類都可以使用一些方法。比如我在Dog類中with一個Fly,這就意味著我們的狗可以飛了!

所以...為了守護自然界的秩序,mixin提供了一種限制:on 關鍵字

規定了:on後面銜接的類和它的子類才可以被混入

除此之外,on還可以限定mixin之間的繼承關係,參考下一小節

mixin Fly on Bird{
    void fly(){
        print('只有鳥類可以混入Fly')
    }
}

除了類的限制外,mixin本身就是一種限制。

因為剛剛提到,dart中的任何一個類都可以被混入,而使用mixin宣告的類,需要使用with關鍵字才可以替換。

除此之外的一點小改動...

細心的你可能會發現,在我們的樣例中直接這樣修改是沒辦法通過編譯的。這是因為上面那句話:

mixin並不是對子類的拓展,而是對父類別的拓展,也就是說,我們在程式碼中,相當於將Animal拓展了一個Fly功能,而我們規定了,Fly方法只能被Bird及Bird的子類使用。Animal並不屬於Bird的子類(反倒是他的父類別),所以會報錯。

繼承的二義性問題

先說說什麼是二義性問題:

(內容參考如下文章:C++多繼承中的二義性問題_繼承的二義性-CSDN部落格

在C++中,派生類繼承基礎類別,對基礎類別成員的存取應該是確定的、唯一的,但是常常會有以下情況導致存取不一致,產生二義性。

1.在繼承時,基礎類別之間、或基礎類別與派生類之間發生成員同名時,將出現對成員存取的不確定性——同名二義性。

2.當派生類從多個基礎類別派生,而這些基礎類別又從同一個基礎類別派生,則在存取此共同基礎類別中的成員時,將產生另一種不確定性——路徑二義性。

而在介面中,犧牲了介面的普通成員方法實現,最終才解決二義性問題,最終能夠支援多實現。

混入類中,不能擁有構造方法,也就是說不能範例化。這一點跟抽象類介面是一樣的。

看如下的範例:

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,會覆蓋前面的mixin的特性

為了驗證這個結論,我們給mixin加入super呼叫和mixin的繼承關係

mixin MA on S {
  fun() {
    super.fun();
    print('MA');
  }
}
mixin MB on S {
  fun() {
    super.fun();
    print('MB');
  }
}

執行程式碼,得到如下結果:

A

MA

MB

A

MB

MA

這裡我們得到mixin的工作方式:線性化

Mixin的線性化

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

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

  1. mixin 可以實現類似多重繼承的功能,但是實際上和多重繼承又不一樣。多重繼承中相同的函數執行並不會存在 」父子「 關係
  2. mixin 可以抽象和重用一系列特性
  3. mixin 實際上實現了一條繼承鏈
  4. A is S,A is MA,A is MB。

最終我們可以得出一個很重要的結論

宣告 mixin 的順序代表了繼承鏈的繼承順序,宣告在後面的 mixin,一般會最先執行

線性化的覆蓋範例

參考如下程式碼

class S {
  fun()=>print('A');
}
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會把前面宣告的mixin函數覆蓋掉,所以即使我們此時在MA函數中呼叫了log,而事實上MA裡面的log函數被MB覆蓋了,最後呼叫的是MB。

小結論:呼叫了super就可以從前往後看執行順序,如果存在函數內同名呼叫函數的情況要從後往前看

混入類之間的繼承關係

另外,兩個混入類間可以通過 on 關鍵字產生類似於 繼承 的關係

mixin A{
    int i = 5;
}
mixin B on A{
    int j = 6;
    void show(){
        print(i);
        print(j);
    }
}
class C with A,B{

}
main(){
    C c = new C();
    c.show();
}

可以看到,B中可以通過on A來存取A內的成員變數。

同時C with A,B不可以調換順序,否則編譯器會報錯。這也符合我們之前說的線性關係,因為「B繼承A」,所以,只有「B覆蓋了A」這種線性關係才是可以被接受的。

extends,mixin,implements的執行順序

class Ex{
  Ex(){
    print('extends constructor');
  }
  void show(){
    print('extends show');
  }
}

// dart 沒有 interface 關鍵字,但是可以使用 abstract class 來實現介面的功能
abstract class It{
  void show();
}


mixin mx1 on Ex{
  void show(){
    super.show();
    print('mx1show');
  }
}

mixin mx2 on Ex{
  void show(){
    super.show();
    print('mx2show');
  }
}

class C12 extends Ex with mx1,mx2 implements It{
  @override
  void show() {
    super.show();
    print('it show');
  }
}

class C21 extends Ex with mx2,mx1 implements It{
  @override
  void show() {
    super.show();
    print('it show');
  }
}

void main(){
  C12 c12 = new C12();
  c12.show();
  C21 c21 = new C21();
  c21.show();
}

執行結果:

extends constructor

extends show

mx1show

mx2show

it show

extends constructor

extends show

mx2show

mx1show

it show

結論:執行順序是 extends 繼承優先執行,之後是 with 混入,最後是 implements 介面過載;

Flutter的runAPP

接下來我們回到Flutter,看一下runAPP()的形式

WidgetsFlutterBinding.ensureInitialized 方法如下:

WidgetsFlutterBinding 混合結構如下:

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

BindingBase 及建構函式如下:

其執行了 initInstances 和 initServiceExtensions 方法。看下面混合的順序:

從後到前依次執行其 initInstances 和 initServiceExtensions(如果有) 方法,由於 initInstances 和 initServiceExtensions 方法中首先執行 super.initInstances()super.initServiceExtensions() ,所以最後執行的順序為:BindingBase -> GestureBinding -> SchedulerBinding -> ServicesBinding -> PaintingBinding -> SemanticsBinding -> RendererBindinsg -> WidgetsBinding 。

而在WidgetsBinding和RendererBinding中,都有一個叫做drawFrame的函數,而Widget的drawFrame呼叫了super.drawFrame,同時Widgets on Renderer

這裡反應的邏輯有如下兩點:

  • 保證widget等的drawFrame能夠先於render呼叫。保證了flutter在佈局和渲染處理時 widgets->render
  • 保證了順序的同時,兩者仍然各個負責自己的部分

參考文章

Flutter 語法進階 | 深入理解混入類 mixin - 掘金 (juejin.cn)

徹底理解 Dart mixin 機制 - 掘金 (juejin.cn)

Flutter 必知必會系列 —— mixin 和 BindingBase 的巧妙配合 - 掘金 (juejin.cn)

【Flutter 專題】103 初識 Flutter Mixin - 掘金 (juejin.cn)

跟我學flutter:我們來舉個例子通俗易懂講解dart 中的 mixin - 掘金 (juejin.cn)

Flutter 中不得不會的 mixin - 老孟Flutter - 部落格園 (cnblogs.com)

深入理解 Dart mixin 機制 - 知乎 (zhihu.com)

C++多繼承中的二義性問題_繼承的二義性-CSDN部落格

Flutter 必知必會系列 —— runApp 做了啥 - 掘金 (juejin.cn)