- 泛型型別、函式泛型參數
class T<_> {}
void genericFunction<_>() {}
takeGenericCallback(<_>() => true);
Code language: JavaScript (javascript)
- 函式參數
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 的絕對值。如果 i 是 null,執行 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 原生支援的屬性或方法(hashCode、toString()),則允許直接呼叫。
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 修飾符有兩種適用場景:
- 宣告不可空變數,後續程式再進行賦值;
- 實現變數延遲初始化。
多數情況下,Dart 的流程分析能判斷某個不可空變數在使用前一定會被賦予非空值。但有兩種常見場景分析會失效:頂層變數與實例變數,編譯器無法自動確認是否完成賦值,因此會報錯。
如果你能確認變數在取用前一定會賦值,但編譯器無法自動判斷,就能在變數前方加上 late 修飾來消除錯誤提示:
以下範例的 description 是全域變數,編譯器無法自動辨識是否會賦值,若你確定一定會先賦值再使用,就可加上 late 修飾
late String description;
void main() {
description = 'Feijoada!';
print(description);
}
重要提醒
若宣告
late變數卻從未賦值,後續程式存取該變數時會拋出執行階段例外。
如果宣告 late 變數時直接給予初始值,這段初始化程式只會在變數第一次被存取時執行。這種延遲初始化在兩種場景非常實用:
- 該變數有可能全程不會被取用,且初始化運算耗費大量資源;
- 初始化實例變數時,初始化邏輯需要存取
this。
下方範例中,如果全程沒有使用 temperature,資源消耗較高的 readThermometer() 函式永遠不會執行:
// 本程式只有在存取 temperature 時才會呼叫 readThermometer()
late String temperature = readThermometer(); // 延遲初始化
final 與 const
如果不希望變數數值被覆寫修改,可以使用 final 或 const;兩者可單獨使用,也能搭配型別標註,取代 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]; // 錯誤:常數變數不可重新賦值
定義常數時,可以搭配型別判斷、型別轉換(is、as)、集合 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、第3個數值,第二個使用 _ 捨棄
final [a, _, c] = [10, 20, 30];
print(a); // 10
print(c); // 30
// print(_); // 報錯,禁止讀取萬用字元
頂層宣告、會影響程式庫私有存取權限的類別成員,不允許使用萬用字元變數。僅區塊作用域內的宣告可使用,範例如下:
- 區域變數宣告
void main() {
var _ = 1;
int _ = 2;
}
- for 迴圈變數
for (var _ in list) {}
- catch 捕捉例外參數
try {
throw '!';
} catch (_) {
print('oops');
}
- 泛型型別、函式泛型參數
class T<_> {}
void genericFunction<_>() {}
takeGenericCallback(<_>() => true);
- 函式參數
Foo(_, this._, super._, void _()) {}
list.where((_) => true);
void f(void g(int _, bool _)) {}
typedef T = void Function(String _, String _);