Dart 提供了多種建構子型別,方便開發者根據不同情境初始化物件。工廠建構子可在傳回物件前執行額外邏輯,例如驗證或轉換資料。命名建構子則允許定義多個建構子,提升程式碼可讀性。靜態成員屬於類別本身,而非特定例項,常用於定義常數、工具方法或單例模式。理解這些建構子和靜態成員的特性,有助於編寫更具彈性且易於維護的 Dart 程式碼。此外,Dart 的空安全機制能有效避免 null pointer exceptions,提升程式碼的穩定性。可空型別以問號標記,提醒開發者注意潛在的 null 值,並使用 null-aware operators 進行安全處理,確保程式碼的健壯性。

工廠建構子(Factory Constructors)

工廠建構子是一種特殊的建構子,允許您在傳回新物件之前執行一些工作。它們使用 factory 關鍵字來定義。工廠建構子可以用來執行驗證、錯誤檢查和修改引數等工作。

以下是使用工廠建構子建立 User 物件的範例:

class User {
  final int id;
  final String name;

  factory User.fromJson(Map<String, Object> json) {
    final userId = json['id'] as int;
    final userName = json['name'] as String;
    return User(id: userId, name: userName);
  }

  User({required this.id, required this.name});
}

在這個範例中,User.fromJson 是一個工廠建構子,它從一個 JSON 物件中建立一個 User 物件。它執行一些工作來初始化 idname 屬性,然後傳回一個新建立的 User 物件。

命名建構子(Named Constructors)

命名建構子是一種特殊的建構子,允許您建立多個建構子,每個建構子都有不同的名稱。命名建構子使用 : 關鍵字來定義。

以下是使用命名建構子建立 User 物件的範例:

class User {
  final int id;
  final String name;

  User.fromJson(Map<String, Object> json)
      : id = json['id'] as int,
        name = json['name'] as String;
}

在這個範例中,User.fromJson 是一個命名建構子,它從一個 JSON 物件中建立一個 User 物件。

建構子比較(Constructor Comparisons)

建構子可以有很多種不同的變化,包括:

  • 順向或非順向(Forwarding or non-forwarding)
  • 命名或非命名(Named or unnamed)
  • 生成或工廠(Generative or factory)
  • 常數或非常數(Constant or not constant)
  • 有引數或無引數(With parameters or without)
  • 簡短形式或長形式(Short-form or long-form)
  • 公開或私有(Public or private)

以下是使用不同種類的建構子建立 User 物件的範例:

class User {
  final int id;
  final String name;

  const User(this.id, this.name); // 公開、非順向、非命名、生成、常數、有引數、簡短形式

  factory User.fromJson(Map<String, Object> json) {
    // ...
  } // 工廠建構子

  User._internal(); // 私有、非順向、命名、生成、非常數、無引數、長形式
}

挑戰(Challenges)

現在,讓我們來做一個挑戰,建立一個 Student 類別,具有 firstNamelastName 的最終屬性,以及 grade 的可變屬性。新增一個建構子來初始化所有屬性,並新增一個方法來格式化 Student 物件的輸出。

以下是解決方案:

class Student {
  final String firstName;
  final String lastName;
  int grade;

  Student({required this.firstName, required this.lastName, required this.grade});

  String toString() {
    return '$firstName $lastName: $grade';
  }
}

void main() {
  Student bert = Student(firstName: 'Bert', lastName: 'Ernie', grade: 95);
  Student ernie = Student(firstName: 'Ernie', lastName: 'Bert', grade: 85);

  print(bert);
  print(ernie);
}

這個解決方案建立了一個 Student 類別,具有 firstNamelastNamegrade 的屬性。它增加了一個建構子來初始化所有屬性,並增加了一個 toString 方法來格式化 Student 物件的輸出。最終,它建立了兩個 Student 物件,berternie,並將它們的屬性輸出到主控臺。

Dart 中的靜態成員和建構函式

在 Dart 中,建構函式是用於建立類別的物件。您可以透過類別名稱加上括號來呼叫建構函式,例如 var obj = MyClass();。Dart 還支援命名建構函式,允許您定義多個建構函式,每個都有不同的名稱。

建構函式的種類

Dart 中有兩種建構函式:無名建構函式和命名建構函式。無名建構函式的名稱與類別名稱相同,而命名建構函式則在類別名稱後面增加了額外的識別符。

class MyClass {
  // 無名建構函式
  MyClass() {
    print('無名建構函式');
  }

  // 命名建構函式
  MyClass.named() {
    print('命名建構函式');
  }
}

初始化列表

初始化列表允許您在建構函式中初始化欄位變數。這是透過在建構函式引數列表後面新增冒號和初始化列表來實作的。

class MyClass {
  int _x;
  int _y;

  MyClass(int x, int y) : _x = x, _y = y {
    print('建構函式');
  }
}

常數建構函式

新增 const 標記到建構函式可以讓您建立不可變的類別例項。

class MyClass {
  final int _x;
  final int _y;

  const MyClass(int x, int y) : _x = x, _y = y {
    print('常數建構函式');
  }
}

靜態成員

靜態成員是屬於類別而不是例項的變數或方法。您可以透過在成員變數或方法前面新增 static 標記來定義靜態成員。

class MyClass {
  static int myProperty = 0;

  static void myMethod() {
    print('靜態方法');
  }
}

您可以直接透過類別名稱來訪問靜態成員,而不需要建立例項。

void main() {
  print(MyClass.myProperty);
  MyClass.myMethod();
}

靜態變數通常用於定義常數或單例模式。

常數

您可以使用 const 標記來定義常數。

class TextStyle {
  static const double _defaultFontSize = 17.0;

  final double fontSize;

  TextStyle({this.fontSize = _defaultFontSize});
}

這種方式可以使您的程式碼更清晰、更易於維護。

靜態成員:Color Codes 和 Singleton Pattern

在前面的例子中,我們看到了一個私有的常數,但在某些情況下,公共常數也很有用。例如,Flutter 的 Colors 類別就是一個很好的例子。現在,我們將建立一個簡化的版本。

class Colors {
  static const int red = 0xFFD13F13;
  static const int purple = 0xFF8107D9;
  static const int blue = 0xFF1432C9;
}

這些是紅、紫和藍的十六進位制值。0x 字首告訴 Dart 我們正在使用十六進位制值來表示整數。八個十六進位制字元之後的 0x 跟隨著 AARRGGBB 模式,其中 AA 是 alpha 或透明度,RR 是紅色,GG 是綠色,BB 是藍色。

然後,在 main 中新增以下行:

final backgroundColor = Colors.purple;

這是一種更易於理解的方式來描述顏色,而不是在整個應用程式中散佈十六進位制值。

Singleton Pattern

另一個使用靜態變數的方法是建立一個單例類別。單例是一種設計模式,只有一個物件例項。雖然有些人辯論它們的好處,但它們使某些任務更加方便。

在 Dart 中建立單例很容易。你不會想要讓 User 成為單例,因為你可能會有多個不同的使用者,需要多個 User 例項。但你可能想要建立一個單例類別作為資料庫幫助器,以確保你不會開啟多個資料庫連線。

以下是基本單例類別的外觀:

class MySingleton {
  MySingleton._();
  static final MySingleton instance = MySingleton._();
}

MySingleton._() 部分是一個私有的、具名的建構函式。有些人喜歡稱它為 _internal 以強調它不能從外部呼叫。底線使得它不可能正常例項化類別。但靜態屬性,只初始化一次,提供了一個對例項化物件的參照。

注意:靜態欄位和頂級變數(類別外的全域性變數)是懶惰初始化的,這意味著 Dart 不會在你第一次使用它們之前計算和分配它們的值。

你可以像這樣訪問單例:

final mySingleton = MySingleton.instance;

由於工廠建構函式不需要傳回物件的新例項,因此你也可以使用工廠建構函式實作單例模式:

class MySingleton {
  MySingleton._();
  static final MySingleton _instance = MySingleton._();
  factory MySingleton() => _instance;
}

這裡的優點是你可以隱藏它是一個單例的事實,對於誰使用它來說:

final mySingleton = MySingleton();

從外部來看,這看起來完全像一個正常的物件。這允許你稍後將其更改回生成建構函式,而不會影響專案的其他部分的程式碼。

過去兩個部分都在關於靜態變數。接下來,你將看看靜態方法。

靜態方法

你可以使用靜態方法做一些有趣的事情。靜態方法是屬於類別本身的方法,而不是例項方法。它們不需要建立類別的例項就可以被呼叫。

以下是靜態方法的例子:

class MyClass {
  static void myStaticMethod() {
    print('這是靜態方法');
  }
}

你可以像這樣呼叫靜態方法:

MyClass.myStaticMethod();

靜態方法可以用來提供實用函式或工具函式,例如數學函式或字串處理函式。

內容解密:

  • 我們建立了一個 Colors 類別,包含靜態常數來表示顏色。
  • 我們使用十六進位制值來表示顏色,並使用 0x 字首來告訴 Dart 我們正在使用十六進位制值。
  • 我們建立了一個單例類別 MySingleton,使用靜態變數和私有的具名建構函式。
  • 我們使用工廠建構函式來實作單例模式,允許我們隱藏它是一個單例的事實。
  • 我們介紹了靜態方法的概念,展示瞭如何建立和呼叫靜態方法。

圖表翻譯:

  flowchart TD
    A[建立 Colors 類別] --> B[定義靜態常數]
    B --> C[使用十六進位制值]
    C --> D[建立單例類別]
    D --> E[使用靜態變數和私有的具名建構函式]
    E --> F[使用工廠建構函式]
    F --> G[建立靜態方法]
    G --> H[呼叫靜態方法]

靜態方法的應用

靜態方法是與類別相關的方法,但不與任何特定的例項相關。它們通常用於建立公用方法或工具方法。以下是使用靜態方法的範例:

class Math {
  static num max(num a, num b) {
    return (a > b) ? a : b;
  }

  static num min(num a, num b) {
    return (a < b) ? a : b;
  }
}

您可以在 main 函式中使用這些靜態方法:

print(Math.max(2, 3)); // 3
print(Math.min(2, 3)); // 2

然而,在 Dart 中,通常更好的是將這些公用方法放在自己的檔案中作為頂級函式。您可以將這些函式放在一個名為 math.dart 的檔案中:

num max(num a, num b) {
  return (a > b) ? a : b;
}

num min(num a, num b) {
  return (a < b) ? a : b;
}

然後,您可以在需要使用這些函式的檔案中匯入它們:

import 'package:starter/math.dart';

void main() {
  print(max(2, 3)); // 3
  print(min(2, 3)); // 2
}

如果您需要避免命名衝突,您可以使用 as 關鍵字:

import 'package:starter/math.dart' as math;

void main() {
  print(math.max(2, 3)); // 3
  print(math.min(2, 3)); // 2
}

建立新物件

您也可以使用靜態方法建立新物件。例如,您可以使用靜態方法實作與工廠建構函式相同的功能。以下是使用靜態方法建立新物件的範例:

class User {
  final int id;
  final String name;

  User({required this.id, required this.name});

  static User fromJson(Map<String, Object> json) {
    final userId = json['id'] as int;
    final userName = json['name'] as String;
    return User(id: userId, name: userName);
  }
}

您可以使用這個靜態方法建立新物件:

void main() {
  final json = {'id': 1, 'name': 'John'};
  final user = User.fromJson(json);
  print(user.id); // 1
  print(user.name); // John
}

Dart 中的靜態成員和空安全性

在 Dart 中,靜態成員是屬於類別而非例項的屬性或方法。靜態常數可用於儲存不變的值,例如 π。單例是一個只有單一例項的類別,而工具方法是與類別相關但不屬於任何例項的方法。

靜態成員

靜態成員使用 static 關鍵字宣告。靜態屬性和方法屬於類別本身,而非例項。例如:

class Sphere {
  static const double pi = 3.14159;

  final double radius;

  const Sphere({required this.radius});

  double get volume => (4/3) * pi * radius * radius * radius;
  double get surfaceArea => 4 * pi * radius * radius;
}

在上述例子中,pi 是一個靜態常數,volumesurfaceArea 是例項方法。

工廠建構函式

工廠建構函式是一種特殊的建構函式,傳回類別的例項或子類別的例項。工廠建構函式使用 factory 關鍵字宣告。例如:

class User {
  final int id;
  final String name;

  const User({required this.id, required this.name});

  factory User.fromJson(Map<String, dynamic> json) {
    final userId = json['id'] as int;
    final userName = json['name'] as String;
    return User(id: userId, name: userName);
  }
}

在上述例子中,User.fromJson 是一個工廠建構函式,傳回 User 的例項。

空安全性

空安全性是一種語言特性,確保變數不為空。Dart 2.12 引入了空安全性,允許開發者選擇是否啟用空安全性。

在空安全性啟用時,變數必須明確宣告為可空或不可空。例如:

int? postalCode; // 可空
int postalCode = 12345; // 不可空

如果變數宣告為可空,則必須使用 ? 來訪問其屬性或方法。例如:

int? postalCode;
print(postalCode?.toString()); // 如果 postalCode 不為空,則列印其字串表示

如果變數宣告為不可空,則無需使用 ? 來訪問其屬性或方法。例如:

int postalCode = 12345;
print(postalCode.toString()); // 列印其字串表示

什麼是 Null?

在程式設計中,Null 是一個特殊的值,代表「沒有值」或「空值」。它是一個有用的概念,可以用來表示某個變數或屬性尚未被初始化或沒有值。

Null 的問題

雖然 Null 是一個有用的概念,但它也可能導致一些問題。開發人員可能會忘記 Null 的存在,並沒有在程式碼中處理它。這可能導致程式崩潰或出現錯誤。

Dart 中的 Null

在 Dart 中,Null 是一個物件,實際上是 Null 類別的唯一例項。這意味著 Null 是一個有值的物件,但它沒有任何方法或屬性。

Dart 中的 Null 安全性

Dart 有一個名為 Null 安全性的功能,可以幫助開發人員避免 Null 相關的錯誤。這個功能可以確保變數或屬性不會被指定為 Null,除非它們被明確宣告為可為 Null。

可為 Null 的型別

在 Dart 中,型別可以被宣告為可為 Null 或不可為 Null。可為 Null 的型別以問號(?)結尾,例如 int?String?。不可為 Null 的型別則不以問號結尾,例如 intString

非可為 Null 的型別

非可為 Null 的型別是 Dart 中的預設型別。這意味著變數或屬性不會被指定為 Null,除非它們被明確宣告為可為 Null。

例子

以下是一些例子,展示瞭如何在 Dart 中使用可為 Null 和不可為 Null 的型別:

int myInt = 1; // 非可為 Null 的型別
int? myIntNullable = null; // 可為 Null 的型別

double myDouble = 3.14159265; // 非可為 Null 的型別
double? myDoubleNullable = null; // 可為 Null 的型別

bool myBool = true; // 非可為 Null 的型別
bool? myBoolNullable = null; // 可為 Null 的型別

String myString = 'Hello, Dart!'; // 非可為 Null 的型別
String? myStringNullable = null; // 可為 Null 的型別

Dart 中的可空型別

在 Dart 中,任何型別都可以是可空的,這意味著它可以包含 null 值。可空型別透過在型別名稱後新增一個問號 (?) 來表示。例如,int? 是一個可空的整數型別,String? 是一個可空的字串型別。

可空型別的特點

可空型別可以包含 null 值,這意味著你可以將 null 指定給一個可空變數。例如:

int? myInt = null;
double? myDouble = null;
bool? myBool = null;
String? myString = null;
User? myUser = null;

如果你不初始化一個可空變數,它將預設為 null

非可空型別與可空型別的關係

每個非可空型別都有一個對應的可空型別。例如,intint?boolbool?UserUser?。非可空型別是其可空型別的子型別,這意味著你可以將一個非可空值指定給一個可空變數。

處理可空型別

Dart 現在要求你必須處理可空型別的 null 值。你不能直接使用一個可空變數,直到你處理了 null 的可能性。例如:

String? name;
print(name); // 錯誤:不能使用可空變數

要處理可空型別,你需要使用 null-aware 運算子,例如 ???.

null-aware 運算子

?? 運算子用於提供一個預設值,當可空變數為 null 時使用。例如:

String? name;
String fullName = name ?? 'Unknown';
print(fullName); // 輸出:Unknown

?. 運算子用於訪問可空變數的屬性或方法,當可空變數為 null 時傳回 null。例如:

String? name;
String? uppercaseName = name?.toUpperCase();
print(uppercaseName); // 輸出:null

在 Dart 中,使用可空型別可以幫助你避免 null 值相關的錯誤。透過使用 null-aware 運算子,你可以安全地處理可空型別的 null 值。

程式碼示例

void main() {
  int? age;
  double? height;
  String? message;

  print(age); // 輸出:null
  print(height); // 輸出:null
  print(message); // 輸出:null

  String? profession;
  profession = 'basketball player';
  print(profession); // 輸出:basketball player

  const iLove = 'Dart';
  print(iLove); // 輸出:Dart
}

圖表翻譯

  graph LR
  A[可空型別] -->|包含|> B[null 值]
  A -->|不包含|> C[非 null 值]
  B -->|處理|> D[null-aware 運算子]
  C -->|使用|> E[變數值]

內容解密

在 Dart 中,使用可空型別可以幫助你避免 null 值相關的錯誤。透過使用 null-aware 運算子,你可以安全地處理可空型別的 null 值。可空型別可以包含 null 值,這意味著你可以將 null 指定給一個可空變數。非可空型別是其可空型別的子型別,這意味著你可以將一個非可空值指定給一個可空變數。

Dart 中的 Nullability

Dart 是一種靜態型別語言,意味著它可以在編譯時期就檢查出可能的錯誤。其中一個重要的功能是 nullability,它可以幫助你避免 null pointer exceptions。

Type Promotion

Dart 的編譯器可以自動推斷變數的型別,甚至可以將 nullable 型別提升為 non-nullable 型別。例如:

String? name;
name = 'Ray';
print(name.length);

在這個例子中,name 的型別是 String?,但因為它被指定為一個非 null 的字串,所以 Dart 可以自動將它提升為 String 型別。

Flow Analysis

Dart 的編譯器使用流分析(flow analysis)來檢查程式碼的每一個可能的路徑。如果沒有任何路徑會導致 null,則可以將 nullable 型別提升為 non-nullable 型別。例如:

bool isPositive(int? anInteger) {
  if (anInteger == null) {
    return false;
  }
  return !anInteger.isNegative;
}

在這個例子中,anInteger 的型別是 int?,但因為它已經被檢查為非 null,所以可以將它提升為 int 型別。

Null-Aware Operators

Dart 提供了一系列的 null-aware operators,可以幫助你處理可能為 null 的值。其中包括:

  • ??:如果 null,則使用右邊的值。
  • ??=:如果 null,則指定為右邊的值。
  • ?.:如果 null,則不呼叫方法或存取屬性。
  • ?.:如果 null,則不呼叫方法。
  • !:斷言值不為 null。
  • ?..:如果 null,則不呼叫方法或存取屬性。
  • ?[]:如果 null,則不存取陣列元素。
  • ...?:如果 null,則不展開陣列。

從程式碼設計和維護的視角來看,Dart 的建構子和靜態成員提供強大的機制來管理物件的建立和使用。深入分析命名建構子、工廠建構子、初始化列表和常數建構子,可以發現它們在提升程式碼可讀性、靈活性和安全性方面扮演著關鍵角色。 Dart 的空安全特性更進一步強化了這一點,透過可空型別和 null-aware 運算子,有效降低了 null pointer exceptions 的風險,提升了程式碼的穩定性。

分析 Dart 的型別提升和流分析機制,我們可以發現,Dart 編譯器能智慧地判斷變數的 nullability,並在安全的情況下自動將可空型別提升為非可空型別,這不僅簡化了程式碼的編寫,也提升了執行效率。同時,靜態成員和方法的應用,例如單例模式和工具方法的建立,有效地組織了程式碼結構,提升了程式碼的可重用性。然而,需要注意的是,過度使用單例模式可能會增加程式碼的耦合性,因此需要謹慎使用。

展望未來,隨著 Dart 生態系統的持續發展,預計會有更多工具和最佳實務出現,以進一步提升程式碼的品質和開發效率。 Dart 的空安全特性和型別推斷機制將持續演進,提供更強大的型別安全保障。同時,社群也將持續探索靜態成員和建構子的最佳實踐,以更好地利用這些特性構建更健壯、更易維護的應用程式。

玄貓認為,深入理解 Dart 的建構子和靜態成員,並結合空安全特性,是每個 Dart 開發者進階的必經之路。掌握這些核心概念,不僅能寫出更優雅、更安全的程式碼,也能提升整體的軟體設計能力。對於追求程式碼品質的開發者而言,持續學習和應用 Dart 的新特性和最佳實踐至關重要。