Try   HackMD

Dart 類的特性、建構函數、mixin 抽象

Overview of Content

如有引用參考請詳註出處,感謝

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

以下使用 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 是透過底線(_)來將屬性、方法私有化

    範例如下:

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

      ​​​​​​​​// info_bean.dart ​​​​​​​​class InfoBean { ​​​​​​​​ String? name; ​​​​​​​​ int? age; ​​​​​​​​ // 變數前使用 `_` 就會變為私有變量 ​​​​​​​​ int? _id, _phone; ​​​​​​​​}
    2. 創建 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; ​​​​​​​​}

      Image Not Showing Possible Reasons
      • The image was uploaded to a note which you don't have access to
      • The note which the image was originally uploaded to has been deleted
      Learn More →

  • 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 Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

Getter & Setter:屬性

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

    class_內部

    內部使用如同方法

    看起來就像成員

    屬性

    方法

    外部訪問

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

    Dart 屬性操控 格式 注意
    get <類型> get <屬性名稱> => 操作函數 -
    set <類型> set <屬性名稱> (<參數>) => 操作函數 不可以為 final 常量
    • 當操作函數只有一行時可以使用 => 描述函數體,當超過一行時就必須使用 {} 描述

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

  • Dart 使用屬性的範例如下

    1. 類中普通的成員

      ​​​​​​​​class Point { ​​​​​​​​ // 一般的 member ​​​​​​​​ int x = 0; ​​​​​​​​ int y = 0; ​​​​​​​​}
    2. 在類中宣告 get 屬性:在內部使用起來就如同使用方法一般,如果符合 Dart 語法規則就可以簡化為單行

      ​​​​​​​​class Point { ​​​​​​​​ ... ​​​​​​​​ // 宣告 get 屬性 xy ​​​​​​​​ int get xy { ​​​​​​​​ return x + y; ​​​​​​​​ } ​​​​​​​​ // 單行,省略大括號、return ​​​​​​​​ int get xy2 => x + y; ​​​​​​​​}
    3. 在類中宣告 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}"); ​​​​​​​​}

      image

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

    ​​​​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

操作符重載 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"); ​​​​}

    image

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"); ​​​​}

    image

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 "); ​​​​}

    image

建構函數 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); ​​​​​​​​}

      image

初始化列表:與 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); ​​​​​​​​}

      image

      • 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 命名函數中

      image

      • 重新定向建構函數限制

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

          image

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

          image

const 建構函數:加強效能

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

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

    const 建構函數範例如下

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

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

      ​​​​​​​​// 錯誤示範 ​​​​​​​​ ​​​​​​​​class HelloWorld { ​​​​​​​​ final String name; ​​​​​​​​ const HelloWorld(); ​​​​​​​​}

      image

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

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

      ​​​​​​​​class HelloWorld { ​​​​​​​​ final String name; ​​​​​​​​ int number; ​​​​​​​​ const HelloWorld(this.name, this.number); ​​​​​​​​}

      image

    3. 使用 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 創建的物件,若是成員數值相同則會被歸類為同一個物件,規劃在同一塊記憶體上

      image

工廠建構函數: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}"); ​​​​}

    image

使用 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}"); ​​​​​​​​}

      image

      由於 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}"); ​​​​​​​​}

      image

      想了解夠多的工廠設計方法,請查看 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}"); ​​​​​​​​}

      image

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

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

    image

介面 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"; ​​​​​​​​ } ​​​​​​​​}

      image

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

      ​​​​​​​​class Coffee { ​​​​​​​​ String describe() { ​​​​​​​​ return "Coffee"; ​​​​​​​​ } ​​​​​​​​} ​​​​​​​​mixin class Hostel extends Coffee { // 混合類不能再繼承其他類! ​​​​​​​​ String describe() { ​​​​​​​​ return "Hostel"; ​​​​​​​​ } ​​​​​​​​}

      image

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

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

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

        abstractA

        +method()

        abstractB

        +method()

        abstractC

        +method()

        D

        +method()

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

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

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

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

    mixinB

    +method()

    mixinC

    +method()

    D

    +method()

  • 對於這個問題,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

      image

Appendix & FAQ

tags: FlutterDart