---
title: 'Dart 類的特性、建構函數、mixin 抽象'
disqus: kyleAlien
---
Dart 類的特性、建構函數、mixin 抽象
===
## Overview of Content
如有引用參考請詳註出處,感謝 :cat:
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**深入解析 Dart 語言:命名慣例、類特性、建構函數與抽象特性**](https://devtechascendancy.com/guide_dart-class_abstract-mixin-class/)
:::
:::info
以下使用 Dart SDK `3.4.3` 版本
:::
[TOC]
## Dart 檔案、方法取名慣例
先了解一下 Java、Dart 對於檔案取名方式的差異 (以下是取名慣例)
* Java 語言
| 語言類型 | 檔案取名 | 方法取名 |
| -------- | -------- | -------- |
| Java | **開頭大寫** + 駝峰 | **開頭小寫** + 駝峰 |
```java=
// Java 檔案名稱 -> HelloWorld.java
class HelloWorld {
void sayHello() {
}
}
```
* Dart 語言
| 語言類型 | 檔案取名 | 方法取名 |
| -------- | -------- | -------- |
| Dart | **開頭小寫** + 底線 | **開頭小寫** + 駝峰 |
```dart=
// Dart 檔案名稱 -> hello_world.dart
class HelloWorld {
void sayHello() {
}
}
```
## Dart 類特性
接下來我們再來了解 Dart 語言所創造的類的特性、特點(以下會跟 Java 語言做比較)
### 成員屬性域:訪問權
* Java 對於類中的屬性有詳細的描述 (`public`、`protected`、`private`、`package`),而 Dart 並沒有這些描述字,**Dart 預設所有的成員皆是公開的**
如果要表達「私有」的特性,**Dart 是透過底線(`_`)來將屬性、方法私有化**
範例如下:
1. **創建 `info_bean.dart` 檔案**:並在內部設置私有、公開成員
```dart=
// info_bean.dart
class InfoBean {
String? name;
int? age;
// 變數前使用 `_` 就會變為私有變量
int? _id, _phone;
}
```
2. **創建 `main.dart` 檔案**:
```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 這種添加底線的私有的特性是 **對於不同檔案才有用,無法使用在相同檔案中**
:::warning
也就是說私有方法、成員放在同個檔案中,就可以自由地被相同檔案中的程式存取
:::
範例如下:
```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 中有被提議但尚未實作的功能,**它鑑於成員變量與方法之間**… 使用屬性時,外部呼叫者看起來就是如同呼叫成員,而內部的實作則是方法
```mermaid
graph LR
subgraph class_內部
屬性
屬性 <--> |內部使用如同方法| 方法
end
外部訪問 --> |看起來就像成員| 屬性
```
而在 Dart 中,每個成員都可以透過 `get`、`set` 關鍵字將成員轉為屬性使用
| Dart 屬性操控 | 格式 | 注意 |
| -------- | -------- | -------- |
| get | <類型> get <屬性名稱> => 操作函數 | - |
| set | <類型> set <屬性名稱> (<參數>) => 操作函數 | 不可以為 final 常量 |
:::info
* **當操作函數只有一行時可以使用 `=>` 描述函數體,當超過一行時就必須使用 `{}` 描述**
* 另外在使用 `get` 屬性時,若參數為空也可以省略參數括號
:::
* Dart 使用屬性的範例如下
1. 類中普通的成員
```dart=
class Point {
// 一般的 member
int x = 0;
int y = 0;
}
```
2. 在類中宣告 get 屬性:在內部使用起來就如同使用方法一般,如果符合 Dart 語法規則就可以簡化為單行
```dart=
class Point {
...
// 宣告 get 屬性 xy
int get xy {
return x + y;
}
// 單行,省略大括號、return
int get xy2 => x + y;
}
```
3. 在類中宣告 set、get 屬性:對於 Set 屬性則必須接收一個參數,該參數是外部使用者傳入的數值,通常會搭配一個私有數值保存真正的數值
```dart=
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();
}
```
* 使用範例:在外部使用者使用其來就如同使用類的成員一樣,不會感知到它是屬性的特性
```java=
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}");
}
```
> 
* 適時的使用屬性可以增加程式可讀性與彈性,並減少複雜度並隱藏細節,第二個使用屬性的範例如下:
```java=
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++ 操作符多載**](https://hackmd.io/Roh5BF4MRN2YPgmZZ7c3Sw#operator-%E9%81%8B%E7%AE%97%E5%AD%90%E5%A4%9A%E8%BC%89) (而 Java 就不支持操作符重載),同樣可以重新定義操作符號;透過 **++關鍵字 operator++** 就可以重載操作符,而且 Dart 比起 C++ 更靈活,**返回值不一定要是自身類**
Dart 可重載的操作符如下表(全部的操作符請點擊 [**Operator 連結**](https://dart.dev/language/operators) 去官網訪站查看)
| 符號 | 符號 | 符號 | 符號 |
| - | - | - | - |
| < | > | <= | >= |
| - | + | * | / |
| ~/ | % | [] | []= |
| \| | & | << | >> |
| ~ | == | % | |
```dart=
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` 函數… 使用範例如下
```dart=
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 類使用的範例如下:
```dart=
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 ");
}
```
> 
## 建構函數 constructor
Dart 的建構函數可以使用 [**可選命名參數**](https://devtechascendancy.com/dart-java-compare_exception_function-methods/#%E5%8F%AF%E9%81%B8%E5%91%BD%E5%90%8D%E5%8F%83%E6%95%B8%EF%BC%9A%E9%A0%90%E8%A8%AD%E5%8F%83%E6%95%B8)(`{}` 符號)、[**可選位置參數**](https://devtechascendancy.com/dart-java-compare_exception_function-methods/#%E5%8F%AF%E9%81%B8%E4%BD%8D%E7%BD%AE%E5%8F%83%E6%95%B8%EF%BC%9A%E9%A0%90%E8%A8%AD%E5%8F%83%E6%95%B8)(`[]` 符號)來包裹參數達到函數重載的特性 (**因為 Dart 沒有函數重載**)
### 命名建構函數:賦予建構函數意義
* **==命名建構函數==**:當要載入特定建構函數,可以使用指定的命名方式
使用 「類名.屬性名」,**使用屬性名稱命名,可以更清晰的知道自己創建的建構函數使用到哪個屬性**,增加程式的可讀性 (以往建構函數並不具有可讀性,**因為它沒有名稱**,所以對於類的使用者來說缺乏語意)
:::success
* 有可選位置參數的話,為什麼還需要命名建構函數?
同樣是為了更好的可讀性,它可以更好的去了解到目前使用的類有作用在哪,而不用每個建構參數都去了解!
:::
使用範例如下:
* **定義命名建構函數**
```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";
}
}
```
* **使用命名建構函數**
```dart=
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++ 的建構函數一樣,使用冒號(`:`)開頭,後面接續需要賦值的屬性
> 初始化列表很常被使用在定義私有的成員
使用範例如下:
* 在建構函數後使用初始化列表
```dart=
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";
}
}
```
* 使用命名建構函數與初始化列表
```dart=
void main() {
InfoBean fish = InfoBean.initList("Fish", 19);
print(fish);
}
```
> 
:::info
* Dart 初始化列表與 C++ 初始化列表區別
C++ 11 以後也有初始化列表,不過 C++ 的初始化列表不能使用在類別宣告 (除了 inline 函數),因為 C++ 會把宣告與實現分開,而 Dart 不會
```cpp=
#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 也可以使用 **初始化列表的方式呼叫別的建構函數**
* **建構函數重新定向**
```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";
}
}
```
* **呼叫重定向的建構函數**
```dart=
void main() {
final hey = InfoBean.redirect("Hey", 21);
print(hey);
}
```
如下圖所見,呼叫重定向的建構函數後,它確被重新導向 `InfoBean._nameAge` 命名函數中
> 
:::warning
* **重新定向建構函數限制**
1. 重新定向建構函數不可以使用 `this` 初始化
> 
2. 重新定向建構函數不能有方法體
> 
:::
### const 建構函數:加強效能
* **const 是在編譯期間就確定**,並且它也可以說用在建構函數上… 而需要實現這個功能就要一些條件(或是說限制) **^1.^ 使用 `const` 修飾建構函數,並 ^2.^ 聲明類的 ==++所有++ 成員為 `final`==,^3.^ 使用時必須以 `const` 宣告**
:::info
const 可以減少動態規劃記憶體空間的耗能,提高運行效能
:::
const 建構函數範例如下
1. **所有的成員都要初始化**:
以下是個錯誤示範,由於 `name` 成員沒有被定義,所以不能使用 `const` 宣告建構函數
```dart=
// 錯誤示範
class HelloWorld {
final String name;
const HelloWorld();
}
```
> 
2. **所有的成員皆為 `final` 描述**:
以下是個錯誤示範,雖然建構函數中都有賦予值,但是由於 `number` 成員沒有被 `final` 描述,所以也無法建構 `const` 建構函數
```dart=
class HelloWorld {
final String name;
int number;
const HelloWorld(this.name, this.number);
}
```
> 
3. 使用 `const` 建構函數:用法如同呼叫一般建構函數,但是要注意,若要發揮出 `const` 建構函數的較能,則呼叫時也要用 `const` 宣告
```dart=
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
}
```
:::success
* 如下圖,我們可以發現一件很有趣的事情
使用 const 創建的物件,若是成員數值相同則會被歸類為同一個物件,規劃在同一塊記憶體上
:::
> 
### 工廠建構函數:factory
* **Dart 有提供 ==`factory`== 關鍵字,讓自動產生一個 [工廠設計模式](https://devtechascendancy.com/object-oriented_design_factory_framework/) 的方法**(想了解更多工廠設計請點擊連結),而 `factory` 這種提供方式就像是替我們創建了一個靜態方法,而該方法專門產生類的實體(`instance`)
> 但這個產生的實體並非是單例,而是新物件
:::info
* factory & static 方法差異
factory 強制返回的就是該類的物件,而 static 則可以返回不同的物件 (或是不返回)
:::
`factory` 範例如下
```dart=
// 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 實現 [單一工廠](https://devtechascendancy.com/object-oriented_design_factory_framework/#%E5%96%AE%E4%B8%80%E5%B7%A5%E5%BB%A0%EF%BC%9A%E9%9D%9C%E6%85%8B%E5%B7%A5%E5%BB%A0)、[覆用工廠](https://devtechascendancy.com/object-oriented_design_factory_framework/#%E6%8B%93%E5%B1%95%E5%B7%A5%E5%BB%A0%EF%BC%9A%E8%A6%86%E7%94%A8%E5%B7%A5%E5%BB%A0)
* 以下使用 Dart 的 `factory` 實現 [**單一工廠**](https://devtechascendancy.com/object-oriented_design_factory_framework/#%E5%96%AE%E4%B8%80%E5%B7%A5%E5%BB%A0%EF%BC%9A%E9%9D%9C%E6%85%8B%E5%B7%A5%E5%BB%A0) 模式
實現單例的手法仍然相同,重點就是 ^1.^ 私有化建構函數、^2.^ 使用靜態變量保存單例物件、^3.^ 使用靜態方法(而這裡就改成使用 `factory` 關鍵字)往外提供單例物件
* **實現物件單例**:
```dart=
class Manager {
static Manager? _instance;
// Constructor
// 使用 _<Function name> 可以讓外部函數無法調用
Manager._internal();
factory Manager.getInstance() {
if(_instance == null) {
_instance = new Manager._internal();
}
return _instance!;
}
}
```
* **使用單例物件**
```dart=
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}");
}
```
> 
:::success
想了解更多單例設計方式,請查看 [**Singleton 單例模式、設計 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/)
:::
:::warning
**由於 Dart 是單執行緒,所以不用擔心同步問題** (不像 java 必須使用 `volatile`、`synchronized` 關鍵字)
:::
* 同樣的,我們也可以使用 `factory` 關鍵字來實現 [覆用工廠](https://devtechascendancy.com/object-oriented_design_factory_framework/#%E6%8B%93%E5%B1%95%E5%B7%A5%E5%BB%A0%EF%BC%9A%E8%A6%86%E7%94%A8%E5%B7%A5%E5%BB%A0)
其重點就是 ^1.^ 私有化建構函數、^2.^ 使用 Map 變量保存物件、^3.^ 使用靜態方法(而這裡就改成使用 `factory` 關鍵字)往外提供覆用物件
* **實現覆用物件**:
```dart=
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;
}
}
}
```
* **使用覆用物件**:
```dart=
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}");
}
```
> 
:::success
想了解夠多的工廠設計方法,請查看 [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework)
:::
## Dart 抽象設計
Java 的抽象設計有 `abstract class`、`interface` 並且它是單一繼承制度,而 Dart 在物件導向的設計上給予了不同於 Java 的設計(接下來這小節要介紹的)
### 抽象類 abstract class
* 使用 abstract 修飾符定義抽象類(`abstract class`)
```dart=
abstract class Parent {
// 抽象方法,省略 abstract
void printInfo();
}
```
這個抽象類如同 Java 抽象類不能實例化
> 
* 繼承抽象類如同 Java 使用關鍵字 `extends`,若是有抽象方法就必須實作,並且透過 super 呼叫父類建構函數、方法、成員
範例如下
* **透過 `abstract` 關鍵字定義抽象類**
```dart=
// 抽象類
abstract class Parent {
String name;
Parent(this.name) {
print('Parent construct');
}
// 抽象方法,省略 abstract
void printInfo();
void description() {
name = "Mr.$name";
}
}
```
* **定義子類,透過 `extends` 關鍵字繼承於父類**
```dart=
// 子類
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";
}
}
```
* 使用抽象類範例:
```dart=
void main() {
// Parent parent = Parent(); // 不能實體化抽象類
Parent p1 = Child("Alien"); // 可多型
p1.printInfo();
print("p1.runtimeType: ${p1.runtimeType}");
}
```
> 
:::danger
* **Dart 不支援內部類**,Java 才支援內部類
```dart=
// 內部類
class Hello {
// Dart 不支援內部類
class World {
}
}
```
> 
:::
### 介面 implements
* **Dart 並++沒有 `interface`++ 關鍵字**,**Dart 中每個類都隱式的定義了一個包含實例成員的介面(不管抽象 or 實體類)**
:::warning
* **那要怎麼分辨當前類是使用「繼承」的抽象類,還是使用「實作」的介面類**?
這其實要依靠類的實現關鍵字來決定
* 如果實現類要使用繼承類,那就使用 `extends` 關鍵字
* 如果要使用實作介面類,那就要使用 `implements` 關鍵字
:::
* 接下來我們看實現類如何透過實作,實體類(`class`)的介面、抽象類(`abstract class`)的介面
* **實作「實體類」的介面**
* 定義實體類
由於實體類的方法必須定義方法體,所以以下使用方法的空實現
```dart=
class IInterface {
// 空實現
void connect() {} // 實體類,所以需要 "{}"
void disconnect() {}
}
```
* 實現類把實體類作為介面使用
使用 `implements` 關鍵字實作實體類的介面方法… 由於 `IInterface` 的方法中有方法體,所以這邊的實現算是覆寫(`override`)
```dart=
class MyClass implements IInterface {
@override
void connect() {
print("connect");
}
@override
void disconnect() {
print("disconnect");
}
}
```
* **實作「抽象類」的介面**
* 定義抽象類
由於使用抽象類,所以抽象方法不需要方法體
```dart=
abstract class AClass {
// 定義抽象方法
void group();
void item();
}
```
* 實現類把抽象類作為介面使用
同樣使用 `implements` 關鍵字來實作抽象類的方法
```dart=
class Router implements AClass {
@override
void group() {
print("group");
}
@override
void item() {
print("item");
}
}
```
### 混合類 mixin:類多重繼承/菱形問題
* Dart 與一般的高級語言(像是 `Java`、`Kotlin`、`Swift`)不同的特點之一就是「**多重繼承**」(但 Dart 其實並非真正實現多重繼承,而是使用 Mixins 手動來達成「類似多重繼承」的效果)
Dart 以 `mixin` 關鍵字來定義「**混合類**」,並以 `with` 關鍵字來使用混合類
混合類範例如下
```dart=
// 使用 mixin 關鍵字定義混合類
mixin class Hostel {
String describe() {
return "Hostel";
}
}
// 配合 `with` 關鍵字就可以使用混合類
class Voyage with Hostel {
}
```
:::info
* 當然 `mixin` 關鍵字不只可以使用在類上,以可以在另一个 mixin 中使用 mixin
```dart=
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 類的限制如下所示
* **混合類除了空建構函數之外,不能有有參建構函數**
```dart=
mixin class Hostel {
final String address;
Hostel(this.address);
String describe() {
return "Hostel";
}
}
```
> 
* **混合類除了 Object 類以外,不能在繼承其它類**,利用這個規則來避免多重繼承的菱形問題!
```dart=
class Coffee {
String describe() {
return "Coffee";
}
}
mixin class Hostel extends Coffee { // 混合類不能再繼承其他類!
String describe() {
return "Hostel";
}
}
```
> 
:::warning
* **什麼是菱形問題?混合類(`mixin class`)有多重繼承的菱形問題**?
在多重繼承裡有個較為麻煩的「**菱形問題**」:菱形問題是說,假設繼承兩個混合類,而這兩個混合類中又有相同的方法,那呼叫時到底該定位到哪個實現呢?
傳統多重繼承導致的菱形問題概念圖如下(下圖問題不會出現在 Dart 中)
```mermaid
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` 時該定位到哪個方法呢?
```mermaid
classDiagram
class mixinB {
+method()
}
class mixinC {
+method()
}
class D {
+method()
}
mixinB <|-- D
mixinC <|-- D
```
* 對於這個問題,Dart 使用了 **方法解析順序(`Method Resolution Order`, MRO)** 來確保在使用 `mixin` 和 `mixin class` 時,方法的調用順序是可預測的和一致的,從而解決了多重繼承定位方法的問題
使用範例(測試)如下
* 定義多個 `mixin class`,並類這些混合類中都擁有相同的方法
```dart=
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()`),不過 **繼承的順序不同!**
```dart=
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()}");
}
}
```
* 測試多重混合繼承後,對於相同的方法會被定位到哪類上
```dart=
void main() {
print("Use VoyageFinalHostel");
VoyageFinalHostel().printMsg();
print("\nUse VoyageFinalCoffee");
VoyageFinalCoffee().printMsg();
}
```
如下圖所見,我們可以看到混合方法會依照順序被覆蓋,由於 `VoyageFinalHostel` 類最後繼承的混合類是 `Hostel`,所以 `describe()` 就會定位到 Hostel;而 `VoyageFinalCoffee` 類最後的繼承混合類是 `Coffee`,所以會定位到 Coffee
> 
## Appendix & FAQ
:::info
:::
###### tags: `Flutter`、`Dart`