物件導向這個用詞的重點在於「物件」兩個字,簡單來說就是將資料、相同的邏輯都包裝在一個「物件」當中。因此一個程式之中可能會有數個不同的物件,這些物件互不影響,就好像一台台獨立的機器在程式中運作一樣。聽起來有點抽象,我們從簡單的例子開始說起。
物件導向是一個很大的概念,我沒有辦法在有限的篇章中完全提及,我們會專注在「語法」的特性上進行介紹!
在成為一個物件之前,我們需要先定義 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 。明天見囉~