Dart 作為一種現代化的程式語言,其函式和類別設計簡潔而強大。理解如何有效運用這些特性,是編寫優質 Dart 程式碼的關鍵。本文將帶您深入瞭解 Dart 函式和類別的定義、用法及最佳實務,並輔以程式碼範例和圖表說明,助您編寫更簡潔、易維護的 Dart 程式碼。從命名引數和預設值的使用,到箭頭函式的簡潔語法,再到 Getter 和 Setter 的封裝技巧,本文將逐步引導您掌握 Dart 函式和類別的核心概念。同時,我們也將探討物件導向程式設計中的重要原則,如封裝、繼承和多型,以及如何在 Dart 中有效運用這些原則。

函式的定義

在 Dart 中,函式的定義使用 boolintString 等關鍵字來指定傳回值的型別。函式的名稱和引數列表之間使用括號 () 分隔。以下是個簡單的函式定義例子:

bool withinTolerance(int value, [int min = 0, int max = 10]) {
  return min <= value && value <= max;
}

在這個例子中,withinTolerance 函式接受一個 int 型別的 value 引數和兩個可選的 int 型別的 minmax 引數。函式的傳回值是 bool 型別。

函式的呼叫

函式的呼叫使用函式名稱和引數列表。以下是呼叫 withinTolerance 函式的例子:

withinTolerance(5); // true
withinTolerance(15); // false

在這個例子中,withinTolerance 函式被呼叫兩次,第一次傳入 5,第二次傳入 15。函式的傳回值分別是 truefalse

命名引數

Dart 中的命名引數可以使函式的呼叫更加清晰和易於理解。以下是使用命名引數的例子:

bool withinTolerance(int value, {int min = 0, int max = 10}) {
  return min <= value && value <= max;
}

withinTolerance(9, min: 7, max: 11); // true
withinTolerance(9, max: 11, min: 7); // true

在這個例子中,withinTolerance 函式的 minmax 引數被定義為命名引數。函式的呼叫使用引數名稱來指定引數的值。

內容解密:

在這個例子中,withinTolerance 函式的定義使用了命名引數。這使得函式的呼叫更加清晰和易於理解。函式的傳回值是 bool 型別,表示 value 是否在 minmax 之間。

圖表翻譯:

  flowchart TD
    A[開始] --> B[呼叫 withinTolerance 函式]
    B --> C[傳入 value、min 和 max 引數]
    C --> D[計算 min <= value && value <= max]
    D --> E[傳回結果]
    E --> F[結束]

在這個圖表中,withinTolerance 函式的呼叫和執行過程被視覺化地呈現出來。圖表顯示了函式的呼叫、引數的傳入、計算的執行和結果的傳回。

Dart 中的命名引數和預設值

在 Dart 中,函式的引數可以是命名引數,也可以是位置引數。命名引數是透過名稱來指定的,而位置引數是透過位置來指定的。

命名引數

命名引數是透過名稱來指定的,例如:

void withinTolerance({int value, int min = 0, int max = 10}) {
  // ...
}

在這個例子中,valueminmax 都是命名引數。命名引數可以是可選的,也可以是必需的。

###預設值 命名引數可以有預設值,例如:

void withinTolerance({int value, int min = 0, int max = 10}) {
  // ...
}

在這個例子中,minmax 的預設值分別是 0 和 10。如果沒有指定 minmax 的值,則使用預設值。

必需引數

如果一個命名引數是必需的,可以使用 required 關鍵字來指定,例如:

bool withinTolerance({
  required int value,
  int min = 0,
  int max = 10,
}) {
  return min <= value && value <= max;
}

在這個例子中,value 是必需的引數,如果沒有指定 value 的值,則會報錯。

練習

  1. 寫一個名為 youAreWonderful 的函式,該函式有一個名為 name 的字串引數。該函式應傳回一個字串,使用 name 並說些類似於 “You’re wonderful, Bob.” 的話。
  2. 對於上面的函式,新增一個名為 numberPeople 的整數引數,以便函式傳回類似於 “You’re wonderful, Bob. 10 people think so.” 的字串。
  3. 對於上面的函式,將兩個輸入引數都設為命名引數。將 name 設為必需引數,並將 numberPeople 的預設值設為 30。

解答

String youAreWonderful(String name) { return ‘You're wonderful, $name.’; }

2. ```dart
String youAreWonderful(String name, int numberPeople) {
  return 'You\'re wonderful, $name. $numberPeople people think so.';
}

String youAreWonderful({ required String name, int numberPeople = 30, }) { return ‘You're wonderful, $name. $numberPeople people think so.’; }


## 撰寫良好的函式
良好的函式是軟體開發的基礎。一個良好的函式應該具有明確的目的、少量的程式碼和沒有副作用。以下是撰寫良好的函式的幾個原則。

### 避免副作用
副作用是指函式除了傳回值以外還會對外部環境產生影響。例如,列印輸出到主控臺或修改全域變數。副作用會使函式的行為更加複雜和難以預測。

```dart
void hello() {
  print('Hello!');
}

上述函式具有副作用,因為它列印輸出到主控臺。為了避免副作用,可以改寫函式如下:

String hello() {
  return 'Hello!';
}

現在,函式不再具有副作用,因為它只傳回一個字串。

只做一件事

一個良好的函式應該只做一件事。如果一個函式過於複雜或具有多個責任,應該將它分解為多個小函式。這樣可以使函式更容易理解和維護。

void calculateAreaAndPerimeter(int length, int width) {
  int area = length * width;
  int perimeter = 2 * (length + width);
  print('Area: $area, Perimeter: $perimeter');
}

上述函式可以分解為兩個小函式:

int calculateArea(int length, int width) {
  return length * width;
}

int calculatePerimeter(int length, int width) {
  return 2 * (length + width);
}

void printAreaAndPerimeter(int length, int width) {
  int area = calculateArea(length, width);
  int perimeter = calculatePerimeter(length, width);
  print('Area: $area, Perimeter: $perimeter');
}

選擇良好的名稱

函式名稱應該清晰地描述函式的目的。名稱應該簡短但具有描述性。

String getAverageTemperature(List<int> temperatures) {
  // ...
}

可以改寫為:

String averageTemperature(List<int> temperatures) {
  // ...
}

選擇性型別

Dart是一種選擇性型別的語言,這意味著可以省略型別宣告。然而,為了提高程式碼的可讀性和維護性,建議在可能的情況下新增型別宣告。

String compliment(int number) {
  return '$number is a very nice number.';
}

可以省略型別宣告如下:

compliment(number) {
  return '$number is a very nice number.';
}

但是,這樣會使得Dart無法推斷型別,從而可能導致錯誤。因此,建議在可能的情況下新增型別宣告。

圖表翻譯:

  flowchart TD
    A[撰寫函式] --> B[避免副作用]
    B --> C[只做一件事]
    C --> D[選擇良好的名稱]
    D --> E[選擇性型別]
    E --> F[完成]

上述流程圖描述了撰寫良好的函式的步驟。首先,避免副作用;然後,只做一件事;接下來,選擇良好的名稱;最後,選擇性型別。透過遵循這些步驟,可以撰寫出高質量的函式。

Dart 中的函式

Dart 是一種支援物件導向程式設計的語言,函式是 Dart 中的一個基本單位。函式可以封裝一段程式碼,讓它可以被重複呼叫和使用。

箭頭函式

Dart 有一種特殊的語法,用於定義只有一個陳述式的函式。這種語法被稱為箭頭函式。箭頭函式的定義方式如下:

int add(int a, int b) => a + b;

箭頭函式的優點是可以讓程式碼更加簡潔和易於閱讀。

函式的定義

函式的定義包括函式的名稱、引數和傳回值。函式的名稱是用來呼叫函式的,引數是用來傳遞資料給函式的,傳回值是函式執行完畢後傳回的結果。

函式的呼叫

函式可以被呼叫多次,函式的呼叫方式如下:

int result = add(2, 3);

挑戰

現在,讓我們來做一些挑戰,來測試你的函式知識。

挑戰 1:圓的面積

寫一個函式,計算圓的面積,根據輸入的半徑。

挑戰 2:質數

寫一個函式,檢查一個數字是否是質數。

首先,寫一個函式,檢查一個數字是否可以被另一個數字整除:

bool isNumberDivisible(int number, int divisor) {
  return number % divisor == 0;
}

然後,寫一個函式,傳回 true 如果數字是質數,否則傳回 false:

bool isPrime(int number) {
  if (number < 2) {
    return false;
  }
  for (int i = 2; i * i <= number; i++) {
    if (isNumberDivisible(number, i)) {
      return false;
    }
  }
  return true;
}

測試以下案例:

print(isPrime(6)); // false
print(isPrime(13)); // true
print(isPrime(8893)); // true

類別的基礎

類別(Class)是物件導向程式設計(Object-Oriented Programming, OOP)中的核心概念。它可以被視為是一種藍圖或範本,描述瞭如何建立一個物件。物件是類別的例項,代表了具體的資料和行為。

類別的定義

在Dart中,類別是使用class關鍵字定義的。類別的定義包括了類別的名稱、屬性(Properties)和方法(Methods)。屬性是類別的資料成員,方法是類別的行為成員。

class MyClass {
  // 屬性
  var myProperty;

  // 方法
  void myMethod() {
    print(myProperty);
  }
}

類別的例項化

要建立一個類別的例項,需要使用new關鍵字。例如:

MyClass myObject = new MyClass();

類別的屬性和方法

類別的屬性可以是例項變數或靜態變數。例項變數是每個例項都有的獨立副本,靜態變數是所有例項共享的。

類別的方法可以是例項方法或靜態方法。例項方法是每個例項都有的獨立副本,靜態方法是所有例項共享的。

物件導向程式設計的基本概念

物件導向程式設計的基本概念包括:

  • 封裝(Encapsulation):將資料和行為封裝在一個單元中。
  • 繼承(Inheritance):一個類別可以繼承另一個類別的屬性和方法。
  • 多型(Polymorphism):一個類別可以有多種不同的行為。
  • 抽象(Abstraction):將複雜的系統簡化為一個簡單的模型。

類別的優點

類別的優點包括:

  • 封裝:類別可以封裝資料和行為,減少程式的複雜性。
  • 重用性:類別可以被重用,減少程式的開發時間。
  • 繼承:類別可以繼承其他類別的屬性和方法,增加程式的靈活性。

圖表翻譯:

  classDiagram
  class MyClass {
    - myProperty: var
    + myMethod(): void
  }
  note "MyClass 是一個類別,具有 myProperty 屬性和 myMethod 方法"

內容解密:

在上面的例子中,MyClass是一個類別,具有myProperty屬性和myMethod方法。myProperty是類別的資料成員,myMethod是類別的行為成員。類別的例項化需要使用new關鍵字。類別的屬性和方法可以是例項變數或靜態變數,例項方法或靜態方法。物件導向程式設計的基本概念包括封裝、繼承、多型和抽象。類別的優點包括封裝、重用性、繼承和靈活性。

類別的基礎

類別是物件導向程式設計中的基本概念。它是一種範本,定義了物件的屬性和行為。在Dart中,類別是使用class關鍵字定義的。

定義類別

以下是定義一個簡單的User類別的範例:

class User {
  int id = 0;
  String name = '';
}

這個類別有兩個屬性:idnameid是整數型別,預設值為0;name是字串型別,預設值為空字串。

建立物件

要建立一個物件,需要使用類別的名稱後面加上括號,例如:

final user = User();

這會建立一個新的User物件,並將其指定給user變數。

屬性存取

可以使用點符號(.)來存取物件的屬性,例如:

user.name = 'Ray';
user.id = 42;

這會將name屬性設定為"Ray",將id屬性設定為42。

建構函式

在Dart中,建構函式是一種特殊的函式,用於建立物件。它的名稱與類別名稱相同,且沒有傳回型別。以下是User類別的建構函式範例:

class User {
  int id;
  String name;

  User({this.id = 0, this.name = ''});
}

這個建構函式有兩個引數:idname,它們都有預設值。

物件的建立

可以使用建構函式來建立物件,例如:

final user = User(id: 42, name: 'Ray');

這會建立一個新的User物件,並將其指定給user變數。

內容解密:

上述程式碼定義了一個簡單的User類別,具有idname屬性。然後,建立了一個新的User物件,並將其指定給user變數。最後,使用點符號存取物件的屬性,並將其設定為新的值。

  flowchart TD
    A[定義類別] --> B[建立物件]
    B --> C[存取屬性]
    C --> D[設定屬性值]

圖表翻譯:

此圖表示了建立物件和存取其屬性的過程。首先,定義了一個類別,然後建立了一個新的物件。接著,使用點符號存取物件的屬性,並將其設定為新的值。

類別的基礎

在Dart中,類別是用來定義物件的藍圖。您可以使用類別來建立新的物件,並為其定義屬性和方法。讓我們來看一個簡單的例子:

class User {
  int id = 0;
  String name = '';
}

在這個例子中,我們定義了一個名為User的類別,它有兩個屬性:idname。您可以使用這個類別來建立新的User物件,並為其定義屬性值。

列印物件

當您列印一個物件時,Dart會呼叫該物件的toString()方法。預設情況下,toString()方法會傳回一個字串,指示物件的類別名稱和例項。讓我們來看一個例子:

User user = User();
user.id = 42;
user.name = 'Ray';
print(user);

如果您執行這段程式碼,輸出將會是Instance of 'User'。這不是很有用,因為我們想要看到idname的值。為瞭解決這個問題,我們可以覆寫toString()方法:

class User {
  int id = 0;
  String name = '';

  @override
  String toString() {
    return 'User(id: $id, name: $name)';
  }
}

現在,如果您執行相同的程式碼,輸出將會是User(id: 42, name: Ray)

序列化物件

序列化是指將物件轉換為字串或其他可儲存或傳輸的格式。Dart提供了多種序列化格式,包括JSON。讓我們來看一個例子:

class User {
  int id = 0;
  String name = '';

  String toJson() {
    return '{"id":$id,"name":"$name"}';
  }
}

在這個例子中,我們定義了一個名為toJson()的方法,它將User物件轉換為JSON字串。您可以使用這個方法來序列化User物件:

User user = User();
user.id = 42;
user.name = 'Ray';
print(user.toJson());

輸出將會是{"id":42,"name":"Ray"}

物件導向程式設計中的物件參考

在 Dart 中,物件被視為記憶體中類別例項的參考。這意味著當您將一個物件指派給另一個物件時,後者只會持有對相同記憶體位置的參考,而不是建立一個新的例項。

物件參考的示例

class MyClass {
  var myProperty = 1;
}

void main() {
  final myObject = MyClass();
  final anotherObject = myObject;

  print(myObject.myProperty); // 1
  anotherObject.myProperty = 2;
  print(myObject.myProperty); // 2
}

在這個例子中,myObjectanotherObject 都參考相同的記憶體位置。因此,當我們修改 anotherObjectmyProperty 時,也會影響 myObjectmyProperty

物件導向程式設計中的封裝

封裝是物件導向程式設計中的核心原則之一,指的是隱藏類別中的內部資料和邏輯,僅暴露必要的介面給外部世界。這樣做有幾個好處:

  • 類別控制自己的資料,包括誰可以存取和修改它。
  • 隔離控制使得邏輯更容易理解。
  • 隱藏資料意味著更好的隱私性。
  • 封裝防止無關的類別存取和修改資料,從而減少難以找到錯誤的可能性。
  • 你可以在不破壞外部世界的前提下修改內部變數名稱和邏輯。

實作封裝

在Dart中,你可以透過使變數私有化來實作封裝。要使變數私有化,只需在變數名稱前新增底線(_)即可。例如:

class Password {
  String _plainText = 'pass123';
}

這樣,_plainText變數就變成了私有變數,外部無法直接存取它。

Getter

Getter是一種特殊的方法,傳回私有欄位變數的值。它是私有變數的公共介面。如果你曾經使用過Java或C++,你可能見過以getColorgetWidth命名的getter方法。在Dart中,你可以使用get關鍵字定義getter。

class Password {
  String _plainText = 'pass123';

  String get plainText => _plainText;
}

現在,你可以使用plainText屬性存取_plainText變數的值。

final myPassword = Password();
final text = myPassword.plainText;

這樣,你就可以控制外部如何存取私有變數的值,並且保持了封裝原則。

隱藏內部實作

透過使用getter,你可以隱藏類別的內部實作,僅暴露必要的介面給外部世界。這使得你的程式碼更容易維護和擴充套件。

class Password {
  String _plainText = 'pass123';

  String get plainText => _plainText;
}

在這個例子中,_plainText變數是私有的,外部無法直接存取它。但是,透過plainTextgetter,外部可以存取它的值。

內容解密:

上述程式碼定義了一個Password類別,包含一個私有變數_plainText和一個getter方法plainText。getter方法傳回私有變數的值,使得外部可以存取它的值,而不需要直接存取私有變數。

圖表翻譯:

  classDiagram
  class Password {
    - _plainText: String
    + plainText(): String
  }

此圖表顯示了Password類別的結構,包括私有變數_plainText和getter方法plainText()

類別中的Getter和Setter

在Dart中,getter和setter是類別中用於控制對物件屬性存取的重要工具。getter允許你定義一個方法,該方法不需要括號就可以被呼叫,傳回一個值。setter則允許你定義一個方法,該方法可以用於設定一個值。

Getter

Getter是一種特殊的方法,允許你從物件中取得一個值,而不需要顯式地呼叫一個方法。以下是定義一個getter的例子:

class Password {
  String _plainText;

  String get plainText => _plainText;
}

在這個例子中,plainText是一個getter,傳回 _plainText 的值。注意,getter不需要括號就可以被呼叫。

Setter

Setter是一種特殊的方法,允許你設定一個值到物件中。以下是定義一個setter的例子:

class Password {
  String _plainText;

  set plainText(String text) => _plainText = text;
}

在這個例子中,plainText是一個setter,設定 _plainText 的值。

計算屬性

計算屬性是一種特殊的getter,傳回一個計算出的值。以下是定義一個計算屬性的例子:

class Password {
  String _plainText;

  String get obfuscated {
    final length = _plainText.length;
    return '*' * length;
  }
}

在這個例子中,obfuscated是一個計算屬性,傳回一個由星號組成的字串,長度與 _plainText 相同。

使用Getter和Setter

以下是使用getter和setter的例子:

final myPassword = Password();
final text = myPassword.obfuscated;
print(text); // 輸出:*******
myPassword.plainText = '123456';
print(myPassword.plainText); // 輸出:123456

在這個例子中,myPassword是一個 Password 物件,obfuscated是一個計算屬性,傳回一個由星號組成的字串。plainText是一個getter和setter,允許你取得和設定 _plainText 的值。

使用Getter和Setter進行資料驗證

在Dart中,Getter和Setter可以用來控制對物件屬性的存取。以下是如何使用Setter進行資料驗證的範例:

class Password {
  String _plainText = '';

  String get plainText => _plainText;

  set plainText(String text) {
    if (text.length < 6) {
      print('密碼必須有6個或以上的字元!');
      return;
    }
    _plainText = text;
  }
}

在這個範例中,Setter會檢查密碼的長度是否少於6個字元,如果是,則會印出警告訊息並不更新密碼。

測試Setter

以下是測試Setter的範例:

void main() {
  final shortPassword = Password();

  shortPassword.plainText = 'aaa';

  final result = shortPassword.plainText;

  print(result);
}

執行這個程式,會印出以下的輸出:

密碼必須有6個或以上的字元!

密碼並沒有被更新為aaa

不需要過度使用Getter和Setter

如果只需要簡單地存取一個內部變數,則不需要使用Getter和Setter。例如,以下的類別:

class Email {
  String _value = '';

  String get value => _value;
  set value(String value) => _value = value;
}

可以簡化為:

class Email {
  String value = '';
}

這樣可以減少不必要的程式碼。

類別與建構式

在物件導向程式設計中,類別是封裝資料和方法的基本單位。Dart 中的類別可以定義屬性和方法,屬性是類別中的變數,方法是類別中的函式。

從技術架構視角來看,Dart 的函式和類別設計體現了現代程式語言的特性,兼顧了簡潔性、靈活性和安全性。命名引數、預設值、箭頭函式等語法糖簡化了程式碼,提升了可讀性。類別的封裝、繼承、多型等特性則提供了建構複雜應用程式的基礎。然而,Dart 的選擇性型別系統雖然提供了靈活性,但也可能隱藏型別錯誤的風險,需要開發者更加謹慎地處理型別推斷和型別檢查。展望未來,隨著 Dart 生態系統的持續發展,預計會有更多工具和最佳實踐出現,以幫助開發者更好地利用 Dart 的特性,構建更健壯、高效的應用程式。對於追求程式碼品質和長期維護性的團隊,深入理解 Dart 的型別系統和物件導向程式設計原則至關重要。