Dart 變數

  1. 泛型型別、函式泛型參數
class T<_> {}
void genericFunction<_>() {}

takeGenericCallback(<_>() => true);
Code language: JavaScript (javascript)
  1. 函式參數
Foo(_, this._, super._, void _()) {}

list.where((_) => true);

void f(void g(int _, bool _)) {}

typedef T = void Function(String _, String _);Code language: JavaScript (javascript)

接下來學習 Dart 當中的變數。以下是建立並初始化變數的範例:

var name = 'Jack';

變數實際存放的是參考位址,名為 name 的變數內儲存一筆參考,該參考指向內容為 “Jack” 的字串物件。

所謂參考就是記憶體位址;為了方便電腦在記憶體存放資料,每一塊記憶體區塊都會標註獨立位址,概念就跟家門牌號碼相同。

編譯器會自動推斷 name 的型別為 String,你也可以手動標註型別來覆蓋自動推論結果。若變數需要存放多種不同型別的物件,可將型別宣告為 Object(必要時也能使用 dynamic)。

Object name = 'Jack';

另一種寫法是明確指定型別(例如我們確認內容是文字,就直接標註字串 String):

String name = 'Jack';

Null Safety 空值安全

空值安全能夠避免誤存取值為 null 的變數所引發的錯誤,這類錯誤稱作空參考錯誤(null dereference error)。當你對運算結果為 null 的運算式存取屬性或呼叫方法時,就會觸發空參考錯誤。

有一個例外狀況:若 null 本身原生支援該屬性或方法(例如 toString()hashCode),則不會報錯。透過空值安全機制,Dart 編譯器會在編譯階段就偵測出這類潛在錯誤。

有開發經驗的讀者應該都知道,空參考錯誤是極常見的程式問題。如果能在編譯階段就提前攔截這類錯誤,程式的穩定性與健壯性會大幅提升。

舉例說明:假設你要取得 int 型別變數 i 的絕對值。如果 inull,執行 i.abs() 就會觸發空解參考錯誤。其他程式語言只會在程式執行時才爆出錯誤,但 Dart 編譯器會直接禁止這類非法呼叫,提前阻擋問題。

空值安全帶來三項核心調整:

1 可空型別,宣告型別時可以控制該型別是否允許存入空值,只需要在型別後方加上問號即可。

String? name  // 可空型別:數值可以是 null 或是一般字串
String name   // 不可空型別:不允許為 null,只能存放字串

部分程式語言當中,string name 預設都能存放 null,但在 Dart 當中不允許;如果要儲存空值,必須使用 string? 這種可空型別。

2 變數使用前必須完成初始化。可空變數預設值即為 null,等同自動完成初始化;不可空型別沒有預設值,強制開發者手動賦予初始值。Dart 禁止存取未初始化的變數,藉此杜絕一類常見錯誤。

void main() {
  int year;        // 錯誤:未初始化的不可空變數
  print(year);     // 編譯錯誤:year 尚未賦值
}

上方程式執行編譯會直接報錯

C:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:7:9: Error: Non-nullable variable 'year' must be assigned before it can be used.
  print(year);     // 編譯錯誤:year 尚未賦值
        ^^^^

如果是可空變數則允許以下寫法

void main() {
  int? age;        // 可空變數,預設值為 null
  print(age);      // 輸出 null,不會觸發錯誤
  
  //age = 18;        // 手動賦值
  //print(age);      // 輸出 18
}

3 不可直接對可空型別的運算式存取屬性、呼叫方法。同樣有例外:若為 null 原生支援的屬性或方法(hashCodetoString()),則允許直接呼叫。

void main() {
  String? name; // 可空型別,預設值為 null

  // 錯誤:直接存取 length 會報錯
  // print(name.length);

  // 合法:允許呼叫 null 原生支援的方法
  print(name.toString());  // 輸出 "null"
  print(name.hashCode);    // 輸出一組整數

  // 合法:使用空值安全運算子
  print(name?.length);     // 輸出 null,不會報錯
}

預設值

沒有手動賦值的可空變數,初始預設值一律為 null。就算是數字型別的變數,預設值同樣是 null

因為在 Dart 裡,數字與其他所有資料本質上都屬於物件。

void main() {
  int? lineCount;
  assert(lineCount == null);
}

注意:正式上線的生產環境會忽略 assert() 檢查;但開發階段如果 assert(條件) 內的判斷不成立,程式會直接拋出例外。

開啟空值安全後,不可空變數在使用前必須完成初始化:

int lineCount = 0;

區域變數不一定要在宣告同一行賦值,但必須在取用前完成賦值。下方程式屬於合法寫法,因為 Dart 流程分析能判斷執行 print() 時,lineCount 一定已存入非空數值:

int lineCount;

if (weLikeToCount) {
  lineCount = countLines();
} else {
  lineCount = 0;
}

print(lineCount);

補充說明:上面程式當中 lineCount = countLines(); 與 lineCount = 0; 僅是對變數賦值,不算「使用」;所謂使用指讀取變數內存放的數值。舉例下方程式就會觸發編譯錯誤

void main() {
	int lineCount;

	print(lineCount);
}

編譯失敗提示

頂層變數與類別成員變數屬於延遲初始化:只有第一次被取用時,才會執行賦值邏輯。

late 修飾變數

late 修飾符有兩種適用場景:

  1. 宣告不可空變數,後續程式再進行賦值;
  2. 實現變數延遲初始化。

多數情況下,Dart 的流程分析能判斷某個不可空變數在使用前一定會被賦予非空值。但有兩種常見場景分析會失效:頂層變數與實例變數,編譯器無法自動確認是否完成賦值,因此會報錯。

如果你能確認變數在取用前一定會賦值,但編譯器無法自動判斷,就能在變數前方加上 late 修飾來消除錯誤提示:

以下範例的 description 是全域變數,編譯器無法自動辨識是否會賦值,若你確定一定會先賦值再使用,就可加上 late 修飾

late String description;

void main() {
  description = 'Feijoada!';
  print(description);
}

重要提醒

若宣告 late 變數卻從未賦值,後續程式存取該變數時會拋出執行階段例外。

如果宣告 late 變數時直接給予初始值,這段初始化程式只會在變數第一次被存取時執行。這種延遲初始化在兩種場景非常實用:

  1. 該變數有可能全程不會被取用,且初始化運算耗費大量資源;
  2. 初始化實例變數時,初始化邏輯需要存取 this

下方範例中,如果全程沒有使用 temperature,資源消耗較高的 readThermometer() 函式永遠不會執行:

// 本程式只有在存取 temperature 時才會呼叫 readThermometer()
late String temperature = readThermometer(); // 延遲初始化

final 與 const

如果不希望變數數值被覆寫修改,可以使用 finalconst;兩者可單獨使用,也能搭配型別標註,取代 var

  • final 變數僅能賦值一次;
  • const 變數屬於編譯期常數(const 變數隱含 final 特性)。

注意

類別實例成員變數可使用 final 修飾,但不能使用 const;類別層級常數請使用 static const

以下為定義 final 變數的範例:

final name = 'Jack'; // 不標註型別
final String nickname = 'Jason';

你無法重新修改 final 變數的數值,編譯器會直接報錯:

void main() {
	final name = 'Jack'; // 不標註型別
	final String nickname = 'Jason';
	
	name = 'Alice'; // 錯誤:final 變數僅允許賦值一次
}
C:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:6:2: Error: Can't assign to the final variable 'name'.
        name = 'Alice'; // 錯誤:final 變數僅允許賦值一次

const 用於宣告編譯期常數。若常數定義在類別內部,必須加上 static 修飾寫成 static const。宣告 const 變數時,賦值內容必須是編譯階段就能確定的數值,例如數字、字串字面量、其他 const 變數,或是常數數值參與四則運算的結果:

const bar = 1000000; // 壓力單位(達因/平方公分)
const double atm = 1.01325 * bar; // 標準大氣壓

const 關鍵字不只能宣告常數變數,也能用來建立常數物件,或是定義可產生常數實例的建構函式。一般變數也能接收常數物件做為數值。

var foo = const [];
final bar = const [];
const baz = []; // 等同 const []

const 變數的賦值運算式中可以省略 const 關鍵字,如同上方 baz 的寫法。

就算變數曾經接收常數物件,只要該變數不是 final、也不是 const,就能更換它指向的參考:

foo = [1, 2, 3]; // foo 原本指向常數空陣列,現在可更換參考

const 變數不允許重新賦值,編譯器會報錯:

// 靜態檢查:程式錯誤
baz = [42]; // 錯誤:常數變數不可重新賦值

定義常數時,可以搭配型別判斷、型別轉換(isas)、集合 if、展開運算子(......?):

void main() {
	const Object i = 3; // i 是 Object 型別常數,內部存放 int 數值
	const list = [i as int]; // 使用型別轉換
	const map = {if (i is int) i: 'int'}; // 使用 is 判斷與集合if
	const set = {if (list is List<int>) ...list}; // 搭配展開運算子
}

注意

final 修飾的物件本身參考無法更動,但物件內部的欄位可以修改;相對地,const 修飾的物件連同內部所有資料都不可變更,屬於完全不可變物件。

想了解更多使用 const 建立常數集合的內容,可參考陣列、映射、類別相關教學章節。

*萬用字元變數

版本規格:萬用字元變數要求 Dart 語言版本至少 3.7。

_ 命名的萬用字元變數屬於無綁定區域變數/參數,本質僅做佔位用途。若帶有初始化運算式,該運算式依舊會執行,但你無法讀取此變數存放的數值。同一作用域內可宣告多個名稱為 _ 的變數,不會發生命名衝突。

什麼是萬用字元變數

Dart 當中的單一下底線 _ 即為萬用字元變數:

  1. 用來接收不需要使用的數值,告知編譯器「這個變數我不會讀取、不會取用」;
  2. 編譯器不會彈出「變數宣告未使用」的警告;
  3. 不能讀取 _(讀取會直接報錯),只能做佔位捨棄資料。
// 只取第1、第3個數值,第二個使用 _ 捨棄
final [a, _, c] = [10, 20, 30];
print(a); // 10
print(c); // 30
// print(_); // 報錯,禁止讀取萬用字元

頂層宣告、會影響程式庫私有存取權限的類別成員,不允許使用萬用字元變數。僅區塊作用域內的宣告可使用,範例如下:

  1. 區域變數宣告
void main() {
  var _ = 1;
  int _ = 2;
}
  1. for 迴圈變數
for (var _ in list) {}
  1. catch 捕捉例外參數
try {
  throw '!';
} catch (_) {
  print('oops');
}
  1. 泛型型別、函式泛型參數
class T<_> {}
void genericFunction<_>() {}

takeGenericCallback(<_>() => true);
  1. 函式參數
Foo(_, this._, super._, void _()) {}

list.where((_) => true);

void f(void g(int _, bool _)) {}

typedef T = void Function(String _, String _);

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *