物件導向這個用詞的重點在於「物件」兩個字,簡單來說就是將資料、相同的邏輯都包裝在一個「物件」當中。因此一個程式之中可能會有數個不同的物件,這些物件互不影響,就好像一台台獨立的機器在程式中運作一樣。聽起來有點抽象,我們從簡單的例子開始說起。
物件導向是一個很大的概念,我沒有辦法在有限的篇章中完全提及,我們會專注在「語法」的特性上進行介紹!
在成為一個物件之前,我們需要先定義 class (類別)。class 是定義一件事物的模板,比如每個人一定會有名字、年紀、性別這些基本資訊;物件則是根據類別所定義的模板去創建instance  (實例)。
在創建一個實例的會需要呼叫一個稱作 constructor (建構子) 的函式,並且函式名稱會與 class  的名稱相同。我們從下面例子來看
// 定義一個抽象的 class
class Person {
  // 每個 Person 有以下三種屬性
  String name; // 名字
  int age; // 年紀
  bool isMale; // 性別
  // Constructor (建構子)
  // 標準寫法
  Person(String name, int age, bool isMale):
    this.name = name,
    this.age = age,
    this.isMale = isMale;
  // 簡化寫法 (建議使用此寫法)
  Person(this.name, this.age, this.isMale);
  // Method (方法)
  void printName() {
    print('My name is ' + name);
  }
}
void main() {
  // 經由建構子將對應的資訊生成物件
  var p = Person('Micro Jordan', 25, true);
  // 取用物件屬性
  print(p.name);
  // 取用物件方法
  p.printName();
}
使用上述例子的 constructor  須確保呼叫建構子的順序為先 name 、age  再 isMale 。
當我們的屬性有一大堆要初始化時,要這樣一一對照順序真的很容易搞混,又因為 constructor  本身也是個函式,因此可以使用我們前面在 function  提到的 named parameters ,如此在創建一個新物件時便可以更加的容易操作。
class Person {
  // -- 屬性略過 --
  Person({required this.name, required this.age, required this.isMale});
  ...
}
void main() {
  var p = Person(name:'Micro Jordan', age: 25, isMale: true);
}
具名的建構子,可以讓建構一個物件是更具可讀性。
class Person {
  // -- 屬性略過 --
  // Named Parameters
  Person({required this.name, required this.age, required this.isMale});
  // Named Constructors
  // 建構確定生理性別為女性的人時,可以確定生理性別一定不為男性
  Person.whoIsFemale(name, age) : 
    this.name = name, 
    this.age = age, 
    this.isMale = false;
}
void main() {
  var f = Person.whoIsFemale('Amy', 25);
}
關於建構子還有很多種形式,我們就先不一一介紹拉。
在很多程式語言中會有相對於建構子存在的解構子(destructor),當該物件不需使用時呼叫解構子來釋放資源。在 Dart 中並不提供直接解構的方法,而是透過自動回收機制 (garbage collection)來自動管理物件的生命週期,並自動釋放不再需要的資源。
Methods 是物件提供物件進行某些操作的函式,例如我們上面的Person 物件的例子中實現了一個 printName 的方法就是用來印出當前物件的人名。我們可以任意的去寫我們所需的 method。
此外Dart 還提供我們去實作 operators  的操作,假設我想要判斷兩個 Person  誰的年紀比較大時,我們就可以去覆寫 >  運算子,同樣我們也舉例來說明。
class Person {
  // -- 屬性與建構子略過 --
  // 因為我可能會跟任何物件比較,因此傳入 Object (所有型態始祖),並於判斷式中先判斷是否為 Person class 再進行判斷年紀屬性
  @override
  bool operator >(Object other) =>
    other is Person && this.age > other.age;
  
}
void main() {
  var p1 = Person(name: 'Andy', age: 25, isMale: true);
  var p2 = Person(name: 'Amy', age: 24, isMale: false);
  // 因為 25 > 24,所以邏輯為真
  print(p1 > p2);
}
註記:如果要覆寫的是
==這個運算子,則必須連同hashCode的方法也一同覆寫
在 Dart 中可使用 getter 與 setter 來取得/修改類別的屬性,下面舉個例子就可以明白拉。
class Person {
  // -- 屬性與建構子略過 --
  // getter 方法:出生的年份毋須再宣告成一屬性,因為可以實時的運算取得
  int get bornYear {
    return 2023 - age;
  }
  // setter 方法:一旦過了生日,就年長一歲
  set birthday(bool isAfter) {
    if (isAfter) {
      age += 1;
    }
  }
}
void main() {
  var p = Person(name: 'Micro Jordan', age: 24, isMale: true);
  // 取用 getter
  print(p.bornYear);
  // 假設今年已經過了生日,調用 setter 方法
  p.birthday = true;
  // 從 24 -> 25
  print(p.age);
}
Extends  可以用來擴充類別,並且繼承原先 class  中的所有屬性及方法。但仍有可能遇到方法需要進行覆寫的情況,因此需要加上 override 的關鍵字。
class Person {
  String name;
  int age;
  bool isMale;
  Person({required this.name, required this.age, required this.isMale});
  void printName() {
    print('Hello my name is ' + name);
  }
}
class Student extends Person {
  String school;
  // 由於 name, age, isMale 皆繼承自父類別,因此呼叫父類別的建構子,再加上新的屬性 school
  Student({required String name, required int age, required bool isMale, required this.school}) : 
    super(name: name, age: age, isMale: isMale);
  // 覆寫 printName 這個 method
  @override
  void printName() {
    print("Hello my name is $name and I'm from $school school.");
  }
}
void main() {
  var p = Person(name: 'Andy', age: 25, isMale: true);
  
  var s = Student(name: 'Andy', age: 25, isMale: true, school: 'ABC');
  p.printName(); // Hello my name is Andy
  s.printName(); // Hello my name is Andy and I'm from ABC school.
}
抽象類別是一個特殊的存在,只有定義類別,而不能實例化。通常是用於定義一個共享的介面或是基礎類別時而使用的。若有 class 要引用 abstract class 則加入 implements  關鍵字表示要實作該抽象類別。
abstract class Speak {
  // 定義一個 method,每個引用的 class 都得實作
  void sayGoodbye();
}
class Person implements Speak {
  // -- 屬性與建構子略過 -- 
  @override
  void sayGoodbye() {
    print("I'm $name, see you next time");
  }
}
class Student extends Person implements Speak {
  // -- 屬性與建構子略過 -- 
  @override
  void sayGoodbye() {
    print("I'm $name from $school, see you next time");
  }
}
class  的概念在 Dart 中真的非常重要,真的認真講起來會花很多篇幅可能才有辦法一一解釋清楚。因此與其一一講清楚,倒不如我們先從簡單例子下去了解大概某個概念的用法為何,先有初步了解後,從實戰中去學習,我覺得更加重要!
接下來我們來淺談 Dart 的錯誤處理。
Exceptions  表示某種未預期的錯誤或異常情況發生時,用於提供關於這些錯誤中有用的資訊。倘若這些錯誤沒有被處理時,很可能會導致程式的終止,想必這並不是使用者所樂見的情形,因此錯誤處理相當重要。
在 Dart 中可以使用 try  來包住可能引發異常的程式碼,catch  來捕獲以及處理這些異常情況。finally  用於無論 try- catch  中成功或失敗都應執行的程式碼
try {
  // 可能引發異常的程式碼
  // 除以 0 是會引發異常的行為
  int n = 1 / 0;
} catch (err) {
  // Unsupported operation: Result of truncating division is Infinity: 1 ~/ 0
  print('錯誤: $err');
} finally {
  print('無論如何都會執行 finally,通常用於清理的操作');
}
在 Dart 中已經定義了數種 Exception 的形式,我們也經常會用到 throw  去引發自定義的一些異常行為,舉例來說
try {
  if (age < 0) {
    // 當給定一個年齡值為負數時,就拋出錯誤並提供有用的資訊
    throw Exception('年齡不得為負數');
  }
} catch (e) {
  print('錯誤: $e');
}
當然你也可以自定義想要的 Exception class  將這些錯誤進行分門別類
// 自定義一個狀態碼的 exception
class HttpCodeException implements Exception {
  final int statusCode;
  final String message;
  HttpCodeException(this.statusCode, this.message);
  // Exception 為 abstract class 有定義 toString,因此一定要 @override
  @override
  String toString() {
    return 'HttpCodeException: code ' + statusCode + ', Message: ' + message;
  }
}
void main() {
  try {
    // 取得 api 失敗
    throw HttpCodeException(400, 'Bad Request');
  } on HttpCodeException catch (e) {
    // 針對特定 exception 處理
    print(e);
  } on Exception catch (e) {
    // 處理其他的 Exception
    print(e);
  } catch (e) {
    // 處理剩下非 Exception 的其他錯誤
    print(e);
  }
}
今天的內容涵蓋很廣:
1. 類別在成為物件之前,需先定義 class 類別;物件是根據類別的定義創建的 instance
2. 創建 instance 是透過呼叫與 class 同名的 constructor 來產生
3. Named parameters 能具名的傳遞參數避免混淆
4. Named constructors 允許多個建構子存在,提高可讀性
5. Abstract class 僅用於定義類別而無法實例化。其他類別要引用時可以使用 implements 對其進行實作
6. Exception handling 則介紹了可使用 try、catch 與 finally 來處理 Exceptions,避免未預期的錯誤以及異常情況的發生
大家不妨使用 DartPad 好好的熟悉熟悉。明天將會是介紹 Dart 語法的最後篇章,我們將會提到同步/非同步處理,這在與網路進行交互的時候會不斷地使用,因此大家撐著點XD 。明天見囉~