深入解析 Dart 語言:命名慣例、類特性、建構函數與抽象特性

深入解析 Dart 語言:命名慣例、類特性、建構函數與抽象特性

Overview of Content

在這篇文章中,我們將全面探索 Dart 語言的核心,有關於「類」的基礎概念;本文涵蓋了 Dart 檔案與方法的命名慣例,深入剖析類的特性,包括成員屬性域的訪問權、屬性 Getter & Setter 的使用、操作符重載、以及 Enum 類等進階功能

此外,文章詳細介紹了 Dart 的建構函數,包括命名建構函數的意義、初始化列表的應用、建構函數重定向及其限制,以及如何使用 constfactory 建構函數來提升效能與靈活性。我們也會比較 Dart 與 C++ 在初始化列表上的異同(也會適時的與 Java 語言做比較)

最後,對於面向對象設計中的抽象設計部分,我們將討論 Dart 的抽象類、介面、以及混合類 mixin 的使用,並深入解析多重繼承和菱形問題的解決方案,幫助你掌握 Dart 的方法解析順序(MRO)特性

以下使用 Dart SDK 3.4.3 版本

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


Dart 檔案、方法取名慣例

先了解一下 Java、Dart 對於檔案取名方式的差異 (以下是取名慣例)

● Java 語言

語言類型檔案取名方法取名
Java開頭大寫 + 駝峰開頭小寫 + 駝峰

// Java 檔案名稱 -> HelloWorld.java         

class HelloWorld {
    void sayHello() {

    }
}

● Dart 語言

語言類型檔案取名方法取名
Dart開頭小寫 + 底線開頭小寫 + 駝峰

// Dart 檔案名稱 -> hello_world.dart         

class HelloWorld {
    void sayHello() {

    }
}

Dart 類特性

接下來我們再來了解 Dart 語言所創造的類的特性、特點(以下會跟 Java 語言做比較)

成員屬性域:訪問權

● Java 對於類中的屬性有詳細的描述 (publicprotectedprivatepackage),而 Dart 並沒有這些描述字,Dart 預設所有的成員皆是公開的

如果要表達「私有」的特性,Dart 是透過底線(_)來將屬性、方法私有化

範例如下:

A. 創建 info_bean.dart 檔案:並在內部設置私有、公開成員

// info_bean.dart

class InfoBean {
  String? name;
  int? age;

  // 變數前使用 `_` 就會變為私有變量
  int? _id, _phone;
}

B. 創建 main.dart 檔案

// main.dart

import 'info_bean.dart';    // 引入自訂的類

void main() {
  InfoBean infoBean = InfoBean();   // new 可以省略
  infoBean.name = "Alien";
  infoBean.age = 18;

  // 無法訪問 `_` 開頭的屬性
  infoBean.id = 9527;
  infoBean._phone = 886;
}

● Dart 這種添加底線的私有的特性是 對於不同檔案才有用,無法使用在相同檔案中

也就是說私有方法、成員放在同個檔案中,就可以自由地被相同檔案中的程式存取

範例如下:


class InfoBean {
  String? name;
  int? age;

  // 變數前使用 `_` 就會變為私有變量
  int? _id, _phone;
}

class InfoBeanReset {

  void reset(InfoBean bean) {
    bean.name = null;
    bean.age = null;

    // okay, 可以正常訪問
    bean._id = null;
    bean._phone = null;
  }

}

如下圖所見,我們可以看到就算使用底線描述的成員,由於在相同檔案中,仍可被其他的方法、類訪問

Getter & Setter:屬性

屬性Property)是 Java 中有被提議但尚未實作的功能,它鑑於成員變量與方法之間… 使用屬性時,外部呼叫者看起來就是如同呼叫成員,而內部的實作則是方法

graph LR subgraph class_內部 屬性 屬性 <--> |內部使用如同方法| 方法 end 外部訪問 --> |看起來就像成員| 屬性

而在 Dart 中,每個成員都可以透過 getset 關鍵字將成員轉為屬性使用

Dart 屬性操控格式注意
get<類型> get <屬性名稱> => 操作函數-
set<類型> set <屬性名稱> (<參數>) => 操作函數不可以為 final 常量

當操作函數只有一行時可以使用 => 描述函數體,當超過一行時就必須使用 {} 描述

● 另外在使用 get 屬性時,若參數為空也可以省略參數括號

● Dart 使用屬性的範例如下

A. 類中普通的成員


class Point {
  // 一般的 member
  int x = 0;
  int y = 0;
}

B. 在類中宣告 get 屬性:在內部使用起來就如同使用方法一般,如果符合 Dart 語法規則就可以簡化為單行


class Point {
  ...

  // 宣告 get 屬性 xy
  int get xy {
    return x + y;
  }
  // 單行,省略大括號、return
  int get xy2 => x + y;
}

C. 在類中宣告 set、get 屬性:對於 Set 屬性則必須接收一個參數,該參數是外部使用者傳入的數值,通常會搭配一個私有數值保存真正的數值


import 'dart:math';

class Point {
  ...

  // 用來保存真正的數值
  int _z = 0;
  
  // 宣告 set 屬性 z
  // void 可省略
  void set z(int value) => _z = value;

  // 定義 get 屬性 z
  int get z {
    return _z;
  }
  
  // 在這裡我們還針對使用者傳入的數值進行運作再儲存
  set zz(int value) => _z = pow(2, value).toInt();
}

● 使用範例:在外部使用者使用其來就如同使用類的成員一樣,不會感知到它是屬性的特性


void main() {
  var point = Point()
    ..x = 10
    ..y = 200;

  // 呼叫 get 屬性(但不會感知到其實是屬性,用起來如同成員)
  print("point xy: ${point.xy}, z: ${point.z}");

  point.z = 200;
  print("point z: ${point.z}");

  point.zz = 3;
  print("point zz: ${point.z}");
}

● 適時的使用屬性可以增加程式可讀性與彈性,並減少複雜度並隱藏細節,第二個使用屬性的範例如下:


class Rect {
  String des = "Rectangle";
  num left;
  num top;
  num width;
  num height;

  // constructor
  Rect(this.left, this.top, this.width, this.height);

  // 使用 get 定義了一個 right 屬性
  num get right => left + width;
  set right(num value)  => left = value - width;

  // 使用 get 定義了一個 description 屬性
  String get description => "I am $des";
  set description(String str) => des = str;
}


void main() {
  Rect rect = Rect(1,2,3,4);
  print(rect.right);    // 1+3
  rect.right = 5;
  print(rect.left);     // 5-3

  print(rect.description);
  rect.description = "Square";
  print(rect.description);
}

操作符重載 operator

● Dart 操作符重載,類似於 C++ 操作符多載 (而 Java 就不支持操作符重載),同樣可以重新定義操作符號;透過 關鍵字 operator 就可以重載操作符,而且 Dart 比起 C++ 更靈活,返回值不一定要是自身類

Dart 可重載的操作符如下表(全部的操作符請點擊 Operator 連結 去官網訪站查看)

符號符號符號符號
<><==
-+*/
~/%[][]=
|&<<
~==%

class Point {
  int _x, _y;

  // construct
  Point(this._x, this._y);

  Point operator + (Point point) {
    // 成員函數 可以直接使用
    return Point(point._x + _x, point._y + _y);
  }
  
  // 故意將返回值改為另外一個類
  String operator - (int i) {
    return "Hello";
  }

  int getX() {
    return _x;
  }

  int getY() {
    return _y;
  }
}

void main() {
    var p1 = Point(10, 10);
    var p2 = Point(20, 20);
    
    var p3 = p1 + p2;
    
    print("x value: ${p3.getX()}");
    print("y value: ${p3.getY()}");
    
    var strValue = p1 - 10;
    print("---> $strValue");
}

Call 方法:重載呼叫符號

● Dart 有一個 call 函數,它相當於小括號 () 符號的重載(也就是呼叫符號重載)

透過定義它我們只要使用 () 就可以省略省去呼叫 call 函數… 使用範例如下


class Closure {

  call(String str1, String str2) {
   print("$str1 ~~~ $str2");
  }

}

void main() {
  Closure c = Closure();

  // 可忽略呼叫 call 方法
  c("Use symbol of `()`", "Hello World");
  // 同上功能
  c.call("Use function to call", "Yo Man");
}

Enum 類

● 如同 Java 的枚舉類,每個枚舉類型都有一個 index 的屬性可使用(用來標示元素的位置),並且可以透過 values 來取得 Enum 的集合

Dart Enum 類使用的範例如下:


enum BaseColor {
  Red,
  Green,
  Blue
}

void main() {
  BaseColor baseColor = BaseColor.Blue;

  print("BaseColor index: ${baseColor.index} ");

  print("BaseColor toString: ${baseColor.toString()} ");

  List<BaseColor> values = BaseColor.values;
  print("BaseColor values: $values ");
}

Dart 建構函數 constructor

Dart 的建構函數可以使用 可選命名參數{} 符號)、可選位置參數[] 符號)來包裹參數達到函數重載的特性 (因為 Dart 沒有函數重載)

命名建構函數:賦予建構函數意義

命名建構函數:當要載入特定建構函數,可以使用指定的命名方式

使用 「類名.屬性名」,使用屬性名稱命名,可以更清晰的知道自己創建的建構函數使用到哪個屬性,增加程式的可讀性 (以往建構函數並不具有可讀性,因為它沒有名稱,所以對於類的使用者來說缺乏語意)

● 有可選位置參數的話,為什麼還需要命名建構函數?

同樣是為了更好的可讀性,它可以更好的去了解到目前使用的類有作用在哪,而不用每個建構參數都去了解!

使用範例如下:

定義命名建構函數


class InfoBean {
  // 變數前使用 `_` 就會變為私有變量
  String? name;
  int? _id, age, _phone;

  InfoBean([this.name, this.age = 18, this._id, this._phone]);

  // 命函建構函數,命名為 name
  InfoBean.name(this.name);

  // 命函建構函數,命名為 nameAge
  InfoBean.nameAge(this.name, this.age);

  // 命函建構函數,命名為 a
  // 不必一定要使用屬性取名,但是使用屬性命名較清晰
  InfoBean.a(this.name);
  
  @override
  String toString() {
    return "name: $name, age: $age, id: $_id, phone: $_phone";
  }
  
}

使用命名建構函數


import 'info_bean.dart';

void main() {
  InfoBean infoBean = InfoBean();   // new 可以省略
  infoBean.name = "Alien";
  infoBean.age = 18;


  InfoBean kele = InfoBean.name("Kyle");
  print(kele);

  InfoBean pan = InfoBean.nameAge("Pan", 10);
  print(pan);

  // 假設使用不清晰的命名建構函數(缺乏語意)
  // 那與沒有命名相同,毫無含義
  InfoBean hello = InfoBean.a("Hello");
  print(hello);
}

初始化列表:與 C++ 比較

初始化列表:這種初始化列表的使用方式如同 C++ 的建構函數一樣,使用冒號(:)開頭,後面接續需要賦值的屬性

初始化列表很常被使用在定義私有的成員

使用範例如下:

● 在建構函數後使用初始化列表


class InfoBean {
  // 變數前使用 `_` 就會變為私有變量
  String name;
  int age;

  // 使用命名建構函數 加上 初始化列表
  InfoBean.initList(String name, int age)
      : this.name = name, this.age = age;    // 初始化列表在這


  @override
  String toString() {
    return "name: $_name, age: $_age";
  }
  
}

● 使用命名建構函數與初始化列表


void main() {
  InfoBean fish = InfoBean.initList("Fish", 19);
  
  print(fish);
}

● Dart 初始化列表與 C++ 初始化列表區別

C++ 11 以後也有初始化列表,不過 C++ 的初始化列表不能使用在類別宣告 (除了 inline 函數),因為 C++ 會把宣告與實現分開,而 Dart 不會


#include <string>

using std::string;

class InfoBean {

private:
    string mName;
    int mAge, mPhone;
    long mId;

public:
    InfoBean(string name, 
             int age = 18, 
             long id, 
             int phone)
        : string(name), 
          mAge(age), 
          mId(id), 
          mPhone(phone) {};	// 必須 inline 才可以在 '類宣告' 使用初始化列表

    virtual ~InfoBean();
};

建構函數重定向、重定向的限制

重新定向建構函數:java 在方法體內部使用 this 呼叫另一個建構函數,而 Dart 也可以使用 初始化列表的方式呼叫別的建構函數

建構函數重新定向


class InfoBean {
  // 變數前使用 `_` 就會變為私有變量
  String? _name;
  int? _age;

  // 私有的命名函數
  InfoBean._nameAge(this._name, this._age) {
    print("call InfoBean.nameAge construct");
  }

  // 重新定位不可以有 function body
  InfoBean.redirect(String name, int age) : this._nameAge(name, age);    // 重新定向到 InfoBean._nameAge

  @override
  String toString() {
    return "name: $_name, age: $_age";
  }

}

呼叫重定向的建構函數


void main() {
  final hey = InfoBean.redirect("Hey", 21);

  print(hey);
}

如下圖所見,呼叫重定向的建構函數後,它確被重新導向 InfoBean._nameAge 命名函數中

重新定向建構函數限制

A. 重新定向建構函數不可以使用 this 初始化

B. 重新定向建構函數不能有方法體

const 建構函數:加強效能

const 是在編譯期間就確定,並且它也可以說用在建構函數上… 而需要實現這個功能就要一些條件(或是說限制) 1. 使用 const 修飾建構函數,並 2. 聲明類的 所有 成員為 final3. 使用時必須以 const 宣告

const 可以減少動態規劃記憶體空間的耗能,提高運行效能

const 建構函數範例如下

A. 所有的成員都要初始化

以下是個錯誤示範,由於 name 成員沒有被定義,所以不能使用 const 宣告建構函數


// 錯誤示範

class HelloWorld {
  final String name;

  const HelloWorld();
}

B. 所有的成員皆為 final 描述

以下是個錯誤示範,雖然建構函數中都有賦予值,但是由於 number 成員沒有被 final 描述,所以也無法建構 const 建構函數


class HelloWorld {
  final String name;
  int number;

  const HelloWorld(this.name, this.number);
}

C. 使用 const 建構函數:用法如同呼叫一般建構函數,但是要注意,若要發揮出 const 建構函數的較能,則呼叫時也要用 const 宣告


void main() {
  // 宣告時必須也是 const 開頭
  HelloWorld h1 = const HelloWorld("Apple", 2);
  HelloWorld h2 = const HelloWorld("Banana", 7);
  HelloWorld h3 = const HelloWorld("Apple", 2);
  
  // 沒有 const 描述如同 new,必定會運行時規畫出新的空間(較為耗效能)
  HelloWorld h4 = HelloWorld("Apple", 2);

  print("const h1 hashcode: ${h1.hashCode}");
  print("const h2 hashcode: ${h2.hashCode}");
  print("const h3 hashcode: ${h3.hashCode}");
  print("h4 hashcode: ${h4.hashCode}");

  print("h1 == h2: ${h1 == h2}");    // false
  print("h1 == h3: ${h1 == h3}");    // true
  print("h1 == h4: ${h1 == h4}");    // false
}

● 如下圖,我們可以發現一件很有趣的事情

使用 const 創建的物件,若是成員數值相同則會被歸類為同一個物件,規劃在同一塊記憶體上

工廠建構函數:factory

Dart 有提供 factory 關鍵字,讓自動產生一個 工廠設計模式 的方法(想了解更多工廠設計請點擊連結),而 factory 這種提供方式就像是替我們創建了一個靜態方法,而該方法專門產生類的實體(instance

但這個產生的實體並非是單例,而是新物件

● factory & static 方法差異

factory 強制返回的就是該類的物件,而 static 則可以返回不同的物件 (或是不返回)

factory 範例如下


// factory 示範
class Manager {
  // 必須定義基礎 construct
  Manager();

  // 如果只有一行,你也可以簡化為 factory Manager.useFactory() => Manager();
  factory Manager.useFactory() {    // 不用宣告返回類型,自動定義返回類行為 Manager
    return Manager();
  }

  // 功能同上
  static Manager useStatic() {    // static 自己定義返回
    return Manager();
  }
}

void main() {
  Manager manager1 = Manager.useFactory();
  Manager manager2 = Manager.useFactory();

  print("Factory instance 1: ${manager1.hashCode}");
  print("Factory instance 2: ${manager2.hashCode}");
  print("Factory instance 1 equals 2: ${manager1 == manager2}");

}

使用 factory 實現 單一工廠覆用工廠

● 以下使用 Dart 的 factory 實現 單一工廠 模式

實現單例的手法仍然相同,重點就是 1. 私有化建構函數、2. 使用靜態變量保存單例物件、3. 使用靜態方法(而這裡就改成使用 factory 關鍵字)往外提供單例物件

實現物件單例


class Manager {
  static Manager? _instance;

  // Constructor
  // 使用 _<Function name> 可以讓外部函數無法調用
  Manager._internal();

  factory Manager.getInstance() {
    if(_instance == null) {
      _instance = new Manager._internal();
    }
    return _instance!;
  }
}

使用單例物件


void main() {

  Manager m1 = Manager.getInstance();
  print("m1 instance: ${m1.hashCode}");

  Manager m2 = Manager.getInstance();
  print("m2 instance: ${m2.hashCode}");

  print("m1, m2 is same instance: ${m1 == m2}");

}

想了解更多單例設計方式,請查看 Singleton 單例模式、設計 | 解說實現 | Android Framework Context Service

由於 Dart 是單執行緒,所以不用擔心同步問題 (不像 java 必須使用 volatilesynchronized 關鍵字)

● 同樣的,我們也可以使用 factory 關鍵字來實現 覆用工廠

其重點就是 1. 私有化建構函數、2. 使用 Map 變量保存物件、3. 使用靜態方法(而這裡就改成使用 factory 關鍵字)往外提供覆用物件

實現覆用物件


class Login {
  final String name;

  static Map<String, Login> _map = {};

  Login._name(this.name);     // 私有建構函數

  factory Login.getCount(String name) {
    if(_map.containsKey(name)) {
      return _map[name]!;
    } else {
      Login i = Login._name(name);
      _map[name] = i;
      return i;
    }
  }

}

使用覆用物件


void main() {

  Login l1 = Login.getCount('name');
  print("l1 instance: ${l1.hashCode}");

  Login l2 = Login.getCount('name');
  print("l2 instance: ${l2.hashCode}");

  print("l1, l2 is same instance: ${l1 == l2}");

}

想了解夠多的工廠設計方法,請查看 Factory 工廠方法模式 | 解說實現 | Java 集合設計


Dart 抽象設計

Java 的抽象設計有 abstract classinterface 並且它是單一繼承制度,而 Dart 在物件導向的設計上給予了不同於 Java 的設計(接下來這小節要介紹的)

抽象類 abstract class

● 使用 abstract 修飾符定義抽象類(abstract class


abstract class Parent {
  // 抽象方法,省略 abstract
  void printInfo();
}

這個抽象類如同 Java 抽象類不能實例化

● 繼承抽象類如同 Java 使用關鍵字 extends,若是有抽象方法就必須實作,並且透過 super 呼叫父類建構函數、方法、成員

範例如下

透過 abstract 關鍵字定義抽象類


// 抽象類

abstract class Parent {
  String name;

  Parent(this.name) {
    print('Parent construct');
  }
  // 抽象方法,省略 abstract
  void printInfo();

  void description() {
    name = "Mr.$name";
  }
}

定義子類,透過 extends 關鍵字繼承於父類


// 子類

class Child extends Parent {

  // 使用 super 呼叫父類
  Child(String name) : super(name) {
    print('Child construct');
  }

  @override
  void printInfo() {
    // 呼叫自身的 description 方法
    description();
    print('Hello World, $name');
    
    // 呼叫父類的 description 方法
    super.description();
    print('Call super function: Hello World, $name');
  }

  @override
  void description() {
    name = "Mrs.$name";
  }

}

● 使用抽象類範例:


void main() {
//  Parent parent = Parent();   // 不能實體化抽象類

  Parent p1 = Child("Alien");   // 可多型
  p1.printInfo();
  print("p1.runtimeType: ${p1.runtimeType}");
}

Dart 不支援內部類,Java 才支援內部類


// 內部類
class Hello {
    // Dart 不支援內部類
    class World {

    }
}

介面 implements

Dart 並沒有 interface 關鍵字Dart 中每個類都隱式的定義了一個包含實例成員的介面(不管抽象 or 實體類)

那要怎麼分辨當前類是使用「繼承」的抽象類,還是使用「實作」的介面類

這其實要依靠類的實現關鍵字來決定

● 如果實現類要使用繼承類,那就使用 extends 關鍵字

● 如果要使用實作介面類,那就要使用 implements 關鍵字

● 接下來我們看實現類如何透過實作,實體類(class)的介面、抽象類(abstract class)的介面

實作「實體類」的介面

● 定義實體類

由於實體類的方法必須定義方法體,所以以下使用方法的空實現


class IInterface {

  // 空實現
  void connect() {}    // 實體類,所以需要 "{}"
  
  void disconnect() {}
  
}

● 實現類把實體類作為介面使用

使用 implements 關鍵字實作實體類的介面方法… 由於 IInterface 的方法中有方法體,所以這邊的實現算是覆寫(override


class MyClass implements IInterface {

  @override
  void connect() {
    print("connect");
  }

  @override
  void disconnect() {
    print("disconnect");
  }

}

實作「抽象類」的介面

● 定義抽象類

由於使用抽象類,所以抽象方法不需要方法體


abstract class AClass {

  // 定義抽象方法
  void group();
  
  void item();
  
}

● 實現類把抽象類作為介面使用

同樣使用 implements 關鍵字來實作抽象類的方法


class Router implements AClass {
  @override
  void group() {
    print("group");
  }

  @override
  void item() {
    print("item");
  }

}

混合類 mixin:類多重繼承/菱形問題

● Dart 與一般的高級語言(像是 JavaKotlinSwift)不同的特點之一就是「多重繼承」(但 Dart 其實並非真正實現多重繼承,而是使用 Mixins 手動來達成「類似多重繼承」的效果)

Dart 以 mixin 關鍵字來定義「混合類」,並以 with 關鍵字來使用混合類

混合類範例如下


// 使用 mixin 關鍵字定義混合類
mixin class Hostel {

  String describe() {
    return "Hostel";
  }

}

// 配合 `with` 關鍵字就可以使用混合類
class Voyage with Hostel {

}

● 當然 mixin 關鍵字不只可以使用在類上,以可以在另一个 mixin 中使用 mixin


mixin Logger {
  void log(String message) {
    print('Log: $message');
  }
}

mixin Tracker {
  void track(String event) {
    print('Tracking: $event');
  }
}

mixin Analytics on Logger, Tracker {
  void analyze() {
    log('Analyzing data');
    track('Data analyzed');
  }
}

● 在 Dart 中,mixin 是一組可以應用到其他類上的功能,而 mixin class 它具有一定的限制,這些限制幫助避免多重繼承的菱形問題(最後會說明菱形問題);

mixin 類的限制如下所示

混合類除了空建構函數之外,不能有有參建構函數


mixin class Hostel {
  final String address;

  Hostel(this.address);

  String describe() {
    return "Hostel";
  }

}

混合類除了 Object 類以外,不能在繼承其它類,利用這個規則來避免多重繼承的菱形問題!


class Coffee {

  String describe() {
    return "Coffee";
  }


}

mixin class Hostel extends Coffee {    // 混合類不能再繼承其他類!

  String describe() {
    return "Hostel";
  }

}

什麼是菱形問題?混合類(mixin class)有多重繼承的菱形問題

在多重繼承裡有個較為麻煩的「菱形問題」:菱形問題是說,假設繼承兩個混合類,而這兩個混合類中又有相同的方法,那呼叫時到底該定位到哪個實現呢?

傳統多重繼承導致的菱形問題概念圖如下(下圖問題不會出現在 Dart 中)

classDiagram class abstractA { +method() } class abstractB { +method() } class abstractC { +method() } class D { +method() } abstractA <|-- abstractB abstractA <|-- abstractC abstractB <|-- D abstractC <|-- D

而 Dart 透過限制混合類(mixin class)的繼承來抑制這種菱形問題

混合類 mixin:方法解析順序 MRO

● 如上面小節所述,Dart 可以實現類似多重繼承的功能,並且雖然透過一些限制來避免了菱形問題,但還有另一個問題是「Dart 如何定位 mixin class 的方法」

如下圖所示,D 類別繼承了 B、C 類,但雙方都實現了同名的方法,那在呼叫方法 method 時該定位到哪個方法呢?

classDiagram class mixinB { +method() } class mixinC { +method() } class D { +method() } mixinB <|-- D mixinC <|-- D

● 對於這個問題,Dart 使用了 方法解析順序(Method Resolution Order, MRO) 來確保在使用 mixinmixin class 時,方法的調用順序是可預測的和一致的,從而解決了多重繼承定位方法的問題

使用範例(測試)如下

● 定義多個 mixin class,並類這些混合類中都擁有相同的方法


mixin class Coffee {
    
  // 相同的 describe 方法
  String describe() {
    return "Coffee";
  }

}

mixin class Bread {

  // 相同的 describe 方法
  String describe() {
    return "Bread";
  }

}

mixin class Hostel {

  // 相同的 describe 方法
  String describe() {
    return "Hostel";
  }

}

● 定義兩個實現類,這個個實現類都繼承多個混合類,並呼叫混合類中相同的方法(describe()),不過 繼承的順序不同!


class VoyageFinalHostel with Coffee, Bread, Hostel {

  void printMsg() {
    // 測試同名方法,Hostel 擺最後
    print("Final mixin method of describe: ${describe()}");    
  }

}

class VoyageFinalCoffee with Hostel, Bread, Coffee {

  void printMsg() {
    // 測試同名方法,Coffee 擺最後
    print("Final mixin method of describe: ${describe()}");    
  }

}

● 測試多重混合繼承後,對於相同的方法會被定位到哪類上


void main() {
  print("Use VoyageFinalHostel");
  VoyageFinalHostel().printMsg();

  print("\nUse VoyageFinalCoffee");
  VoyageFinalCoffee().printMsg();
}

如下圖所見,我們可以看到混合方法會依照順序被覆蓋,由於 VoyageFinalHostel 類最後繼承的混合類是 Hostel,所以 describe() 就會定位到 Hostel;而 VoyageFinalCoffee 類最後的繼承混合類是 Coffee,所以會定位到 Coffee


更多的 Flutter/Dart 語言相關文章

了解 Flutter 如何在跨平台開發中佔據重要地位,掌握快速上手的技巧與項目建置流程,開啟你的跨平台開發之旅!

探索跨平台與 Flutter 技術的未來:從認識到 Flutter 專案建置 | 3 種跨平台

Dart 語言基礎

探討 Dart 語言:宣告、數據類型、操作符 | 從基礎到應用指南

快速掌握 Dart 語言的核心概念,包括變數宣告、數據類型及操作符,為 Flutter 開發奠定扎實基礎。

Dart 函數與方法、異常處理、引用庫 | Java 比較

深入了解 Dart 的函數與異常處理特性,並與 Java 的處理方式進行比較,幫助你跨語言切換更加順暢。

深入解析 Dart 語言:命名慣例、類特性、建構函數與抽象特性

學習 Dart 類的設計邏輯及命名慣例,深入探索抽象類與 Mixin 的強大應用場景。

深入探索 Dart 的併發與異步處理:從 Isolate 到 Event Loop 的全面指南 | Future、Stream

徹底搞懂 Dart 的併發與異步處理,掌握 Isolate 與 Event Loop 的運行機制,助你提升應用效能!

深入 Flutter 框架

深入解析 Flutter Navigator:常見錯誤、解決方法與路由跳轉技巧、動畫

從常見問題到自定義解決方案,學會如何利用 Navigator 實現路由跳轉與流暢動畫效果。

深入理解 Flutter 中的數據共享:從普遍方案到 InheritedWidget | 3 種方案

探討數據共享的最佳實踐,了解 InheritedWidget 等三種主要方案,幫助你優化應用結構。

深入解析 Flutter 三顆樹:Widget、Element 與 RenderObject 完整指南

拆解 Flutter 的內部結構,全面了解 Widget、Element 和 RenderObject 之間的關係,提升你的 Flutter 開發技能!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響