Dart - 語法
線上練習網站 DartPad
跟 JavaScript 不一樣,Dart 句尾一定要加 ;
Hello World in Dart
void main() {
print('Hello, World!');
}
The main()
function
每個 Dart app 都會需要一個頂層 main()
函式作為 app 的入口點 (entrypoint),所有的執行邏輯都從 main()
函式開始。
定義變數
在 Dart 中,定義變數可以使用 var
,這時 Dart 會自動去推論變數型態:
var name = 'Jeremy';
print(name);
也可以透過主動指定給予變數型態:
String name = 'Jeremy';
Null safety
String? name // Nullable type. Can be `null` or string.
String name // Non-nullable type. Cannot be `null` but can be string.
Late variables
主要用途有兩個:
- 聲明一個非空變數,但這個變數會在聲明之後初始化。
late String description;
void main() {
description = 'Feijoada!';
print(description);
}
- 懶加載變數,通常用在:
- 當變數在某些情況下可能不會被使用時,避免不必要的初始化來節省資源。
- 初始化實例變數,並且初始化過程需要訪問
this
:在類的實例變數初始化過程中,如果需要訪問實例本身(例如訪問其他屬性或方法),使用late
可以確保變數在第一次使用時才初始化,並且此時實例已經完全構建。
// readThermometer() 函數只有在 temperature 變數第一次被使用時才會被調用
late String temperature = readThermometer(); // Lazily initialized.
final
and const
final
跟 const
用在變數的值不會再改變時,或者換成 JS 的說法就是宣告常數。
const
跟 final
最大的差別在於 const
是編譯時變數,即它的值在程式執行前就已經確定,並且不能在運行時更改。
void main() {
// 使用 final
final String runtimeValue = DateTime.now().toString(); // 運行時初始化
print(runtimeValue);
// 使用 const
const int compileTimeValue = 100; // 編譯時初始化
print(compileTimeValue);
}
如果是在定義 class
時要使用 const
定義常數,必須使用 static const
:
class Example {
static const int MAX_COUNT = 10; // 類級別的常量
}
根據官方說法。const
還有以下用法:
var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`
雖然同樣是定義一個常數,但 const
寫在等號左邊是定義一個變數為常數;const
寫在右邊則是定義變數的值為常數,但這個值是可以被覆蓋的:
foo = [1, 2, 3]; // 這是可以的
baz = [42]; // 這會報錯
Libraries & imports
// simple import
import 'package:lib1/lib1.dart';
// specifying a library prefix
import 'package:lib2/lib2.dart' as lib2;
// Importing only part of a library
// Import only foo
import 'package:lib1/lib1.dart' show foo;
// Import all names EXCEPT foo
import 'package:lib2/lib2.dart' hide foo;
// Lazily loading a library
import 'package:greetings/hello.dart' deferred as hello;
判別式
if else
跟 JavaScript 基本長一樣:
var num = 5;
if (num > 10) {
print('The Number > 10');
} else if (num == 10) {
print('The Number == 10');
} else {
print('The Number < 10');
}
if-case
這是一個 JavaScript 跟 Python 沒有的東西,是 Dart 為了提供更強大的模式匹配能力衍生出的功能:
if (pair case [int x, int y]) return Point(x, y);
什麼是模式匹配? 模式匹配用於對數據進行結構化的匹配和提取,這意味著可以根據數據的結構和特徵來決定該如何處理它們。
舉個生活的例子:
假設你有一堆電子郵件,你想找出其中的廣告郵件。你的模式(或條件)是郵件的標題中包含特價這個詞。
你遍歷每封郵件,將標題與這個模式進行比較。如果標題包含特價,你就將這 封郵件放到一個文件夾中,否則,你就將它放到另一個文件夾中。
什麼是 patterns (模式)?
在 Dart 中模式是一個泛稱,例如在 switch
語句中進行值的匹配、在變量聲明中進行解構、在條件表達式中進行匹配等都可以稱作一種模式。
Matching:
switch (number) {
// Constant pattern matches if 1 == number.
case 1:
print('one');
}
Destructuring:
var numList = [1, 2, 3];
// List pattern [a, b, c] destructures the three elements from numList...
var [a, b, c] = numList;
// ...and assigns them to new variables.
print(a + b + c);
switch
基本用法:
var command = 'OPEN';
switch (command) {
case 'CLOSED':
executeClosed();
case 'PENDING':
executePending();
case 'APPROVED':
executeApproved();
case 'DENIED':
executeDenied();
case 'OPEN':
executeOpen();
default:
executeUnknown();
}
特殊用法:
switch (command) {
case 'OPEN':
executeOpen();
continue newCase; // 在 newCase 標籤處繼續執行。
case 'DENIED': // 空 case 會繼續執行下一個 case。
case 'CLOSED':
executeClosed(); // 適用於 DENIED 和 CLOSED 兩種情況。
newCase:
case 'PENDING':
executeNowClosed(); // 適用於 OPEN 和 PENDING 兩種情況。
}
switch expressions
switch expressions 允許根據表達式匹配的情況,產生一個值:
var x = switch (y) { ... };
print(switch (x) { ... });
return switch (x) { ... };
實際的應用例子如下:
token = switch (charCode) {
slash || star || plus || minus => operator(charCode),
comma || semicolon => punctuation(charCode),
>= digit0 && <= digit9 => number(),
_ => throw FormatException('Invalid')
};
這個 switch expression 根據 charCode 的值進行匹配,並根據不同的情況返回相應的結果。
如果 charCode 是斜線、星號、加號或減號,則返回 operator(charCode) 的結果。
如果是逗號或分號,則返回 punctuation(charCode) 的結果。
如果是數字字符,則返回 number() 的結果。
如果都不符合,則拋出一個異常。
基本上 switch expressions 就是 switch 衍伸的產物 (應該能算是 switch 的語法糖),上述的例子可以用 switch 改寫:
// Where slash, star, comma, semicolon, etc., are constant variables...
switch (charCode) {
case slash || star || plus || minus: // Logical-or pattern
token = operator(charCode);
case comma || semicolon: // Logical-or pattern
token = punctuation(charCode);
case >= digit0 && <= digit9: // Relational and logical-and patterns
token = number();
default:
throw FormatException('Invalid');
}
Exhaustiveness checking (徹底檢查)
徹底檢查會在編譯時檢查 switch 語句中是否存在缺失的 case,並在無法匹配的情況下報告錯誤。
基本上在 switch 中加入預設情況 (default
or _
) 即可達到徹底檢查,或是使用 Enums 或 Sealed 型別亦有同樣的檢查效果。
迴圈
for loop
下列是一個簡單的 for loop 例子:
for (int num = 0; num < 10; num++){
print(num);
}
int
是宣告 num 為一個整數
紀錄一下官方的 for loop 例子,因為裡面用到了一些初學 Dart 時我還沒學到的方法:
void main() {
var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
message.write('!');
}
print(message); // Dart is fun!!!!!
}
Dart 的閉包處理
var callbacks = [];
for (var i = 0; i < 2; i++) {
callbacks.add(() => print(i));
}
for (final c in callbacks) {
c();
}
// 0
// 1
寫過 JavaScript 的應該都知道下面這兩段 code 的差別:
// 使用 var
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 輸出: 5, 5, 5, 5, 5
// 使用 let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 輸出: 0, 1, 2, 3, 4
基本可以想像成:Dart 的 for loop 閉包處理就像是 JS 的 let 產生出來解決變數汙染的問題。
用 for-in loop 遍歷 List 或 Set
// 遍歷 candidates 列表中的每個 candidate 對象,並對每個 candidate 執行 interview() 方 法
for (final candidate in candidates) {
candidate.interview();
}
// 從 candidates 列表中提取每個 candidate 對象的名稱和工作經驗年數,並將它們打印出來
for (final Candidate(:name, :yearsExperience) in candidates) {
print('$name has $yearsExperience of experience.');
}
用 forEach 遍歷
var collection = [1, 2, 3];
collection.forEach(print); // 1 2 3
while loop
下述是基本的 while loop 用法:
var num = 1;
while (num < 10) {
print(num);
num++;
}
Dart 還有一個特殊的 do-while
,但其實就是將條件寫在迴圈要做的事之後:
var num = 1;
do {
print(num);
num++;
} while (num < 10);
Dart 的迴圈跟 Python 一樣都有 break
跟 continue
來中斷迴圈或中斷目前迴圈進入下一個迴圈的功能。
函式
下列為簡單函式範例:
// 不回傳值
sayHello () {
print('Hello, world!');
}
// 回傳值,故給予 int 定義回傳值型態
int sumNum (int a, int b) {
return a + b;
}
// 上述函式簡寫
int shortSumNum (int a, int b) => a + b;
void main() {
// 呼叫第一個函式
sayHello();
// 呼叫第二個函式
var resultSum = sumNum(1, 2);
print(resultSum);
// 呼叫第三個函式
var resultShortSum = shortSumNum(2, 3);
print(resultShortSum);
}
Named parameters
Dart 中定義命名參數是使用 {param1, param2, …}
。
命名參數如果未被標記為 required 則同時也是可選參數。
// 定義命名參數函式
void enableFlags({bool? bold, bool? hidden}) {...}
// 使用函式
enableFlags(bold: true, hidden: false);
也可以在定義函式時先給予命名參數預設值:
void enableFlags({bool bold = false, bool hidden = false}) {...}
// bold will be true; hidden will be false.
enableFlags(bold: true);
也可以添加 required 使之變為必須參數:
void enableFlags({required bool bold, bool? hidden}) {...}
Optional positional parameters
可選位置參數在定義函數時使用 []
包裹,調用函數時按參數順序傳遞值。
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
void main() {
// 不傳入可選位置參數
print(say('Bob', 'Howdy')); // Bob says Howdy
// 傳入可選位置參數
print(say('Bob', 'Howdy', 'smoke signal')); // Bob says Howdy with a smoke signal
}
同樣也可以給予預設值:
String say(String from, String msg, [String device = 'carrier pigeon']) {
var result = '$from says $msg with a $device';
return result;
}
把函式作為參數
例子 1:
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
void main() {
list.forEach(printElement);
}
例子 2:
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
void main() {
print(loudify('Hello'));
}
Anonymous functions (匿名函式)
一個匿名函式的長相大概像這樣:
([[Type]] param1[, ...]]) {
codeBlock;
}
實際應用上如官方範例的 (item) {...}
就是匿名函式:
void main() {
const list = ['apples', 'bananas', 'oranges'];
// (item) {...} 是個匿名函式
var uppercaseList = list.map((item) {
return item.toUpperCase();
}).toList();
for (var item in uppercaseList) {
print('$item: ${item.length}');
}
}
複習一下為何會有匿名函式存在:
- 簡潔性: 不需要額外的函式定義,可以直接在需要的地方定義和使用函式,使得程式碼更加簡潔和易讀。
- 即時性: 可以將函式定義直接放在使用它的地方,更直觀地表達程式邏輯,不需要額外跳轉到函式定義的地方。
- 便利性: 對於一次性使用的簡單函式,使用匿名函式可以減少代碼的複雜性,提高開發效率。
Closure (閉包)
當定義一個函數時,如果在函數內部引用了外部的變量,那麼這個函數就是一個閉包。
這個閉包不僅包含了函數本身,還包含了它所引用的外部變量的狀態。
這意味著即使在函數返回後,這些外部變量的值仍然可以被閉包所記住,並且在以後的調用中可以被使用。
想像一個盒子(函數),這個盒子裡面有一些東西(變量)。
現在,把這個盒子放到了另一個盒子裡(另一個函數的作用域)。
當我們把這個盒子拿出來時,裡面的東西仍然存在,並且可以使用。
閉包就像是這個被放在另一個盒子中的盒子。它可以捕獲(記住)其定義範圍之外的變量,並在以後的任何時候使用這些變量。
這樣,即使函數已經返回,這些變量的狀態仍然可以被保留下來。
Function makeAdder(int addBy) {
return (int i) => addBy + i;
}
void main() {
var add2 = makeAdder(2);
var add4 = makeAdder(4);
print(add2(3)); // 5
print(add4(3)); // 7
}
來拆解一下上述發生的事情:
makeAdder
函式接收一個整數參數addBy
,並返回一個函式 ((int i) => addBy + i
)。這個返回的函式就是一個閉包,因為它在其內部引用了makeAdder
函式的局部變量addBy
。add2
記憶了一個addBy
為 2。add4
記憶了一個addBy
為 4。add2
傳入一個i
為 3,故答案為 2 + 3 。add4
傳入一個i
為 3,故答案為 4 + 3。
Tear-offs
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function tear-off
charCodes.forEach(print);
// Method tear-off
charCodes.forEach(buffer.write);
// 下面為錯誤示範
// Function lambda
charCodes.forEach((code) {
print(code);
});
// Method lambda
charCodes.forEach((code) {
buffer.write(code);
});
Generators
在 Dart 中,當需要延遲生成一個值的序列時,可以使用生成器函式。
這指的是只有在需要時才計算和生成數據,而不是在聲明或初始化時立即生成。
延遲求值在處理大型數據集或無限序列時可以有效地節省計算資源和內存。
Dart 有兩種生成器:
- 同步生成器:返回一個 Iterable 對象,即一個同步的、可以被迭代的集合。使用上為
sync
搭配yield
。
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
void main() {
for (var value in naturalsTo(5)) {
print(value); // 依次打印 0, 1, 2, 3, 4
}
}
- 非同步生成器:返回一個 Stream 對象,即一個可以接收非同步事件或數據的序列,通常用在比如處理用戶輸入、網絡請求等。使用上為
async
搭配yield
。
Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}
void main() async {
// 注意這裡是 await for 來處理非同步迭代
await for (var value in asynchronousNaturalsTo(5)) {
print(value); // 依次打印 0, 1, 2, 3, 4
}
}
External functions
外部函式的意思即函式的執行與聲明分別位在不同地方,比如來自其他 Dart library,或是來自其他語言 (ex. JavaScript)。
external void someFunc(int i);