Dart 的空安全機制是其重要特性之一,旨在幫助開發者避免 Null Pointer Exception 的錯誤。透過區分可空與不可空的型別,Dart 編譯器能有效檢查程式碼中潛在的空值問題,提升程式碼的可靠性。本文將介紹 Dart 空安全的核心概念,包括空感知運運算元、延遲初始化、列表操作等,並輔以程式碼範例和圖表說明,幫助讀者掌握 Dart 空安全的使用技巧。

If-Null Operator

?? 運運算元可以用來提供一個預設值,如果左邊的值為 null。例如:

String? message;
final text = message ?? 'Error';

在這個例子中,如果 message 為 null,則 text 會被指定為 'Error'

Null-Aware Assignment Operator

??= 運運算元可以用來更新一個變數,如果它的值為 null。例如:

double? fontSize;
fontSize = fontSize ?? 20.0;

這個運運算元可以簡化為 fontSize ??= 20.0;

Dart 中的 Null 安全性

Dart是一種強型別的語言,為了避免 Null Pointer Exception,Dart 引入了 Null 安全性。Null 安全性是一種機制,確保變數不會為 Null,除非明確宣告為可 Null。

Null 感知指定運運算元(??=)

Null 感知指定運運算元(??=)是一種簡單的方式,來初始化變數時保證非 Null 值。例如:

double? fontSize;
fontSize ??= 20.0;

如果 fontSize 是 Null,則指定為 20.0,否則保持原值。

Null 感知存取運運算元(?.)

Null 感知存取運運算元(?.)是一種安全的方式,來存取物件的屬性或方法。如果左側的物件是 Null,則傳回 Null,否則傳回右側的屬性或方法。例如:

int? age;
print(age?.isNegative);

如果 age 是 Null,則不會丟擲 NoSuchMethodError,反而傳回 Null。

Null 斷言運運算元(!)

Null 斷言運運算元(!)是一種方式,來告訴 Dart 某個變數不會為 Null。例如:

String nonNullableString = myNullableString!;

注意,使用 Null 斷言運運算元可能會導致 Runtime Error,如果變數實際上是 Null。

Null 感知級聯運運算元(?..)

Null 感知級聯運運算元(?..)是一種方式,來安全地呼叫多個方法或設定多個屬性。例如:

class MyClass {
  void method1() {}
  void method2() {}
}

MyClass? myObject;
myObject?.method1();
myObject?.method2();

如果 myObject 是 Null,則不會丟擲 NoSuchMethodError,反而傳回 Null。

內容解密
  • Null 感知指定運運算元(??=)用於初始化變數時保證非 Null 值。
  • Null 感知存取運運算元(?.)用於安全地存取物件的屬性或方法。
  • Null 斷言運運算元(!)用於告訴 Dart 某個變數不會為 Null。
  • Null 感知級聯運運算元(?..)用於安全地呼叫多個方法或設定多個屬性。

圖表翻譯

  flowchart TD
    A[Null 安全性] --> B[Null 感知指定運運算元]
    B --> C[Null 感知存取運運算元]
    C --> D[Null 斷言運運算元]
    D --> E[Null 感知級聯運運算元]

這個流程圖描述了 Dart 中的 Null 安全性機制,包括 Null 感知指定運運算元、Null 感知存取運運算元、Null 斷言運運算元和 Null 感知級聯運運算元。

Dart 中的 Nullability

在 Dart 中,nullability 是一個重要的概念,指的是變數或屬性是否可以為 null。瞭解 nullability 有助於我們避免 null pointer exceptions 和寫出更安全的程式碼。

非空屬性

在 Dart 中,屬性可以是非空的,也可以是可空的。非空屬性必須在初始化時指定,否則會導致編譯錯誤。

class User {
  String name;
}

在上面的例子中,name 屬性是非空的,必須在初始化時指定。

可空屬性

可空屬性可以為 null,需要在宣告時加上 ? 符號。

class User {
  String? name;
}

在上面的例子中,name 屬性是可空的,可以為 null。

初始化非空屬性

有兩種方式可以初始化非空屬性:使用初始值和使用初始化形式。

使用初始值

class User {
  String name = 'anonymous';
}

在上面的例子中,name 屬性被初始化為 ‘anonymous’。

使用初始化形式

class User {
  User(this.name);
  String name;
}

在上面的例子中,name 屬性被初始化為建構函式的引數。

Null-Aware 運算子

Dart 提供了兩個 null-aware 運算子:?.?..

?. 運算子

?. 運算子用於存取可空物件的屬性或方法。如果物件為 null,則不會呼叫屬性或方法,直接傳回 null。

String? lengthString = user?.name?.length.toString();

在上面的例子中,如果 username 為 null,則 lengthString 會被指定為 null。

?.. 運算子

?.. 運算子用於可空物件的 cascade 運算。如果物件為 null,則不會執行 cascade 運算。

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

在上面的例子中,如果 user 為 null,則不會執行 nameid 的指定。

使用初始化列表

您也可以使用初始化列表來設定欄位變數:

class User {
  User(String name) : _name = name;
  String _name;
}

私有的 _name 欄位在建構函式被呼叫時將被保證獲得一個值。

使用預設引數值

可選引數如果沒有被設定,則預設為 null,因此對於非可為 null 的型別,您必須提供一個預設值。

您可以為位置引數設定預設值,如下所示:

class User {
  User([this.name = 'anonymous']);
  String name;
}

或對於具名引數:

class User {
  User({this.name = 'anonymous'});
  String name;
}

現在,即使在沒有任何引數的情況下建立物件,name 仍將至少具有預設值。

必需的具名引數

如第 7 章「函式」中所學,如果您想使具名引數成為必需的,請使用 required 關鍵字。

class User {
  User({required this.name});
  String name;
}

由於 name 是必需的,因此不需要提供預設值。

可為 Null 的例項變數

上述所有方法都保證類別欄位將被初始化,並且不僅初始化,而且初始化為非 null 值。由於欄位是非可為 null 的,因此甚至不可能犯以下錯誤:

final user = User(name: null);

Dart 不會允許您這樣做,您將會得到以下編譯時錯誤:

The argument type 'Null' can't be assigned to the parameter type 'String'

內容解密

在這個例子中,我們使用初始化列表和預設引數值來保證類別欄位被初始化。同時,我們也使用 required 關鍵字來使具名引數成為必需的。這些方法可以幫助我們避免 null 值的錯誤,並使程式碼更加安全。

圖表翻譯

  flowchart TD
    A[建立 User 物件] --> B[初始化欄位]
    B --> C[設定預設值]
    C --> D[檢查是否為 null]
    D --> E[如果為 null,丟擲錯誤]
    E --> F[如果不是 null,繼續執行]

這個流程圖展示了建立 User 物件的過程,包括初始化欄位、設定預設值、檢查是否為 null 等步驟。

Dart 中的 Nullability

在 Dart 中,nullability 是一個重要的概念,用於處理可能為 null 的變數。在本章中,我們將探討 Dart 中的 nullability,包括 nullable 型別、非本地變數的型別提升以及 late 關鍵字。

Nullable 型別

在 Dart 中,你可以使用 nullable 型別來表示一個變數可能為 null。例如:

class User {
  User({this.name});
  String? name;
}

在這個例子中,name 屬性是 nullable 的,這意味著它可以為 null。

非本地變數的型別提升

Dart 提供了型別提升的功能,可以自動將 nullable 變數提升為非 nullable 型別。但是,這個功能只適用於本地變數,而不適用於非本地變數。

例如:

bool isLong(String? text) {
  if (text == null) {
    return false;
  }
  return text.length > 100;
}

在這個例子中,text 變數是本地的,Dart 可以保證它在訪問 length 屬性之前不會為 null,因此可以將其提升為非 nullable 型別。

但是,如果我們將這個例子修改為非本地變數,如下所示:

class TextWidget {
  String? text;

  bool isLong() {
    if (text == null) {
      return false;
    }
    return text.length > 100; // error
  }
}

Dart 編譯器會報錯,因為它不能保證 text 變數在訪問 length 屬性之前不會為 null。

解決方案

有兩個解決方案可以解決這個問題:

  1. 使用 ! 運算子:
bool isLong() {
  if (text == null) {
    return false;
  }
  return text!.length > 100;
}
  1. 使用區域性變數 shadowing:
class TextWidget {
  String? text;

  bool isLong() {
    final text = this.text; // shadowing

    if (text == null) {
      return false;
    }
    return text.length > 100;
  }
}

Late 關鍵字

在某些情況下,你可能想要使用非 nullable 型別,但不能在建構函式中初始化它。在這種情況下,你可以使用 late 關鍵字來延遲初始化變數。

例如:

class User {
  User(this.name);
  final String name;
  final int _secretNumber = _calculateSecret();
}

在這個例子中,_secretNumber 變數是使用 late 關鍵字延遲初始化的。

在本章中,我們探討了 Dart 中的 nullability,包括 nullable 型別、非本地變數的型別提升以及 late 關鍵字。我們還學習瞭如何使用 ! 運算子和區域性變數 shadowing 來解決非本地變數的型別提升問題。

Dart 中的 Nullability 和 Lazy Initialization

在 Dart 中,nullability 是一個重要的概念,指的是變數是否可以為 null。Dart 提供了多種方式來處理 nullability,包括使用 ? 來表示可為 null 的變數、使用 ! 來表示不可為 null 的變數,以及使用 late 來延遲初始化變數。

使用 late 來延遲初始化變數

late 是 Dart 中的一個關鍵字,用於延遲初始化變數。當你使用 late 來初始化一個變數時,Dart 不會立即初始化它,而是等到你第一次訪問它時才會初始化。

class User {
  late final int _secretNumber;

  int _calculateSecret() {
    return 42;
  }

  User() {
    _secretNumber = _calculateSecret();
  }
}

在上面的例子中, _secretNumber 是一個 late 初始化的變數,它的值是在 User 類的建構函式中計算出來的。

使用 late 的風險

使用 late 來延遲初始化變數有一定的風險。如果你沒有在使用變數之前初始化它,Dart 會丟擲一個 LateInitializationError

class User {
  late String name;
}

void main() {
  final user = User();
  print(user.name); // 會丟擲 LateInitializationError
}

在上面的例子中, name 是一個 late 初始化的變數,但它沒有被初始化,所以當你試圖訪問它時,Dart 會丟擲一個 LateInitializationError

使用 late 的好處

使用 late 來延遲初始化變數也有好處。如果你有一個變數需要進行昂貴的計算來初始化,但你不確定是否需要使用它,使用 late 可以避免不必要的計算。

class SomeClass {
  late String? value = doHeavyCalculation();

  String? doHeavyCalculation() {
    // 進行昂貴的計算
  }
}

在上面的例子中, value 是一個 late 初始化的變數,它的值是在 doHeavyCalculation 函式中計算出來的。但是,如果你從來沒有訪問過 valuedoHeavyCalculation 函式就不會被呼叫,從而避免了不必要的計算。

練習題

  1. 建立一個 Name 類,包含 givenNamesurname 屬性。
  2. 新增一個 surnameIsFirst 屬性來表示姓氏是否放在名字之前。
  3. 新增一個 toString 方法來傳回全名。
class Name {
  final String givenName;
  final String? surname;
  final bool surnameIsFirst;

  Name({required this.givenName, this.surname, this.surnameIsFirst = false});

  @override
  String toString() {
    if (surname == null) {
      return givenName;
    } else if (surnameIsFirst) {
      return '$surname $givenName';
    } else {
      return '$givenName $surname';
    }
  }
}

Dart 中的 Null 安全性和列表

Dart 2.12 引入了 null 安全性,這是一個重要的特性,可以幫助開發者避免 null 相關的錯誤。null 安全性可以區分可空和不可空的型別,確保不可空的型別永遠不會是 null。

Null 安全性運算子

Dart 提供了多個 null 安全性運算子,包括:

  • ??:如果-null 運算子
  • ??=:null-aware 指派運算子
  • ?.:null-aware 存取運算子
  • ?.:null-aware 方法呼叫運算子
  • !:null 斷言運算子
  • ?..:null-aware 連鎖運算子
  • ?[]:null-aware 索引運算子
  • ...?:null-aware 展開運算子

延遲初始化

Dart 的 late 關鍵字可以用於延遲初始化一個欄位。使用 late 關鍵字可以使初始化變得懶惰,這意味著變數的值只有在第一次存取時才會被計算。

列表

列表是 Dart 中的一種重要的集合型別。列表可以用於儲存多個相同型別的物件,並且可以進行排序。Dart 中的列表可以透過列表字面量建立,例如:

var desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];

列表可以進行修改和存取其元素。例如,可以使用 add 方法新增元素,使用 remove 方法刪除元素。

列表的基本操作

列表提供了多種基本操作,包括:

  • 建立列表
  • 修改列表
  • 存取列表元素

列表的建立

列表可以透過列表字面量建立,也可以使用 List 類別建立。例如:

var desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
var snacks = <String>[];

列表的修改

列表可以進行修改,例如新增或刪除元素。例如:

desserts.add('cake');
desserts.remove('cupcakes');

列表的存取

列表可以存取其元素,例如:

print(desserts[0]); // 輸出:cookies
內容解密

上述程式碼示範瞭如何建立和修改列表,包括新增和刪除元素。列表可以透過列表字面量建立,也可以使用 List 類別建立。列表可以進行修改和存取其元素。

圖表翻譯

  graph LR
    A[建立列表] --> B[修改列表]
    B --> C[存取列表元素]
    C --> D[新增元素]
    D --> E[刪除元素]
    E --> F[存取元素]

上述圖表示範了列表的基本操作,包括建立、修改和存取列表元素。

列表的基本操作

在 Dart 中,列表(List)是一種常用的資料結構,允許您儲存多個元素。下面我們將探討如何存取列表中的元素、修改列表元素的值以及列印列表。

存取列表元素

要存取列表中的元素,您可以使用索引(Index)來參照它。索引是從 0 開始的,這意味著第一個元素的索引是 0,第二個元素的索引是 1,依此類推。

void main() {
  List<String> desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
  String secondElement = desserts[1];
  print(secondElement); // 輸出: cupcakes
}

在上面的例子中,desserts[1] 傳回列表中的第二個元素,即 'cupcakes'

使用 indexOf 方法

如果您知道元素的值,但不知道其索引,您可以使用 indexOf 方法來查詢其索引。

void main() {
  List<String> desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
  int index = desserts.indexOf('pie');
  String value = desserts[index];
  print('The value at index $index is $value.'); // 輸出: The value at index 3 is pie.
}

在上面的例子中,desserts.indexOf('pie') 傳回 'pie' 的索引,即 3

修改列表元素的值

您可以使用索引來修改列表元素的值。

void main() {
  List<String> desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
  desserts[1] = 'cake';
  print(desserts); // 輸出: [cookies, cake, donuts, pie]
}

在上面的例子中,desserts[1] = 'cake' 修改了列表中的第二個元素的值為 'cake'

列印列表

您可以使用 print 函式來列印列表。

void main() {
  List<String> desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
  print(desserts); // 輸出: [cookies, cupcakes, donuts, pie]
}

在上面的例子中,print(desserts) 列印了列表中的所有元素。

內容解密

  • 列表的索引是從 0 開始的。
  • 您可以使用 indexOf 方法來查詢元素的索引。
  • 您可以使用索引來修改列表元素的值。
  • 您可以使用 print 函式來列印列表。

圖表翻譯

  flowchart TD
    A[列表] --> B[索引]
    B --> C[存取元素]
    C --> D[修改元素值]
    D --> E[列印列表]

在上面的圖表中,我們展示了列表的基本操作,包括存取元素、修改元素值和列印列表。

列表元素的新增和刪除

在 Dart 中,列表是可擴充的,因此您可以使用 add 方法新增元素到列表的末端。例如:

void main() {
  List<String> desserts = ['cookies', 'cake', 'donuts', 'pie'];
  desserts.add('brownies');
  print(desserts);
}

這會輸出:[cookies, cake, donuts, pie, brownies]

如果您想在列表中間新增元素,可以使用 insert 方法。例如:

void main() {
  List<String> desserts = ['cookies', 'cake', 'donuts', 'pie', 'brownies'];
  desserts.insert(1, 'ice cream');
  print(desserts);
}

這會輸出:[cookies, ice cream, cake, donuts, pie, brownies]

如果您想刪除列表中的元素,可以使用 remove 方法。例如:

void main() {
  List<String> desserts = ['cookies', 'ice cream', 'cake', 'donuts', 'pie', 'brownies'];
  desserts.remove('cake');
  print(desserts);
}

這會輸出:[cookies, ice cream, donuts, pie, brownies]

如果您知道要刪除的元素的索引,可以使用 removeAt 方法。例如:

void main() {
  List<String> desserts = ['cookies', 'ice cream', 'donuts', 'pie', 'brownies'];
  desserts.removeAt(0);
  print(desserts);
}

這會輸出:[ice cream, donuts, pie, brownies]

內容解密

在上述範例中,我們使用 add 方法新增元素到列表的末端,使用 insert 方法新增元素到列表中間,使用 remove 方法刪除列表中的元素,使用 removeAt 方法刪除列表中指定索引的元素。

圖表翻譯

  flowchart TD
    A[新增元素] --> B[add 方法]
    B --> C[列表末端新增元素]
    A --> D[insert 方法]
    D --> E[列表中間新增元素]
    A --> F[刪除元素]
    F --> G[remove 方法]
    G --> H[刪除列表中的元素]
    F --> I[removeAt 方法]
    I --> J[刪除列表中指定索引的元素]

在這個圖表中,我們展示了新增和刪除列表元素的流程。新增元素可以使用 add 方法新增到列表末端,也可以使用 insert 方法新增到列表中間。刪除元素可以使用 remove 方法刪除列表中的元素,也可以使用 removeAt 方法刪除列表中指定索引的元素。

Dart 中的列表排序和操作

在 Dart 中,列表(List)是一種常用的資料結構,用於儲存多個元素。當您需要對列表中的元素進行排序或插入、刪除操作時,Dart 提供了多種方法。

從技術架構視角來看,Dart 的 Null 安全性機制,包括可空型別、Null 感知運算子和 late 關鍵字,有效地解決了 null pointer exceptions 的問題,提升了程式碼的健壯性。分析 Dart 的 Null 安全特性可以發現,它允許開發者更精確地控制變數的可空性,並提供工具來安全地處理可能為 null 的值。然而,對於非本地變數的型別提升的限制,仍需額外處理,例如使用 ! 運算子或區域性變數 shadowing。展望未來,隨著 Dart 的持續發展,預計 Null 安全機制將更加完善,並與其他語言特性更好地整合,進一步提升開發效率和程式碼品質。對於追求程式碼品質的開發者而言,深入理解和應用 Dart 的 Null 安全機制至關重要。