Dart語法篇之物件導向繼承和Mixins(六)

mikyou發表於2019-11-14

簡述:

上一篇文章中我們詳細地介紹了Dart中的物件導向的基礎,這一篇文章中我們繼續探索Dart中物件導向的重點和難點(繼承和mixins). mixins(混合)特性是很多語言中都是沒有的。這篇文章主要涉及到Dart中的普通繼承、mixins多繼承的形式(實際上本質並不是真正意義的多繼承)、mixins線性化分析、mixins型別、mixins使用場景等。

一、類的單繼承

1、基本介紹

Dart中的單繼承和其他語言中類似,都是通過使用 extends 關鍵字來宣告。例如

class Student extends Person {//Student類稱為子類或派生類,Person類稱為父類或基類或超類。這一點和Java中是一致的。
    ...
}
複製程式碼

2、繼承中的建構函式

  • 子類中建構函式會預設呼叫父類中無參建構函式(一般為主建構函式)
class Person {
  String name;
  String age;

  Person() {
    print('person');
  }
}

class Student extends Person {
  String classRoom;
  Student() {
    print('Student');
  }
}

main(){
  var student = Student();//構造Student()時會先呼叫父類中無參建構函式,再呼叫子類中無參建構函式
}
複製程式碼

輸出結果:

person
Student

Process finished with exit code 0
複製程式碼
  • 若父類中沒有預設無參的建構函式,則需要顯式呼叫父類的建構函式(可以是命名建構函式也可以主建構函式或其他), 並且在初始化列表的尾部顯式呼叫父類中建構函式, 也即是類建構函式 :後面列表的尾部
class Person {
  String name;
  int age;

  Person(this.name, this.age); //指定了帶引數的建構函式為主建構函式,那麼父類中就沒有預設無參建構函式
  //再宣告兩個命名建構函式
  Person.withName(this.name);

  Person.withAge(this.age);
}

class Student extends Person {
  String classRoom;

  Student(String name, int age) : super(name, age) { //顯式呼叫父類主建構函式
    print('Student');
  }

  Student.withName(String name) : super.withName(name) {} //顯式呼叫父類命名建構函式withName

  Student.withAge(int age) : super.withAge(age) {} //顯式呼叫父類命名建構函式withAge
}

main() {
  var student1 = Student('mikyou', 18);
  var student2 = Student.withName('mikyou');
  var student3 = Student.withAge(18);
}
複製程式碼
  • 父類的建構函式在子類建構函式體開始執行的位置呼叫,如果有初始化列表,初始化列表會在父類建構函式執行之前執行
class Person {
  String name;
  int age;

  Person(this.name, this.age); //指定了帶引數的建構函式為主建構函式,那麼父類中就沒有預設無參建構函式
}

class Student extends Person {
  final String classRoom;

  Student(String name, int age, String room) : classRoom = room, super(name, age) {//注意super(name, age)必須位於初始化列表尾部
    print('Student');
  }
}

main() {
  var student = Student('mikyou', 18, '三年級八班');
}
複製程式碼

二、基於Mixins的多繼承

除了上面和其他語言類似的單繼承外,在Dart中還提供了另一繼承的機制就是基於Mixins的多繼承,但是它不是真正意義上類的多繼承,它始終還是隻能有一個超類(基類)

1、為什麼需要Mixins?

為什麼需要Mixins多繼承?它實際上為了解決單繼承所帶來的問題,我們很多語言中都是採用了單繼承+介面多實現的方式。但是這種方式並不能很好適用於所有場景。

假設一下下面場景,我們把車進行分類,然後下面的顏色條表示各種車輛具有的能力。

Dart語法篇之物件導向繼承和Mixins(六)

我們通過上圖就可以看到,這些車輛都有一個共同的父類 Vehicle,然後它又由兩個抽象的子類: MotorVehicleNonMotorVehicle 。有些類是具有相同的行為和能力,但是有的類又有自己獨有的行為和能力。比如公交車 Bus 和摩托車 Motor 都能使用汽油驅動,但是摩托車 Motor 還能載貨公交車 Bus 卻不可以。

如果僅僅是單繼承模型下無法把部分子類具有相同行為和能力抽象放到基類,因為對於不具有該行為和能力的子類來說是不妥的,所以只能在各自子類另外實現。那麼就問題來了,部分具有相同能力和行為的子類中都要保留一份相同的程式碼實現。這就是產生冗餘,突然覺得單繼承模型有點雞肋,食之無味棄之可惜。

//單繼承模型的普通實現
abstract class Vehicle {}

abstract class MotorVehicle extends Vehicle {}

abstract class NonMotorVehicle extends Vehicle {}

class Motor extends MotorVehicle {
  void petrolDriven() => print("汽油驅動");

  void passengerService() => print('載人');

  void carryCargo() => print('載貨');
}

class Bus extends MotorVehicle {
  void petrolDriven() => print("汽油驅動");

  void electricalDriven() => print("電能驅動");

  void passengerService() => print('載人');
}

class Truck extends MotorVehicle {
  void petrolDriven() => print("汽油驅動");

  void carryCargo() => print('載貨');
}

class Bicycle extends NonMotorVehicle {
  void electricalDriven() => print("電能驅動");

  void passengerService() => print('載人');
}

class Bike extends NonMotorVehicle {
  void passengerService() => print('載人');
}
複製程式碼

可以從上述實現程式碼來看發現,有很多相同冗餘程式碼實現,請注意這裡所說的相同程式碼是連具體實現都相同的。很多人估計想到一個辦法那就是將各個能力提升成介面,然後各自的選擇去實現相應能力。但是我們知道即使抽成了介面,各個實現類中還是需要寫對應的實現程式碼,冗餘還是無法擺脫。不妨我們來試試用介面:

//單繼承+介面多實現
abstract class Vehicle {}

abstract class MotorVehicle extends Vehicle {}

abstract class NonMotorVehicle extends Vehicle {}

//將各自的能力抽成獨立的介面,這樣的好處就是可以從抽象角度對不同的實現類賦予不同介面能力,
// 職責更加清晰,但是這個只是一方面的問題,它還是無法解決相同能力實現程式碼冗餘的問題
abstract class PetrolDriven {
  void petrolDriven();
}

abstract class PassengerService {
  void passengerService();
}

abstract class CargoService {
  void carryCargo();
}

abstract class ElectricalDriven {
  void electricalDriven();
}

//對於Motor賦予了PetrolDriven、PassengerService、CargoService能力
class Motor extends MotorVehicle implements PetrolDriven, PassengerService, CargoService {
  @override
  void carryCargo() => print('載貨');//仍然需要重寫carryCargo

  @override
  void passengerService() => print('載人');//仍然需要重寫passengerService

  @override
  void petrolDriven() => print("汽油驅動");//仍然需要重寫petrolDriven
}

//對於Bus賦予了PetrolDriven、ElectricalDriven、PassengerService能力
class Bus extends MotorVehicle implements PetrolDriven, ElectricalDriven, PassengerService {
  @override
  void electricalDriven() => print("電能驅動");//仍然需要重寫electricalDriven

  @override
  void passengerService() => print('載人');//仍然需要重寫passengerService

  @override
  void petrolDriven() => print("汽油驅動");//仍然需要重寫petrolDriven
}

//對於Truck賦予了PetrolDriven、CargoService能力
class Truck extends MotorVehicle implements PetrolDriven, CargoService {
  @override
  void carryCargo() => print('載貨');//仍然需要重寫carryCargo

  @override
  void petrolDriven() => print("汽油驅動");//仍然需要重寫petrolDriven
}

//對於Bicycle賦予了ElectricalDriven、PassengerService能力
class Bicycle extends NonMotorVehicle implements ElectricalDriven, PassengerService {
  @override
  void electricalDriven() => print("電能驅動");//仍然需要重寫electricalDriven

  @override
  void passengerService() => print('載人');//仍然需要重寫passengerService
}

//對於Bike賦予了PassengerService能力
class Bike extends NonMotorVehicle implements PassengerService {
  @override
  void passengerService() => print('載人');//仍然需要重寫passengerService
}

複製程式碼

針對相同實現程式碼冗餘的問題,使用Mixins就能很好的解決。它能複用類中某個行為的具體實現而不是像介面僅僅從抽象角度規定了實現類具有哪些能力,至於具體實現介面方法都必須重寫,也就意味著即使是相同的實現還得重新寫一遍。一起看下Mixins改寫後程式碼:

//mixins多繼承模型實現
abstract class Vehicle {}

abstract class MotorVehicle extends Vehicle {}

abstract class NonMotorVehicle extends Vehicle {}

//將各自的能力抽成獨立的Mixin類
mixin PetrolDriven {//使用mixin關鍵字代替class宣告一個Mixin類
  void petrolDriven() => print("汽油驅動");
}

mixin PassengerService {//使用mixin關鍵字代替class宣告一個Mixin類
  void passengerService() => print('載人');
}

mixin CargoService {//使用mixin關鍵字代替class宣告一個Mixin類
  void carryCargo() => print('載貨');
}

mixin ElectricalDriven {//使用mixin關鍵字代替class宣告一個Mixin類
  void electricalDriven() => print("電能驅動");
}

class Motor extends MotorVehicle with PetrolDriven, PassengerService, CargoService {}//利用with關鍵字使用mixin類

class Bus extends MotorVehicle with PetrolDriven, ElectricalDriven, PassengerService {}//利用with關鍵字使用mixin類

class Truck extends MotorVehicle with PetrolDriven, CargoService {}//利用with關鍵字使用mixin類

class Bicycle extends NonMotorVehicle with ElectricalDriven, PassengerService {}//利用with關鍵字使用mixin類

class Bike extends NonMotorVehicle with PassengerService {}//利用with關鍵字使用mixin類
複製程式碼

可以對比發現Mixins類能真正地解決相同程式碼冗餘的問題,並能實現很好的複用;所以使用Mixins多繼承模型可以很好地解決單繼承模型所帶來冗餘問題。

2、Mixins是什麼?

用dart官網一句話來概括: Mixins是一種可以在多個類層次結構中複用類程式碼的方式

  • 基本語法

方式一: Mixins類使用關鍵字 mixin 宣告定義

mixin PetrolDriven {//使用mixin關鍵字代替class宣告一個Mixin類
  void petrolDriven() => print("汽油驅動");
}

class Motor extends MotorVehicle with PetrolDriven {//使用with關鍵字來使用mixin類
    ...
}

class Petrol extends PetrolDriven{//編譯異常,注意:mixin類不能被繼承
    ...
}

main() {
    var petrolDriven = PetrolDriven()//編譯異常,注意:mixin類不能例項化
}
複製程式碼

方式二: Dart中的普通類當作Mixins類使用

class PetrolDriven {
    factory PetrolDriven._() => null;//主要是禁止PetrolDriven被繼承以及例項化
    void petrolDriven() => print("汽油驅動");
}

class Motor extends MotorVehicle with PetrolDriven {//普通類也可以作為Mixins類使用
    ...
}
複製程式碼

3、使用Mixins多繼承的場景

那麼問題來了,什麼時候去使用Mixins呢?

當想要在不同的類層次結構中多個類之間共享相同的行為時或者無法合適抽象出部分子類共同的行為到基類中時. 比如說上述例子中在 MotorVehicle (機動車)和 Non-MotorVehicle (非機動車)兩個不同類層次結構中,其中 Bus (公交車)和 Bicycle (電動自行車)都有相同行為 ElectricalDrivenPassengerService. 但是很明顯你無法把這個兩個共同的行為抽象到基類 Vehicle 中,因為這樣的話 Bike (自行車)繼承 Vehicle 會自動帶有一個 ElectricalDriven 行為就比較詭異。所以這種場景下mixins就是一個不錯的選擇可以跨類層次之間複用相同行為的實現

4、Mixins的線性化分析

在說Mixins線性化分析之前,一起先來看個例子

class A {
   void printMsg() => print('A'); 
}
mixin B {
    void printMsg() => print('B'); 
}
mixin C {
    void printMsg() => print('C'); 
}

class BC extends A with B, C {}
class CB extends A with C, B {}

main() {
    var bc = BC();
    bc.printMsg();
    
    var cb = CB();
    cb.printMsg();
}
複製程式碼

不妨考慮下上述例子中應該輸出啥呢?

輸出結果:

C
B

Process finished with exit code 0
複製程式碼

為什麼會是這樣的結果?實際上可以通過線性分析得到輸出結果。理解Mixin線性化分析有一點很重要就是: 在Dart中Mixins多繼承並不是真正意義上的多繼承,實際上還是單繼承;而每次Mixin都是會建立一個新的中間類。並且這個中間類總是在基類的上層

關於上述結論可能有點難以理解,下面通過一張mixins繼承結構圖就能清晰明白了:

Dart語法篇之物件導向繼承和Mixins(六)

通過上圖,我們可以很清楚發現Mixins並不是經典意義上獲得多重繼承的方法。 Mixins是一種抽象和複用一系列操作和狀態的方式,而是生成多箇中間的mixin類(比如生成ABC類,AB類,ACB類,AC類)。它類似於從擴充套件類獲得的複用,但由於它是線性的,因此與單繼承相容

上述mixins程式碼在語義理解上可以轉化成下面形式:

//A with B 會產生AB類混合體,B類中printMsg方法會覆蓋A類中的printMsg方法,那麼AB中間類保留是B類中的printMsg方法
class AB = A with B;
//AB with C 會產生ABC類混合體,C類中printMsg方法會覆蓋AB混合類中的printMsg方法,那麼ABC中間類保留C類中printMsg方法
class ABC = AB with C;
//最終BC類相當於繼承的是ABC類混合體,最後呼叫的方法是ABC中間類保留C類中printMsg方法,最後輸出C
class BC extends ABC {}

//A with C 會產生AC類混合體,C類中printMsg方法會覆蓋A類中的printMsg方法,那麼AC中間類保留是C類中的printMsg方法
class AC = A with C;
//AC with B 會產生ACB類混合體,B類中printMsg方法會覆蓋AC混合類中的printMsg方法,那麼ACB中間類保留B類中printMsg方法
class ACB = AC with B;
//最終CB類相當於繼承的是ACB類混合體,最後呼叫的方法是ACB中間類保留B類中printMsg方法,最後輸出B
class CB extends ACB {}

複製程式碼

5、Mixins中的型別

有了上面的探索,不妨再來考慮一個問題,mixin的例項物件是什麼型別呢? 我們都知道它肯定是它基類的子型別。 一起來看個例子:

class A {
   void printMsg() => print('A'); 
}
mixin B {
    void printMsg() => print('B'); 
}
mixin C {
    void printMsg() => print('C'); 
}

class BC extends A with B, C {}
class CB extends A with C, B {}

main() {
    var bc = BC();
    print(bc is A);
    print(bc is B);
    print(bc is C);
    
    var cb = CB();
    print(cb is A);
    print(cb is B);
    print(cb is C);
}
複製程式碼

輸出結果:

true
true
true
true
true
true

Process finished with exit code 0
複製程式碼

可以看到輸出結果全都是true, 這是為什麼呢?

其實通過上面那張圖就能找到答案,我們知道mixin with後就會生成新的中間類,比如中間類AB、ABC. 同時也會生成 一個新的介面AB、ABC(因為所有Dart類都定義了對應的介面, Dart類是可以直接當做介面實現的)。新的中間類AB繼承基類A以及A類和B類混合的成員(方法和屬性)副本,但它也實現了A介面和B介面。那麼就很容易理解,ABC混合類最後會實現了ABC三個介面,最後BC類繼承ABC類實際上也相當於間接實現了ABC三個介面,所以BC類肯定是ABC的子型別,故所有輸出都是true

其實一般情況mixin中間類和介面都是不能直接引用的,比如這種情況:

class BC extends A with B, C {}//這種情況我們無法直接引用中間AB混合類和介面、ABC混合類和介面
複製程式碼

但是如果這麼寫,就能直接引用中間混合類和介面了:

class A {
  void printMsg() => print('A');
}
mixin B {
  void printMsg() => print('B');
}
mixin C {
  void printMsg() => print('C');
}

class AB = A with B;

class ABC = AB with C;

class D extends AB {}

class E implements AB {
  @override
  void printMsg() {
    // TODO: implement printMsg
  }
}

class F extends ABC {}

class G implements ABC {
  @override
  void printMsg() {
    // TODO: implement printMsg
  }
}

main() {
  var ab = AB();
  print(ab is A);
  print(ab is B);

  var e = E();
  print(e is A);
  print(e is B);
  print(e is AB);

  var abc = ABC();
  print(abc is A);
  print(abc is B);
  print(abc is C);

  var f = F();
  print(f is A);
  print(f is B);
  print(f is C);
  print(f is AB);
  print(f is ABC);
}
複製程式碼

輸出結果:

true
true
true
true
true
true
true
true
true
true
true
true
true

Process finished with exit code 0
複製程式碼

參考資料

總結

到這裡,有關dart中物件導向以及mixins的內容就結束。這篇文章已經對Mixins原理進行了詳細以及全面的分析, 再不用擔心啥時候使用mixins以及用起mixin來心裡沒底。物件導向結束,下一篇文章我們將進入Dart中的型別系統和泛型以及Dart未來版本將要支援的非空和可空型別。

我的公眾號

這裡有最新的Dart、Flutter、Kotlin相關文章以及優秀國外文章翻譯,歡迎關注~~~

Dart語法篇之物件導向繼承和Mixins(六)

Dart系列文章,歡迎檢視:

相關文章