--- 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; } ``` > ![image](https://hackmd.io/_uploads/HJd5mrjqR.png) * 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; } } ``` 如下圖所見,我們可以看到就算使用底線描述的成員,由於在相同檔案中,仍可被其他的方法、類訪問 > ![image](https://hackmd.io/_uploads/ryPGEriqC.png) ### 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}"); } ``` > ![image](https://hackmd.io/_uploads/rkKfhSo5A.png) * 適時的使用屬性可以增加程式可讀性與彈性,並減少複雜度並隱藏細節,第二個使用屬性的範例如下: ```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); } ``` > ![image](https://hackmd.io/_uploads/H14nnHjcC.png) ### 操作符重載 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"); } ``` > ![image](https://hackmd.io/_uploads/HJqDBPs9C.png) ### 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"); } ``` > ![image](https://hackmd.io/_uploads/B1cWvDoc0.png) ### 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 "); } ``` > ![image](https://hackmd.io/_uploads/Byo6BPj50.png) ## 建構函數 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); } ``` > ![image](https://hackmd.io/_uploads/r1u1jDjq0.png) ### 初始化列表:與 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); } ``` > ![image](https://hackmd.io/_uploads/SkXmpvj9A.png) :::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` 命名函數中 > ![image](https://hackmd.io/_uploads/SkkOe_oqA.png) :::warning * **重新定向建構函數限制** 1. 重新定向建構函數不可以使用 `this` 初始化 > ![image](https://hackmd.io/_uploads/rkc4bdicR.png) 2. 重新定向建構函數不能有方法體 > ![image](https://hackmd.io/_uploads/HJ_v-Osc0.png) ::: ### const 建構函數:加強效能 * **const 是在編譯期間就確定**,並且它也可以說用在建構函數上… 而需要實現這個功能就要一些條件(或是說限制) **^1.^ 使用 `const` 修飾建構函數,並 ^2.^ 聲明類的 ==++所有++ 成員為 `final`==,^3.^ 使用時必須以 `const` 宣告** :::info const 可以減少動態規劃記憶體空間的耗能,提高運行效能 ::: const 建構函數範例如下 1. **所有的成員都要初始化**: 以下是個錯誤示範,由於 `name` 成員沒有被定義,所以不能使用 `const` 宣告建構函數 ```dart= // 錯誤示範 class HelloWorld { final String name; const HelloWorld(); } ``` > ![image](https://hackmd.io/_uploads/Byq87dj9A.png) 2. **所有的成員皆為 `final` 描述**: 以下是個錯誤示範,雖然建構函數中都有賦予值,但是由於 `number` 成員沒有被 `final` 描述,所以也無法建構 `const` 建構函數 ```dart= class HelloWorld { final String name; int number; const HelloWorld(this.name, this.number); } ``` > ![image](https://hackmd.io/_uploads/HyRhm_i9C.png) 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 創建的物件,若是成員數值相同則會被歸類為同一個物件,規劃在同一塊記憶體上 ::: > ![image](https://hackmd.io/_uploads/r1X6Edi9A.png) ### 工廠建構函數: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}"); } ``` > ![image](https://hackmd.io/_uploads/r18O9Oo9R.png) ### 使用 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}"); } ``` > ![image](https://hackmd.io/_uploads/BJ9GjOj9C.png) :::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}"); } ``` > ![image](https://hackmd.io/_uploads/Sk9C6_i5A.png) :::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 抽象類不能實例化 > ![](https://i.imgur.com/I5N2KGw.png) * 繼承抽象類如同 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}"); } ``` > ![image](https://hackmd.io/_uploads/H1JAUEhq0.png) :::danger * **Dart 不支援內部類**,Java 才支援內部類 ```dart= // 內部類 class Hello { // Dart 不支援內部類 class World { } } ``` > ![image](https://hackmd.io/_uploads/B1pHvEn9C.png) ::: ### 介面 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"; } } ``` > ![image](https://hackmd.io/_uploads/BkuJlr2cR.png) * **混合類除了 Object 類以外,不能在繼承其它類**,利用這個規則來避免多重繼承的菱形問題! ```dart= class Coffee { String describe() { return "Coffee"; } } mixin class Hostel extends Coffee { // 混合類不能再繼承其他類! String describe() { return "Hostel"; } } ``` > ![image](https://hackmd.io/_uploads/B1DXyS39C.png) :::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 > ![image](https://hackmd.io/_uploads/SJeeYH3qR.png) ## Appendix & FAQ :::info ::: ###### tags: `Flutter`、`Dart`