目錄

Dart 開發語言概覽

本文將從變數和運算子開始到類和庫的使用來向你介紹 Dart 程式語言的主要功能,這裡假設你已經有使用其它語言進行程式設計的經驗。

你可以透過檢視 Dart 函式庫概覽 學習更多關於 Dart 核心函式庫的知識。若還想了解更多有關語言功能的詳細內容,請參閱 Dart 程式設計語言規範

一個簡單的 Dart 程式

下面的應用程式程式碼用到了很多 Dart 的基本功能:

// Define a function.
void printInteger(int aNumber) {
  print('The number is $aNumber.'); // Print to console.
}

// This is where the app starts executing.
void main() {
  var number = 42; // Declare and initialize a variable.
  printInteger(number); // Call a function.
}

下面是上述應用程式中使用到的程式碼片段,這些程式碼片段適用於所有(或幾乎所有)的 Dart 應用:

// This is a comment.

// 註釋。

以雙斜槓開頭的一行陳述式稱為單行註釋。Dart 同樣支援多行註釋和文件註釋。查閱註釋獲取更多相關資訊。

void

一種特殊的型別,表示一個值永遠不會被使用。類似於 main()printInteger() 的函式,以 void 宣告的函式返回型別,並不會返回值。

int

另一種資料型別,表示一個整型數字。 Dart 中一些其他的內建型別包括 StringListbool

42

表示一個數字字面量。數字字面量是一種編譯時常量。

print()

一種便利的將資訊輸出顯示的方式。

'...' (或 "...")

表示字串字面量。

$variableName (或 ${expression})

表示字串插值:字串字面量中包含的變數或表示式。查閱字串獲取更多相關資訊。

main()

一個特殊且 必須的 最上層函式,Dart 應用程式總是會從該函式開始執行。查閱 main() 函式 獲取更多相關資訊。

var

用於定義變數,透過這種方式定義變數不需要指定變數型別。這類變數的型別 (int) 由它的初始值決定 (42)。

重要概念

當你在學習 Dart 語言時, 應該牢記以下幾點:

  • Everything you can place in a variable is an object, and every object is an instance of a class. Even numbers, functions, and null are objects. With the exception of null (if you enable sound null safety), all objects inherit from the Object class.

    所有變數參考的都是 物件,每個物件都是一個 的例項。數字、函式以及 null 都是物件。除去 null 以外(如果你開啟了 空安全), 所有的類都繼承於 Object 類別。

  • 儘管 Dart 是強型別語言,但是在宣告變數時指定型別是可選的,因為 Dart 可以進行型別推斷。在上述程式碼中,變數 number 的型別被推斷為 int 型別。

  • 如果你開啟了 空安全,變數在未宣告為可空型別時不能為 null。你可以透過在型別後加上問號 (?) 將型別宣告為可空。例如,int? 型別的變數可以是整形數字或 null。如果你 明確知道 一個表示式不會為空,但 Dart 不這麼認為時,你可以在表示式後新增 ! 來斷言表示式不為空(為空時將丟擲例外)。例如:int x = nullableButNotNullInt!

  • 如果你想要明確地宣告允許任意類別型,使用 Object?(如果你 開啟了空安全)、 Object 或者 特殊型別 dynamic 將檢查延遲到執行時進行。

  • Dart 支援泛型,比如 List<int>(表示一組由 int 物件組成的列表)或 List<Object>(表示一組由任何型別物件組成的列表)。

  • Dart 支援最上層函式(例如 main 方法),同時還支援定義屬於類或物件的函式(即 靜態例項方法)。你還可以在函式中定義函式(巢狀(Nesting)區域性函式)。

  • Dart 支援最上層 變數,以及定義屬於類或物件的變數(靜態和例項變數)。例項變數有時稱之為域或屬性。

  • Dart 沒有類似於 Java 那樣的 publicprotectedprivate 成員存取限定符。如果一個識別符號以下劃線 (_) 開頭則表示該識別符號在庫內是私有的。可以查閱 庫和可見性 獲取更多相關資訊。

  • 識別符號 可以以字母或者下劃線 (_) 開頭,其後可跟字元和數字的組合。

  • Dart 中 表示式陳述式 是有區別的,表示式有值而陳述式沒有。比如條件表示式 expression condition ? expr1 : expr2 中含有值 expr1expr2。與 if-else 分支陳述式相比,if-else 分支陳述式則沒有值。一個陳述式通常包含一個或多個表示式,但是一個表示式不能只包含一個陳述式。

  • Dart 工具可以顯示 警告錯誤 兩種型別的問題。警告表明程式碼可能有問題但不會阻止其執行。錯誤分為編譯時錯誤和執行時錯誤;編譯時錯誤程式碼無法執行;執行時錯誤會在程式碼執行時導致 例外

關鍵字

下面的表格中列出了 Dart 語言所使用的關鍵字。

應該避免使用這些單詞作為識別符號。但是,帶有上標的單詞可以在必要的情況下作為識別符號:

  • 帶有上標 1 的關鍵字為 上下文關鍵字,只有在特定的場景才有意義,它們可以在任何地方作為有效的識別符號。

  • 帶有上標 2 的關鍵字為 內建識別符號,這些關鍵字在大多數時候都可以作為有效的識別符號,但是它們不能用作類別名稱或者型別名或者作為匯入字首使用。

  • 帶有上標 3 的關鍵字為 Dart 1.0 釋出後用於 支援非同步 相關內容。不能在由關鍵字 asyncasync*sync* 標識的方法體中使用 awaityield 作為識別符號。

其它沒有上標的關鍵字為 保留字,均不能用作識別符號。

變數

下面的範例程式碼將建立一個變數並將其初始化:

var name = 'Bob';

變數僅儲存物件的參考。這裡名為 name 的變數儲存了一個 String 型別物件的參考,“Bob” 則是該物件的值。

name 變數的型別被推斷為 String,但是你可以為其指定型別。如果一個物件的參考不侷限於單一的型別,可以將其指定為 Object(或 dynamic)型別。

Object name = 'Bob';

除此之外你也可以指定型別:

String name = 'Bob';

預設值

在 Dart 中,未初始化以及可空型別的變數擁有一個預設的初始值 null。(如果你未遷移至 空安全,所有變數都為可空型別。)即便數字也是如此,因為在 Dart 中一切皆為物件,數字也不例外。

int? lineCount;
assert(lineCount == null);

若你啟用了空安全,你必須在使用變數前初始化它的值。

int lineCount = 0;

你並不需要在宣告變數時初始化,只需在第一次用到這個變數前初始化即可。例如,下面的程式碼是正確的,因為 Dart 可以在 lineCount 被傳遞到 print() 時檢測它是否為空:

int lineCount;

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

print(lineCount);

最上層變數以及類變數是延遲初始化的,即檢查變數的初始化會在它第一次被使用的時候完成。

延遲初始化變數

Dart 2.12 新增了 late 修飾符,這個修飾符可以在以下情況中使用:

  • 宣告一個非空變數,但不在宣告時初始化。

  • 延遲初始化一個變數。

通常 Dart 的語義分析會在一個已宣告為非空的變數被使用前檢查它是否已經被賦值,但有時這個分析會失敗。例如:在檢查最上層變數和例項變數時,分析通常無法判斷它們是否已經被初始化,因此不會進行分析。

如果你確定這個變數在使用前就已經被宣告,但 Dart 判斷失誤的話,你可以在宣告變數的時候使用 late 修飾來解決這個問題。

late String description;

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

如果一個 late 修飾的變數在宣告時就指定了初始化方法,那麼它實際的初始化過程會發生在第一次被使用的時候。這樣的延遲初始化在以下場景中會帶來便利:

  • Dart 認為這個變數可能在後文中沒被使用,而且初始化時將產生較大的代價。

  • 你正在初始化一個例項變數,它的初始化方法需要呼叫 this

在下面這個例子中,如果 temperature 變數從未被使用的話,那麼 readThermometer() 將永遠不會被呼叫:

// This is the program's only call to readThermometer().
late String temperature = readThermometer(); // Lazily initialized.

Final 和 Const

如果你不想更改一個變數,可以使用關鍵字 final 或者 const 修飾變數,這兩個關鍵字可以替代 var 關鍵字或者加在一個具體的型別前。一個 final 變數只可以被賦值一次;一個 const 變數是一個編譯時常量 (const 變數同時也是 final 的)。

下面的範例中我們建立並設定兩個 final 變數:

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

你不能修改一個 final 變數的值:

name = 'Alice'; // Error: a final variable can only be set once.

使用關鍵字 const 修飾變量表示該變數為 編譯時常量。如果使用 const 修飾類中的變數,則必須加上 static 關鍵字,即 static const(譯者注:順序不能顛倒)。在宣告 const 變數時可以直接為其賦值,也可以使用其它的 const 變數為其賦值:

const bar = 1000000; // Unit of pressure (dynes/cm2)
const double atm = 1.01325 * bar; // Standard atmosphere

const 關鍵字不僅僅可以用來定義常量,還可以用來建立 常量值,該常量值可以賦予給任何變數。你也可以將建構函式宣告為 const 的,這種型別的建構函式建立的物件是不可改變的。

var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`

如果使用初始化表示式為常量賦值可以省略掉關鍵字 const,比如上面的常量 baz 的賦值就省略掉了 const。詳情請查閱 不要冗餘地使用 const

沒有使用 final 或 const 修飾的變數的值是可以被更改的,即使這些變數之前參考過 const 的值。

foo = [1, 2, 3]; // Was const []

常量的值不可以被修改:

baz = [42]; // Error: Constant variables can't be assigned a value.

你可以在常量中使用 型別檢查和強制型別轉換 (isas)、 集合中的 if 以及 展開運運算元 (......?):

const Object i = 3; // Where i is a const Object with an int value...
const list = [i as int]; // Use a typecast.
const map = {if (i is int) i: 'int'}; // Use is and collection if.
const set = {if (list is List<int>) ...list}; // ...and a spread.

可以查閱 ListsMapsClasses 獲取更多關於使用 const 建立常量值的資訊。

內建型別

Dart 語言支援下列內容:

使用字面量來建立物件也受到支援。例如 'This is a string' 是一個字串字面量,true 是一個布林字面量。

由於 Dart 中每個變數參考都指向一個物件(一個 的例項),通常也可以使用 構造器 來初始化變數。一些內建的型別有它們自己的構造器。例如你可以使用 Map() 來建立一個 map 物件。

Some other types also have special roles in the Dart language:

  • Object: The superclass of all Dart classes except Null.
  • Enum: The superclass of all enums.
  • Future and Stream: Used in asynchrony support.
  • Iterable: Used in for-in loops and in synchronous generator functions.
  • Never: Indicates that an expression can never successfully finish evaluating. Most often used for functions that always throw an exception.
  • dynamic: Indicates that you want to disable static checking. Usually you should use Object or Object? instead.
  • void: Indicates that a value is never used. Often used as a return type.

The Object, Object?, Null, and Never classes have special roles in the class hierarchy, as described in the top-and-bottom section of Understanding null safety.

Numbers

Dart 支援兩種 Number 型別:

int

整數值;長度不超過 64 位,具體取值範圍 依賴於不同的平台。在 DartVM 上其取值位於 -263 至 263 - 1 之間。在 Web 上,整型數值代表著 JavaScript 的數字(64 位無小數浮點型),其允許的取值範圍在 -253 至 253 - 1 之間。

double

64 位的雙精度浮點數字,且符合 IEEE 754 標準。

intdouble 都是 num 的子類別。 num 中定義了一些基本的運算子比如 +、-、*、/ 等,還定義了 abs()ceil()floor() 等方法(位運算子,比如 >> 定義在 int 中)。如果 num 及其子類別不滿足你的要求,可以檢視 dart:math 庫中的 API。

整數是不帶小數點的數字,下面是一些定義整數字面量的例子:

var x = 1;
var hex = 0xDEADBEEF;

如果一個數字包含了小數點,那麼它就是浮點型的。下面是一些定義浮點數字面量的例子:

var y = 1.1;
var exponents = 1.42e5;

You can also declare a variable as a num. If you do this, the variable can have both integer and double values.

num x = 1; // x can have both int and double values
x += 2.5;

整型字面量將會在必要的時候自動轉換成浮點數字面量:

double z = 1; // Equivalent to double z = 1.0.

下面是字串和數字之間轉換的方式:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

整型支援傳統的位移操作,比如移位(<<>>>>>)、補碼 (~)、按位與 (&)、按位或 (|) 以及按位異或 (^),例如:

assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 | 4) == 7); // 0011 | 0100 == 0111
assert((3 & 4) == 0); // 0011 & 0100 == 0000

更多範例請檢視 移位運運算元 小節。

數字字面量為編譯時常量。很多算術表示式只要其運算元是常量,則表示式結果也是編譯時常量。

const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;

更多內容,請檢視 Dart 中的數字

Strings

Dart 字串(String 物件)包含了 UTF-16 編碼的字元序列。可以使用單引號或者雙引號來建立字串:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";
// 程式碼中文解釋
var s1 = '使用單引號建立字串字面量。';
var s2 = "雙引號也可以用於建立字串字面量。";
var s3 = '使用單引號建立字串時可以使用斜槓來轉義那些與單引號衝突的字串:\'。';
var s4 = "而在雙引號中則不需要使用轉義與單引號衝突的字串:'";

在字串中,請以 ${表示式} 的形式使用表示式,如果表示式是一個識別符號,可以省略掉 {}。如果表示式的結果為一個物件,則 Dart 會呼叫該物件的 toString 方法來獲取一個字串。

var s = 'string interpolation';

assert('Dart has $s, which is very handy.' ==
    'Dart has string interpolation, '
        'which is very handy.');
assert('That deserves all caps. '
        '${s.toUpperCase()} is very handy!' ==
    'That deserves all caps. '
        'STRING INTERPOLATION is very handy!');
// 程式碼中文解釋
var s = '字串插值';

assert('Dart 有$s,使用起來非常方便。' == 'Dart 有字串插值,使用起來非常方便。');
assert('使用${s.substring(3,5)}表示式也非常方便' == '使用插值表示式也非常方便。');

你可以使用 + 運算子或並列放置多個字串來連線字串:

var s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 ==
    'String concatenation works even over '
        'line breaks.');

var s2 = 'The + operator ' + 'works, as well.';
assert(s2 == 'The + operator works, as well.');
// 程式碼中文解釋
var s1 = '可以拼接'
    '字串'
    "即便它們不在同一行。";
assert(s1 == '可以拼接字串即便它們不在同一行。');

var s2 = '使用加號 + 運算子' + '也可以達到相同的效果。';
assert(s2 == '使用加號 + 運算子也可以達到相同的效果。');

使用三個單引號或者三個雙引號也能建立多行字串:

var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";
// 程式碼中文解釋
var s1 = '''
你可以像這樣建立多行字串。
''';

var s2 = """這也是一個多行字串。""";

在字串前加上 r 作為字首建立 “raw” 字串(即不會被做任何處理(比如轉義)的字串):

var s = r'In a raw string, not even \n gets special treatment.';
// 程式碼中文解釋
var s = r'在 raw 字串中,跳脫字元串 \n 會直接輸出 “\n” 而不是轉義為換行。';

你可以查閱 Runes 與 grapheme clusters 獲取更多關於如何在字串中表示 Unicode 字元的資訊。

字串字面量是一個編譯時常量,只要是編譯時常量 (null、數字、字串、布林) 都可以作為字串字面量的插值表示式:

// These work in a const string.
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// These do NOT work in a const string.
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [1, 2, 3];

const validConstString = '$aConstNum $aConstBool $aConstString';
// const invalidConstString = '$aNum $aBool $aString $aConstList';

可以查閱 字串和正則表示式 獲取更多關於如何使用字串的資訊。

布林型別

Dart 使用 bool 關鍵字表示布林型別,布林型別只有兩個物件 truefalse,兩者都是編譯時常量。

Dart 的型別安全不允許你使用類似 if (nonbooleanValue) 或者 assert (nonbooleanValue) 這樣的程式碼檢查布林值。相反,你應該總是顯示地檢查布林值,比如像下面的程式碼這樣:

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);

// Check for null.
var unicorn;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

Lists

陣列 (Array) 是幾乎所有程式語言中最常見的集合型別,在 Dart 中陣列由 List 物件表示。通常稱之為 List

Dart 中的列表字面量是由逗號分隔的一串表示式或值並以方括號 ([]) 包裹而組成的。下面是一個 Dart List 的範例:

var list = [1, 2, 3];

你可以在 Dart 的集合型別的最後一個專案後新增逗號。這個尾隨逗號並不會影響集合,但它能有效避免「複製貼上」的錯誤。

var list = [
  'Car',
  'Boat',
  'Plane',
];

List 的下標索引從 0 開始,第一個元素的下標為 0,最後一個元素的下標為 list.length - 1。你可以像 JavaScript 中的用法那樣獲取 Dart 中 List 的長度以及元素:

var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);

在 List 字面量前新增 const 關鍵字會建立一個編譯時常量:

var constantList = const [1, 2, 3];
// constantList[1] = 1; // This line will cause an error.

Dart 在 2.3 引入了 擴充運運算元...)和 空感知擴充運運算元...?),它們提供了一種將多個元素插入集合的簡潔方法。

例如,你可以使用擴充運運算元(...)將一個 List 中的所有元素插入到另一個 List 中:

var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);

如果擴充運運算元右邊可能為 null ,你可以使用 null-aware 擴充運運算元(...?)來避免產生例外:

var list2 = [0, ...?list];
assert(list2.length == 1);

可以查閱擴充運運算元建議獲取更多關於如何使用擴充運運算元的資訊。

Dart 還同時引入了 集合中的 if集合中的 for 操作,在建構集合時,可以使用條件判斷 (if) 和迴圈 (for)。

下面範例是使用 集合中的 if 來建立一個 List 的範例,它可能包含 3 個或 4 個元素:

var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet'];

下面是使用 集合中的 for 將列表中的元素修改後新增到另一個列表中的範例:

var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');

你可以查閱 集合中使用控制流建議 獲取更多關於在集合中使用 iffor 的細節內容和範例。

List 類中有許多用於操作 List 的便捷方法,你可以查閱 泛型集合 獲取更多與之相關的資訊。

Sets

在 Dart 中,set 是一組特定元素的無序集合。 Dart 支援的集合由集合的字面量和 Set 類提供。

下面是使用 Set 字面量來建立一個 Set 集合的方法:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

可以使用在 {} 前加上型別引數的方式建立一個空的 Set,或者將 {} 賦值給一個 Set 型別的變數:

var names = <String>{};
// Set<String> names = {}; // This works, too.
// var names = {}; // Creates a map, not a set.

使用 add() 方法或 addAll() 方法向已存在的 Set 中新增專案:

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);

使用 .length 可以獲取 Set 中元素的數量:

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);

可以在 Set 變數前新增 const 關鍵字建立一個 Set 編譯時常量:

final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // This line will cause an error.

從 Dart 2.3 開始,Set 可以像 List 一樣支援使用擴充運運算元(......?)以及 Collection iffor 操作。你可以查閱 List 擴充運運算元List 集合運運算元 獲取更多相關資訊。

你也可以查閱 泛型 以及 Set 獲取更多相關資訊。

Maps

通常來說,Map 是用來關聯 keys 和 values 的物件。其中鍵和值都可以是任何型別的物件。每個 只能出現一次但是 可以重複出現多次。 Dart 中 Map 提供了 Map 字面量以及 Map 型別兩種形式的 Map。

下面是一對使用 Map 字面量建立 Map 的例子:

var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

你也可以使用 Map 的構造器建立 Map:

var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

向現有的 Map 中新增鍵值對與 JavaScript 的操作類似:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // Add a key-value pair

從一個 Map 中獲取一個值的操作也與 JavaScript 類似:

var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');

如果檢索的 Key 不存在於 Map 中則會返回一個 null:

var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);

使用 .length 可以獲取 Map 中鍵值對的數量:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

在一個 Map 字面量前新增 const 關鍵字可以建立一個 Map 編譯時常量:

final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

// constantMap[2] = 'Helium'; // This line will cause an error.

Map 可以像 List 一樣支援使用擴充運運算元(......?)以及集合的 if 和 for 操作。你可以查閱 List 擴充運運算元List 集合運運算元 獲取更多相關資訊。

你也可以查閱 泛型 以及 Maps API 獲取更多相關資訊。

Runes 與 grapheme clusters

在 Dart 中,runes 公開了字串的 Unicode 碼位。使用 characters 包 來存取或者操作使用者感知的字元,也被稱為 Unicode (擴充) grapheme clusters

Unicode 編碼為每一個字母、數字和符號都定義了一個唯一的數值。因為 Dart 中的字串是一個 UTF-16 的字元序列,所以如果想要表示 32 位的 Unicode 數值則需要一種特殊的語法。

表示 Unicode 字元的常見方式是使用 \uXXXX,其中 XXXX 是一個四位數的 16 進位制數字。例如心形字元(♥)的 Unicode 為 \u2665。對於不是四位數的 16 進位制數字,需要使用大括號將其括起來。例如大笑的 emoji 表情(😆)的 Unicode 為 \u{1f600}

如果你需要讀寫單個 Unicode 字元,可以使用 characters 套件中定義的 characters getter。它將返回 Characters 物件作為一系列 grapheme clusters 的字串。下面是使用 characters API 的範例:

import 'package:characters/characters.dart';

void main() {
  var hi = 'Hi 🇩🇰';
  print(hi);
  print('The end of the string: ${hi.substring(hi.length - 1)}');
  print('The last character: ${hi.characters.last}');
}

輸出取決於你的環境,大致類似於:

$ dart run bin/main.dart
Hi 🇩🇰
The end of the string: ???
The last character: 🇩🇰

有關使用 characters 包操作字串的詳細資訊,請參閱用於 characters 套件的範例API 參考

Symbols

Symbol 表示 Dart 中宣告的運運算元或者識別符號。你幾乎不會需要 Symbol,但是它們對於那些透過名稱參考識別符號的 API 很有用,因為程式碼壓縮後,儘管識別符號的名稱會改變,但是它們的 Symbol 會保持不變。

可以使用在識別符號前加 # 字首來獲取 Symbol:

#radix
#bar

Symbol 字面量是編譯時常量。

函式

Dart 是一種真正面向物件的語言,所以即便函式也是物件並且型別為 Function,這意味著函式可以被賦值給變數或者作為其它函式的引數。你也可以像呼叫函式一樣呼叫 Dart 類別的例項。詳情請查閱 可呼叫的類

下面是定義一個函式的例子:

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

雖然高效 Dart 指南建議在 公開的 API 上定義返回型別,不過即便不定義,該函式也依然有效:

isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

如果函式體內只包含一個表示式,你可以使用簡寫語法:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

語法 => 表示式{ return 表示式; } 的簡寫, => 有時也稱之為 箭頭 函式。

引數

函式可以有兩種形式的引數:必要引數可選引數。必要引數定義在引數列表前面,可選引數則定義在必要引數後面。可選引數可以是 命名的位置的

向函式傳入引數或者定義函式引數時,可以使用 尾逗號

命名引數

命名引數預設為可選引數,除非他們被特別標記為 required

定義函式時,使用 {引數1, 引數2, …} 來指定命名引數:如果你沒有提供一個預設值,也沒有使用 required 標記的話,那麼它一定可空的型別,因為他們的預設值會是 null

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool? bold, bool? hidden}) {...}

當呼叫函式時,你可以使用 引數名: 引數值 指定一個命名引數的值。例如:

enableFlags(bold: true, hidden: false);

你可以使用 = 來為一個命名引數指定除了 null 以外的預設值。指定的預設值必須要為編譯時的常量,例如:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold will be true; hidden will be false.
enableFlags(bold: true);

如果你希望一個命名引數是強制需要使用的,呼叫者需要提供它的值,則你可以使用 required 進行宣告:

const Scrollbar({super.key, required Widget child});

當你建立一個不帶 child 引數的 Scrollbar 時,分析器就會報告這裡出了問題。

儘管將位置引數放在最前面通常比較合理,但你也可以將命名引數放在引數列表的任意位置,讓整個呼叫的方式看起來更適合你的 API:

repeat(times: 2, () {
  ...
});

可選的位置引數

使用 [] 將一系列引數包裹起來,即可將其標記為位置引數,因為它們的預設值是 null,所以如果你沒有提供預設值的話,它們的型別必須得是允許為空 (nullable) 的型別。

String say(String from, String msg, [String? device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

下面是不使用可選引數呼叫上述函式的範例

assert(say('Bob', 'Howdy') == 'Bob says Howdy');

下面是使用可選引數呼叫上述函式的範例:

assert(say('Bob', 'Howdy', 'smoke signal') ==
    'Bob says Howdy with a smoke signal');

你可以使用 = 來為一個位置可選引數指定除了 null 以外的預設值。指定的預設值必須要為編譯時的常量,例如:

String say(String from, String msg, [String device = 'carrier pigeon']) {
  var result = '$from says $msg with a $device';
  return result;
}

assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');

main() 函式

每個 Dart 程式都必須有一個 main() 最上層函式作為程式的入口, main() 函式返回值為 void 並且有一個 List<String> 型別的可選引數。

下面是一個簡單 main() 函式:

void main() {
  print('Hello, World!');
}

下面是使用命令列存取帶引數的 main() 函式範例:

// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}

你可以透過使用 引數庫 來定義和解析命令列引數。

函式是一級物件

可以將函式作為引數傳遞給另一個函式。例如:

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// Pass printElement as a parameter.
list.forEach(printElement);

你也可以將函式賦值給一個變數,比如:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

該範例中使用了匿名函式。下一節會有更多與其相關的介紹。

匿名函式

大多數方法都是有名字的,比如 main()printElement()。你可以建立一個沒有名字的方法,稱之為 匿名函式Lambda 表示式Closure 閉包。你可以將匿名方法賦值給一個變數然後使用它,比如將該變數新增到集合或從中刪除。

匿名方法看起來與命名方法類似,在括號之間可以定義引數,引數之間用逗號分割。

後面大括號中的內容則為函式體:

([[型別] 引數[, …]]) {
  函式體;
};

下面程式碼定義了只有一個引數 item 且沒有引數型別的匿名方法。 List 中的每個元素都會呼叫這個函式,列印元素位置和值的字串:

const list = ['apples', 'bananas', 'oranges'];
list.map((item) {
  return item.toUpperCase();
}).forEach((item) {
  print('$item: ${item.length}');
});

點選 Run 按鈕執行程式碼。

void main() {
  const list = ['apples', 'bananas', 'oranges'];
  list.map((item) {
    return item.toUpperCase();
  }).forEach((item) {
    print('$item: ${item.length}');
  });
}

如果函式體內只有一行返回陳述式,你可以使用胖箭頭縮寫法。貼上下面程式碼到 DartPad 中並點選執行按鈕,驗證兩個函式是否一致。

list
    .map((item) => item.toUpperCase())
    .forEach((item) => print('$item: ${item.length}'));

詞法作用域

Dart 是詞法有作用域語言,變數的作用域在寫程式碼的時候就確定了,大括號內定義的變數只能在大括號記憶體取,與 Java 類似。

下面是一個巢狀(Nesting)函式中變數在多個作用域中的範例:

bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

注意 nestedFunction() 函式可以存取包括最上層變數在內的所有的變數。

詞法閉包

閉包 即一個函式物件,即使函式物件的呼叫在它原始作用域之外,依然能夠存取在它詞法作用域內的變數。

函式可以封閉定義到它作用域內的變數。接下來的範例中,函式 makeAdder() 捕獲了變數 addBy。無論函式在什麼時候返回,它都可以使用捕獲的 addBy 變數。

/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // Create a function that adds 2.
  var add2 = makeAdder(2);

  // Create a function that adds 4.
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}

測試函式是否相等

下面是最上層函式、靜態方法和例項方法相等性的測試範例:

void foo() {} // A top-level function

class A {
  static void bar() {} // A static method
  void baz() {} // An instance method
}

void main() {
  Function x;

  // Comparing top-level functions.
  x = foo;
  assert(foo == x);

  // Comparing static methods.
  x = A.bar;
  assert(A.bar == x);

  // Comparing instance methods.
  var v = A(); // Instance #1 of A
  var w = A(); // Instance #2 of A
  var y = w;
  x = w.baz;

  // These closures refer to the same instance (#2),
  // so they're equal.
  assert(y.baz == x);

  // These closures refer to different instances,
  // so they're unequal.
  assert(v.baz != w.baz);
}

返回值

所有的函式都有返回值。沒有顯示返回陳述式的函式最後一行預設為執行 return null;

foo() {}

assert(foo() == null);

運算子

Dart 支援下表所示的運運算元,它也體現了 Dart 運算子的關聯性和 優先順序 的從高到低的順序。這也是 Dart 運算子關係的近似值。你可以將這些運算子實現為 一個類別的成員

Description Operator Associativity
unary postfix expr++    expr--    ()    []    ?[]    .    ?.    ! None
unary prefix -expr    !expr    ~expr    ++expr    --expr      await expr    None
multiplicative *    /    %  ~/ Left
additive +    - Left
shift <<    >>    >>> Left
bitwise AND & Left
bitwise XOR ^ Left
bitwise OR | Left
relational and type test >=    >    <=    <    as    is    is! None
equality ==    !=    None
logical AND && Left
logical OR || Left
if null ?? Left
conditional expr1 ? expr2 : expr3 Right
cascade ..    ?.. Right
assignment =    *=    /=   +=   -=   &=   ^=   etc. Right

一旦你使用了運算子,就建立了表示式。下面是一些運算子表示式的範例:

a++
a + b
a = b
a == b
c ? a : b
a is T

運算子表 中,運算子的優先順序按先後排列,即第一行優先順序最高,最後一行優先順序最低,而同一行中,最左邊的優先順序最高,最右邊的優先順序最低。例如:% 運算子優先順序高於 == ,而 == 高於 &&。根據優先順序規則,那麼意味著以下兩行程式碼執行的效果相同:

// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) ...

// Harder to read, but equivalent.
if (n % i == 0 && d % i == 0) ...

算術運算子

Dart 支援常用的算術運算子:

運算子 描述
+
-
-表示式 一元負, 也可以作為反轉(反轉表示式的符號)
*
/
~/ 除並取整
% 取模

範例:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder

assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 還支援自增自減操作。

Operator++var var = var + 1 (表示式的值為 var + 1)
var++ var = var + 1 (表示式的值為 var)
--var var = var - 1 (表示式的值為 var - 1)
var-- var = var - 1 (表示式的值為 var)

範例:

int a;
int b;

a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1

a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0

a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1

a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b); // -1 != 0

關係運算符

下表列出了關係運算符及含義:

Operator== 相等
!= 不等
> 大於
< 小於
>= 大於等於
<= 小於等於

要判斷兩個物件 x 和 y 是否表示相同的事物使用 == 即可。(在極少數情況下,可能需要使用 identical() 函式來確定兩個物件是否完全相同)。下面是 == 運算子的一些規則:

  1. xy 同時為空時返回 true,而只有一個為空時返回 false。

  2. 返回對 x 呼叫 == 方法的結果,引數為 y。(像 == 這樣的運運算元是對左側內容進行呼叫的。詳情請查閱 運運算元。)

下面的程式碼給出了每一種關係運算符的範例:

assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);

型別判斷運算子

asisis! 運算子是在執行時判斷物件型別的運算子。

Operator Meaning
as 型別轉換(也用作指定 庫字首))
is 如果物件是指定型別則返回 true
is! 如果物件是指定型別則返回 false

當且僅當 obj 實現了 T 的介面,obj is T 才是 true。例如 obj is Object 總為 true,因為所有類都是 Object 的子類別。

僅當你確定這個物件是該型別的時候,你才可以使用 as 運運算元可以把物件轉換為特定的型別。例如:

(employee as Person).firstName = 'Bob';

如果你不確定這個物件型別是不是 T,請在轉型前使用 is T 檢查型別。

if (employee is Person) {
  // Type check
  employee.firstName = 'Bob';
}

賦值運算子

可以使用 = 來賦值,同時也可以使用 ??= 來為值為 null 的變數賦值。

// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;

+= 這樣的賦值運算子將算數運算子和賦值運算子組合在了一起。

= *= %= >>>= ^=
+= /= <<= &= |=
-= ~/= >>=    

下表解釋了複合運算子的原理:

場景 複合運算 等效表示式
假設有運算子 op a op= b a = a op b
範例: a += b a = a + b

下面的例子展示瞭如何使用賦值以及複合賦值運算子:

var a = 2; // Assign using =
a *= 3; // Assign and multiply: a = a * 3
assert(a == 6);

邏輯運算子

使用邏輯運算子你可以反轉或組合布林表示式。

運算子 描述
!表示式 對錶達式結果取反(即將 true 變為 false,false 變為 true)
|| 邏輯或
&& 邏輯與

下面是使用邏輯表示式的範例:

if (!done && (col == 0 || col == 3)) {
  // ...Do something...
}

按位和移位運算子

在 Dart 中,二進位制位運算子可以操作二進位制的某一位,但僅適用於整數。

運算子 描述
& 按位與
| 按位或
^ 按位異或
~表示式 按位取反(即將 “0” 變為 “1”,“1” 變為 “0”)
<< 位左移
>> 位右移
>>> 無符號右移

下面是使用按位和移位運算子的範例:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right
assert((value >>> 4) == 0x02); // Unsigned shift right
assert((-value >> 4) == -0x03); // Shift right
assert((-value >>> 4) > 0); // Unsigned shift right

條件表示式

Dart 有兩個特殊的運算子可以用來替代 if-else 陳述式:

條件 ? 表示式 1 : 表示式 2
如果條件為 true,執行表示式 1並返回執行結果,否則執行表示式 2 並返回執行結果。

表示式 1 ?? 表示式 2
如果表示式 1 為非 null 則返回其值,否則執行表示式 2 並返回其值。

根據布林表示式確定賦值時,請考慮使用 ?:

var visibility = isPublic ? 'public' : 'private';

如果賦值是根據判定是否為 null 則考慮使用 ??

String playerName(String? name) => name ?? 'Guest';

上述範例還可以寫成至少下面兩種不同的形式,只是不夠簡潔:

// Slightly longer version uses ?: operator.
String playerName(String? name) => name != null ? name : 'Guest';

// Very long version uses if-else statement.
String playerName(String? name) {
  if (name != null) {
    return name;
  } else {
    return 'Guest';
  }
}

級聯運算子

級聯運算子 (.., ?..) 可以讓你在同一個物件上連續呼叫多個物件的變數或方法。

比如下面的程式碼:

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

The constructor, Paint(), returns a Paint object. The code that follows the cascade notation operates on this object, ignoring any values that might be returned.

The previous example is equivalent to this code:

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

If the object that the cascade operates on can be null, then use a null-shorting cascade (?..) for the first operation. Starting with ?.. guarantees that none of the cascade operations are attempted on that null object.

querySelector('#confirm') // Get an object.
  ?..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'))
  ..scrollIntoView();

上面的程式碼相當於:

var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();

級聯運算子可以巢狀(Nesting),例如:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

在返回物件的函式中謹慎使用級聯運運算元。例如,下面的程式碼是錯誤的:

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // Error: method 'write' isn't defined for 'void'.

上述程式碼中的 sb.write() 方法返回的是 void,返回值為 void 的方法則不能使用級聯運算子。

其他運算子

大多數其它的運算子,已經在其它的範例中使用過:

運算子 名字 描述
() 使用方法 代表呼叫一個方法
[] 存取 List 存取 List 中特定位置的元素
?[] 判空存取 List 左側呼叫者不為空時,存取 List 中特定位置的元素
. 存取成員 成員存取符
?. 條件存取成員 與上述成員存取符類似,但是左邊的操作物件不能為 null,例如 foo?.bar,如果 foo 為 null 則返回 null ,否則返回 bar
! 空斷言運運算元 將表示式的型別轉換為其基礎型別,如果轉換失敗會丟擲執行時例外。例如 foo!.bar,如果 foo 為 null,則丟擲執行時例外

更多關於 ., ?... 運算子介紹,請參考.

流程控制陳述式

你可以使用下面的陳述式來控制 Dart 程式碼的執行流程:

  • ifelse

  • for 迴圈

  • whiledo-while 迴圈

  • breakcontinue

  • switchcase

  • assert

使用 try-catchthrow 也能影響控制流,詳情參考例外部分。

If 和 Else

Dart 支援 if - else 陳述式,其中 else 是可選的,比如下面的例子。你也可以參考條件表示式

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

Dart 的 if 陳述式中的條件必須是布林值而不能為其它型別。詳情請查閱 布林值

For 迴圈

你可以使用標準的 for 迴圈進行迭代。例如:

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
  message.write('!');
}

在 Dart 語言中,for 迴圈中的閉包會自動捕獲迴圈的 索引值 以避免 JavaScript 中一些常見的陷阱。假設有如下程式碼:

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}

for (final c in callbacks) {
  c();
}

上述程式碼執行後會輸出 01,但是如果在 JavaScript 中執行同樣的程式碼則會輸出兩個 2

如果要遍歷的物件是一個可迭代物件(例如 List 或 Set),並且你不需要知道當前的遍歷索引,則可以使用 for-in 方法進行 遍歷

for (final candidate in candidates) {
  candidate.interview();
}

可迭代物件同時可以使用 forEach() 方法作為另一種選擇:

var collection = [1, 2, 3];
collection.forEach(print); // 1 2 3

While 和 Do-While

while 迴圈會在執行迴圈體前先判斷條件:

while (!isDone()) {
  doSomething();
}

do-while 迴圈則會 先執行一遍迴圈體 再判斷條件:

do {
  printLine();
} while (!atEndOfPage());

Break 和 Continue

使用 break 可以中斷迴圈:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

使用 continue 可以跳過本次迴圈直接進入下一次迴圈:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

如果你正在使用諸如 List 或 Set 之類別的 Iterable 物件,你可以用以下方式重寫上述例子:

candidates
    .where((c) => c.yearsExperience >= 5)
    .forEach((c) => c.interview());

Switch 和 Case

Switch 陳述式在 Dart 中使用 == 來比較整數、字串或編譯時常量,比較的兩個物件必須是同一個型別且不能是子類別並且沒有重寫 == 運運算元。 列舉型別非常適合在 Switch 陳述式中使用。

每一個非空的 case 子句都必須有一個 break 陳述式,也可以透過 continuethrow 或者 return 來結束非空 case 陳述式。

不匹配任何 case 陳述式的情況下,會執行 default 子句中的程式碼:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

下面的例子忽略了 case 子句的 break 陳述式,因此會產生錯誤:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // ERROR: Missing break

  case 'CLOSED':
    executeClosed();
    break;
}

但是,Dart 支援空的 case 陳述式,允許其以 fall-through 的形式執行。

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // Empty case falls through.
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

在非空 case 陳述式中想要實現 fall-through 的形式,可以使用 continue 陳述式配合 label 的方式實現:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // Continues executing at the nowClosed label.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

每個 case 子句都可以有區域變數且僅在該 case 陳述式內可見。

斷言

在開發過程中,可以在條件表示式為 false 時使用 — assert(條件, 可選資訊); — 陳述式來打斷程式碼的執行。你可以在本文中找到大量使用 assert 的例子。下面是相關範例:

// Make sure the variable has a non-null value.
assert(text != null);

// Make sure the value is less than 100.
assert(number < 100);

// Make sure this is an https URL.
assert(urlString.startsWith('https'));

assert 的第二個引數可以為其新增一個字串訊息。

assert(urlString.startsWith('https'),
    'URL ($urlString) should start with "https".');

assert 的第一個引數可以是值為布林值的任何表示式。如果表示式的值為 true,則斷言成功,繼續執行。如果表示式的值為 false,則斷言失敗,丟擲一個 AssertionError 例外。

如何判斷斷言是否生效?斷言是否生效依賴開發工具和使用的框架:

  • Flutter 在 除錯模式 時生效。

  • 一些開發工具比如 [webdev serve][] 通常情況下是預設生效的。

  • 其他一些工具,比如 dart run 以及 [dart compile js][] 透過在執行 Dart 程式時新增命令列引數 --enable-asserts 使 assert 生效。

在生產環境程式碼中,斷言會被忽略,與此同時傳入 assert 的引數不被判斷。

例外

Dart 程式碼可以丟擲和捕獲例外。例外表示一些未知的錯誤情況,如果例外沒有捕獲則會被丟擲從而導致丟擲例外的程式碼終止執行。

與 Java 不同的是,Dart 的所有例外都是非必檢例外,方法不必宣告會丟擲哪些例外,並且你也不必捕獲任何例外。

Dart 提供了 ExceptionError 兩種型別的例外以及它們一系列的子類別,你也可以定義自己的例外型別。但是在 Dart 中可以將任何非 null 物件作為例外丟擲而不侷限於 Exception 或 Error 型別。

丟擲例外

下面是關於丟擲或者 引發 例外的範例:

throw FormatException('Expected at least 1 section');

你也可以丟擲任意的物件:

throw 'Out of llamas!';

因為丟擲例外是一個表示式,所以可以在 => 陳述式中使用,也可以在其他使用表示式的地方丟擲例外:

void distanceTo(Point other) => throw UnimplementedError();

捕獲例外

捕獲例外可以避免例外繼續傳遞(重新丟擲例外除外)。捕獲一個例外可以給你處理它的機會:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

對於可以丟擲多種例外型別的程式碼,也可以指定多個 catch 陳述式,每個陳述式分別對應一個例外型別,如果 catch 陳述式沒有指定例外型別則表示可以捕獲任意例外型別:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

如上述程式碼所示可以使用 oncatch 來捕獲例外,使用 on 來指定例外型別,使用 catch 來捕獲例外物件,兩者可同時使用。

你可以為 catch 方法指定兩個引數,第一個引數為丟擲的例外物件,第二個引數為棧資訊 StackTrace 物件:

try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

關鍵字 rethrow 可以將捕獲的例外再次丟擲:

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // Runtime error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}

Finally

無論是否丟擲例外,finally 陳述式始終執行,如果沒有指定 catch 陳述式來捕獲例外,則例外會在執行完 finally 陳述式後丟擲:

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

finally 陳述式會在任何匹配的 catch 陳述式後執行:

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

你可以閱讀 Dart 核心函式庫概覽的 例外 章節獲取更多相關資訊。

Dart 是支援基於 mixin 繼承機制的面嚮物件語言,所有物件都是一個類別的例項,而除了 Null 以外的所有的類都繼承自 Object 類別。 基於 mixin 的繼承 意味著儘管每個類(top class Object? 除外)都只有一個超類,一個類別的程式碼可以在其它多個類繼承中重複使用。 擴充方法 是一種在不更改類或建立子類別的情況下向類新增功能的方式。

使用類別的成員

物件的 成員 由函式和資料(即 方法例項變數)組成。方法的 呼叫 要透過物件來完成,這種方式可以存取物件的函式和資料。

使用(.)來存取物件的例項變數或方法:

var p = Point(2, 2);

// Get the value of y.
assert(p.y == 2);

// Invoke distanceTo() on p.
double distance = p.distanceTo(Point(4, 4));

使用 ?. 代替 . 可以避免因為左邊表示式為 null 而導致的問題:

// If p is non-null, set a variable equal to its y value.
var a = p?.y;

使用建構函式

可以使用 建構函式 來建立一個物件。建構函式的命名方式可以為 類別名稱 類別名稱 . 識別符號 的形式。例如下述程式碼分別使用 Point()Point.fromJson() 兩種構造器建立了 Point 物件:

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

以下程式碼具有相同的效果,但是建構函式名前面的的 new 關鍵字是可選的:

var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

一些類提供了常量建構函式。使用常量建構函式,在建構函式名之前加 const 關鍵字,來建立編譯時常量時:

var p = const ImmutablePoint(2, 2);

兩個使用相同建構函式相同引數值構造的編譯時常量是同一個物件:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // They are the same instance!

常量上下文 場景中,你可以省略掉建構函式或字面量前的 const 關鍵字。例如下面的例子中我們建立了一個常量 Map:

// Lots of const keywords here.
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

根據上下文,你可以只保留第一個 const 關鍵字,其餘的全部省略:

// Only one const, which establishes the constant context.
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

但是如果無法根據上下文判斷是否可以省略 const,則不能省略掉 const 關鍵字,否則將會建立一個 非常量物件 例如:

var a = const ImmutablePoint(1, 1); // Creates a constant
var b = ImmutablePoint(1, 1); // Does NOT create a constant

assert(!identical(a, b)); // NOT the same instance!

獲取物件的型別

可以使用 Object 物件的 runtimeType 屬性在執行時獲取一個物件的型別,該物件型別是 Type 的例項。

print('The type of a is ${a.runtimeType}');

到目前為止,我們已經瞭解瞭如何 使用 類別。本節的其餘部分將向你介紹如何 實現 一個類別。

例項變數

下面是宣告例項變數的範例:

class Point {
  double? x; // Declare instance variable x, initially null.
  double? y; // Declare y, initially null.
  double z = 0; // Declare z, initially 0.
}

所有未初始化的例項變數其值均為 null

所有例項變數均會隱含地宣告一個 Getter 方法。非終值的例項變數和 late final 宣告但未宣告初始化的例項變數還會隱含地宣告一個 Setter 方法。你可以查閱 Getter 和 Setter 獲取更多相關資訊。

class Point {
  double? x; // Declare instance variable x, initially null.
  double? y; // Declare y, initially null.
}

void main() {
  var point = Point();
  point.x = 4; // Use the setter method for x.
  assert(point.x == 4); // Use the getter method for x.
  assert(point.y == null); // Values default to null.
}

Instance variables can be final, in which case they must be set exactly once. Initialize final, non-late instance variables at declaration, using a constructor parameter, or using a constructor’s initializer list:

class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

If you need to assign the value of a final instance variable after the constructor body starts, you can use one of the following:

建構函式

宣告一個與類別名稱一樣的函式即可宣告一個建構函式(對於命名式建構函式 還可以新增額外的識別符號)。大部分的建構函式形式是產生式建構函式,其用於建立一個類別的例項:

class Point {
  double x = 0;
  double y = 0;

  Point(double x, double y) {
    // See initializing formal parameters for a better way
    // to initialize instance variables.
    this.x = x;
    this.y = y;
  }
}

使用 this 關鍵字參考當前例項。

終值初始化

對於大多數程式語言來說在建構函式中為例項變數賦值的過程都是類似的,而 Dart 則提供了一種特殊的語法糖來簡化該步驟。

構造中初始化的引數可以用於初始化非空或 final 修飾的變數,它們都必須被初始化或提供一個預設值。

class Point {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  Point(this.x, this.y);
}

在初始化時出現的變數預設是隱含終值,且只在初始化時可用。

預設建構函式

如果你沒有宣告建構函式,那麼 Dart 會自動產生一個無引數的建構函式並且該建構函式會呼叫其父類別的無引數構造方法。

建構函式不被繼承

子類別不會繼承父類別的建構函式,如果子類別沒有宣告建構函式,那麼只會有一個預設無引數的建構函式。

命名式建構函式

可以為一個類別宣告多個命名式建構函式來表達更明確的意圖:

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

記住建構函式是不能被繼承的,這將意味著子類別不能繼承父類別的命名式建構函式,如果你想在子類別中提供一個與父類命名建構函式名字一樣的命名建構函式,則需要在子類別中明確地宣告。

呼叫父類非預設建構函式

預設情況下,子類別的建構函式會呼叫父類別的匿名無引數構造方法,並且該呼叫會在子類別建構函式的函式體程式碼執行前,如果子類別建構函式還有一個 初始化列表,那麼該初始化列表會在呼叫父類別的該建構函式之前被執行,總的來說,這三者的呼叫順序如下:

  1. 初始化列表

  2. 父類別的無引數建構函式

  3. 當前類別的建構函式

如果父類沒有匿名無引數建構函式,那麼子類別必須呼叫父類別的其中一個建構函式,為子類別的建構函式指定一個父類別的建構函式只需在建構函式體前使用(:)指定。

下面的範例中,Employee 類別的建構函式呼叫了父類 Person 的命名建構函式。點選執行按鈕執行範例程式碼。

class Person {
  String? firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson().
  Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
  }
}

void main() {
  var employee = Employee.fromJson({});
  print(employee);
  // Prints:
  // in Person
  // in Employee
  // Instance of 'Employee'
}

因為引數會在子類別建構函式被執行前傳遞給父類別的建構函式,因此該引數也可以是一個表示式,比如一個函式:

class Employee extends Person {
  Employee() : super.fromJson(fetchDefaultData());
  // ···
}

超類引數

為了不重複地將引數傳遞到超類構造的指定引數,你可以使用超類引數,直接在子類別的構造中使用超類構造的某個引數。超類引數不能和重新導向的引數一起使用。超類引數的表示式和寫法與 終值初始化 類似:

class Vector2d {
  final double x;
  final double y;

  Vector2d(this.x, this.y);
}

class Vector3d extends Vector2d {
  final double z;

  // Forward the x and y parameters to the default super constructor like:
  // Vector3d(final double x, final double y, this.z) : super(x, y);
  Vector3d(super.x, super.y, this.z);
}

如果超類構造的位置引數已被使用,那麼超類構造引數就不能再繼續使用被佔用的位置。但是超類構造引數可以始終是命名引數:

class Vector2d {
  // ...

  Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
  // ...

  // Forward the y parameter to the named super constructor like:
  // Vector3d.yzPlane({required double y, required this.z})
  //       : super.named(x: 0, y: y);
  Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}

初始化列表

除了呼叫父類建構函式之外,還可以在建構函式體執行之前初始化例項變數。每個例項變數之間使用逗號分隔。

// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map<String, double> json)
    : x = json['x']!,
      y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

在開發模式下,你可以在初始化列表中使用 assert 來驗證輸入資料:

Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

使用初始化列表設定 final 欄位非常方便,下面的範例中就使用初始化列表來設定了三個 final 變數的值。點選執行按鈕執行範例程式碼。

import 'dart:math';

class Point {
  final double x;
  final double y;
  final double distanceFromOrigin;

  Point(double x, double y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

void main() {
  var p = Point(2, 3);
  print(p.distanceFromOrigin);
}

重新導向建構函式

有時候類中的建構函式僅用於呼叫類中其它的建構函式,此時該建構函式沒有函式體,只需在函式簽名後使用(:)指定需要重新導向到的其它建構函式 (使用 this 而非類別名稱):

class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(double x) : this(x, 0);
}

常量建構函式

如果類產生的物件都是不變的,可以在產生這些物件時就將其變為編譯時常量。你可以在類別的建構函式前加上 const 關鍵字並確保所有例項變數均為 final 來實現該功能。

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

常量建構函式建立的例項並不總是常量,具體可以參考使用建構函式章節。

工廠建構函式

使用 factory 關鍵字標識類別的建構函式將會令該建構函式變為工廠建構函式,這將意味著使用該建構函式構造類別的例項時並非總是會返回新的例項物件。例如,工廠建構函式可能會從快取中返回一個例項,或者返回一個子型別的例項。

在如下的範例中, Logger 的工廠建構函式從快取中返回物件,和 Logger.fromJson 工廠建構函式從 JSON 物件中初始化一個最終變數。

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache = <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

工廠建構函式的呼叫方式與其他建構函式一樣:

var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

方法

方法是為物件提供行為的函式。

例項方法

物件的例項方法可以存取例項變數和 this。下面的 distanceTo() 方法就是一個例項方法的例子:

import 'dart:math';

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

運運算元

運算子是有著特殊名稱的例項方法。 Dart 允許您使用以下名稱定義運算子:

< + | >>>
> / ^ []
<= ~/ & []=
>= * << ~
- % >> ==

為了表示重寫運運算元,我們使用 operator 標識來進行標記。下面是重寫 +- 運運算元的例子

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  @override
  bool operator ==(Object other) =>
      other is Vector && x == other.x && y == other.y;

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  final v = Vector(2, 3);
  final w = Vector(2, 2);

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

Getter 和 Setter

Getter 和 Setter 是一對用來讀寫物件屬性的特殊方法,上面說過例項物件的每一個屬性都有一個隱含的 Getter 方法,如果為非 final 屬性的話還會有一個 Setter 方法,你可以使用 getset 關鍵字為額外的屬性新增 Getter 和 Setter 方法:

class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

使用 Getter 和 Setter 的好處是,你可以先使用你的例項變數,過一段時間過再將它們包裹成方法且不需要改動任何程式碼,即先定義後更改且不影響原有邏輯。

抽象方法

例項方法、Getter 方法以及 Setter 方法都可以是抽象的,定義一個介面方法而不去做具體的實現讓實現它的類去實現該方法,抽象方法只能存在於 抽象類別中。

直接使用分號(;)替代方法體即可宣告一個抽象方法:

abstract class Doer {
  // Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}

抽象類別

使用關鍵字 abstract 標識類可以讓該類成為 抽象類別,抽象類別將無法被例項化。抽象類別常用於宣告介面方法、有時也會有具體的方法實現。如果想讓抽象類同時可被例項化,可以為其定義 工廠建構函式

抽象類別常常會包含 抽象方法。下面是一個宣告具有抽象方法的抽象類別範例:

// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {
  // Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}

隱含介面

每一個類別都隱含地定義了一個介面並實現了該介面,這個介面包含所有這個類別的例項成員以及這個類所實現的其它介面。如果想要建立一個 A 類支援呼叫 B 類別的 API 且不想繼承 B 類,則可以實現 B 類別的介面。

一個類別可以透過關鍵字 implements 來實現一個或多個介面並實現每個介面定義的 API:

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final String _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  String get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

如果需要實現多個類介面,可以使用逗號分割每個介面類:

class Point implements Comparable, Location {...}

擴充一個類別

使用 extends 關鍵字來建立一個子類,並可使用 super 關鍵字參考一個父類:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

想了解其他 extends 的用法,請檢視 泛型 部分中的 引數化型別

重寫類成員

子類別可以重寫父類別的例項方法(包括 運運算元)、 Getter 以及 Setter 方法。你可以使用 @override 註解來表示你重寫了一個成員:

class Television {
  // ···
  set contrast(int value) {...}
}

class SmartTelevision extends Television {
  @override
  set contrast(num value) {...}
  // ···
}

An overriding method declaration must match the method (or methods) that it overrides in several ways:

  • The return type must be the same type as (or a subtype of) the overridden method’s return type.
  • Argument types must be the same type as (or a supertype of) the overridden method’s argument types. In the preceding example, the contrast setter of SmartTelevision changes the argument type from int to a supertype, num.
  • If the overridden method accepts n positional parameters, then the overriding method must also accept n positional parameters.
  • A generic method can’t override a non-generic one, and a non-generic method can’t override a generic one.

你可以使用 covariant 關鍵字 來縮小程式碼中那些符合 型別安全 的方法引數或例項變數的型別。

noSuchMethod 方法

如果呼叫了物件上不存在的方法或例項變數將會觸發 noSuchMethod 方法,你可以重寫 noSuchMethod 方法來追蹤和記錄這一行為:

class A {
  // Unless you override noSuchMethod, using a
  // non-existent member results in a NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: '
        '${invocation.memberName}');
  }
}

只有下面其中一個條件成立時,你才能呼叫一個未實現的方法:

  • 接收方是靜態的 dynamic 型別。

  • 接收方具有靜態型別,定義了未實現的方法(抽象亦可),並且接收方的動態型別實現了 noSuchMethod 方法且具體的實現與 Object 中的不同。

你可以查閱 noSuchMethod 轉發規範 獲取更多相關資訊。

擴充方法

擴充方法是向現有庫新增功能的一種方式。你可能已經在不知道它是擴充方法的情況下使用了它。例如,當您在 IDE 中使用程式碼完成功能時,它建議將擴充方法與常規方法一起使用。

這裡是一個在 String 中使用擴充方法的範例,我們取名為 parseInt(),它在 string_apis.dart 中定義:

import 'string_apis.dart';
...
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.

有關使用以及實現擴充方法的詳細資訊,請參閱 擴充方法頁面

列舉型別

列舉型別是一種特殊的型別,也稱為 enumerationsenums,用於定義一些固定數量的常量值。

宣告簡單的列舉

你可以使用關鍵字 enum 來定義簡單的列舉型別和列舉值:

enum Color { red, green, blue }

宣告增強的列舉型別

Dart 中的列舉也支援定義欄位、方法和常量構造,常量構造只能構造出已知數量的常量例項(已定義的列舉值)。

你可以使用與定義 類似的陳述式來定義增強的列舉,但是這樣的定義有一些限制條件:

  • 例項的欄位必須是 final,包括由 mixin 混入的欄位。

  • 所有的 例項化構造 必須以 const 修飾。

  • 工廠構造 只能返回已知的一個列舉例項。

  • 由於 Enum 已經自動進行了繼承,所以列舉類不能再繼承其他類別。

  • 不能重載 indexhashCode 和比較運運算元 ==

  • 不能宣告 values 欄位,否則它將與列舉本身的靜態 values getter 衝突。

  • 在進行列舉定義時,所有的例項都需要首先進行宣告,且至少要宣告一個列舉例項。

下方是一個增強列舉的例子,它包含多個列舉例項、成員變數、getter 並且實現了介面:

enum Vehicle implements Comparable<Vehicle> {
  car(tires: 4, passengers: 5, carbonPerKilometer: 400),
  bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
  bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);

  const Vehicle({
    required this.tires,
    required this.passengers,
    required this.carbonPerKilometer,
  });

  final int tires;
  final int passengers;
  final int carbonPerKilometer;

  int get carbonFootprint => (carbonPerKilometer / passengers).round();

  @override
  int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}

想要了解更多關於定義增強列舉的內容,可以閱讀 小節。

使用列舉

你可以像存取 靜態變數 一樣存取列舉值:

final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
  print('Your favorite color is blue!');
}

每一個列舉值都有一個名為 index 成員變數的 Getter 方法,該方法將會返回以 0 為基準索引的位置值。例如,第一個列舉值的索引是 0 ,第二個列舉值的索引是 1。以此類推。

assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

想要獲得全部的列舉值,使用列舉類別的 values 方法獲取套件含它們的列表:

List<Color> colors = Color.values;
assert(colors[2] == Color.blue);

你可以在 Switch 陳述式中使用列舉,但是需要注意的是必須處理列舉值的每一種情況,即每一個列舉值都必須成為一個 case 子句,不然會出現警告:

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
}

如果你想要獲取一個列舉值的名稱,例如 Color.blue'blue',請使用 .name 屬性:

print(Color.blue.name); // 'blue'

使用 Mixin 為類新增功能

Mixin 是一種在多重繼承中複用某個類中程式碼的方法模式。

使用 with 關鍵字並在其後跟上 Mixin 類別的名字來使用 Mixin 模式:

class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

想要實現一個 Mixin,請建立一個繼承自 Object 且未宣告建構函式的類別。除非你想讓該類與普通的類一樣可以被正常地使用,否則請使用關鍵字 mixin 替代 class。例如:

mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

可以使用關鍵字 on 來指定哪些類可以使用該 Mixin 類,比如有 Mixin 類 A,但是 A 只能被 B 類使用,則可以這樣定義 A:

class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // ...
}

In the preceding code, only classes that extend or implement the Musician class can use the mixin MusicalPerformer. Because SingerDancer extends Musician, SingerDancer can mix in MusicalPerformer.

類變數和方法

使用關鍵字 static 可以宣告類變數或類方法。

靜態變數

靜態變數(即類變數)常用於宣告類範圍內所屬的狀態變數和常量:

class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}

靜態變數在其首次被使用的時候才被初始化。

靜態方法

靜態方法(即類方法)不能對例項進行操作,因此不能使用 this。但是他們可以存取靜態變數。如下面的例子所示,你可以在一個類別上直接呼叫靜態方法:

import 'dart:math';

class Point {
  double x, y;
  Point(this.x, this.y);

  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

可以將靜態方法作為編譯時常量。例如,你可以將靜態方法作為一個引數傳遞給一個常量建構函式。

泛型

如果你檢視陣列的 API 文件,你會發現陣列 List 的實際型別為 List<E>。 <…> 符號表示陣列是一個 泛型(或 引數化型別通常 使用一個字母來代表型別引數,比如 E、T、S、K 和 V 等等。

為什麼使用泛型?

泛型常用於需要要求型別安全的情況,但是它也會對程式碼執行有好處:

  • 適當地指定泛型可以更好地幫助程式碼產生器。

  • 使用泛型可以減少程式碼重複。

比如你想宣告一個只能包含 String 型別的陣列,你可以將該陣列宣告為 List<String>(讀作“字串型別的 list”),這樣的話就可以很容易避免因為在該陣列放入非 String 類變數而導致的諸多問題,同時編譯器以及其他閱讀程式碼的人都可以很容易地發現並定位問題:

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

另一個使用泛型的原因是可以減少重複程式碼。泛型可以讓你在多個不同型別實現之間共享同一個介面宣告,比如下面的例子中聲明瞭一個類別用於快取物件的介面:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

不久後你可能又會想專門為 String 類物件做一個快取,於是又有了專門為 String 做快取的類:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

如果過段時間你又想為數字型別也建立一個類別,那麼就會有很多諸如此類別的程式碼……

這時候可以考慮使用泛型來宣告一個類別,讓不同型別的快取實現該類做出不同的具體實現即可:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

在上述程式碼中,T 是一個替代型別。其相當於型別佔位符,在開發者呼叫該介面的時候會指定具體型別。

使用集合字面量

List、Set 以及 Map 字面量也可以是引數化的。定義引數化的 List 只需在中括號前新增 <type>;定義引數化的 Map 只需要在大括號前新增 <keyType, valueType>

var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

使用型別引數化的建構函式

在呼叫構造方法時也可以使用泛型,只需在類別名稱後用尖括號(<...>)將一個或多個型別包裹即可:

var nameSet = Set<String>.from(names);

下面程式碼建立了一個鍵為 Int 型別,值為 View 型別的 Map 物件:

var views = Map<int, View>();

泛型集合以及它們所包含的型別

Dart的泛型型別是 固化的,這意味著即便在執行時也會保持型別資訊:

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

限制引數化型別

有時使用泛型的時候,你可能會想限制可作為引數的泛型範圍,也就是引數必須是指定型別的子類別,這時候可以使用 extends 關鍵字。

一種常見的非空型別處理方式,是將子類別限制繼承 Object (而不是預設的 Object?)。

class Foo<T extends Object> {
  // Any type provided to Foo for T must be non-nullable.
}

You can use extends with other types besides Object. Here’s an example of extending SomeBaseClass, so that members of SomeBaseClass can be called on objects of type T:

class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

這時候就可以使用 SomeBaseClass 或者它的子類別來作為泛型引數:

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

這時候也可以指定無引數的泛型,這時無引數泛型的型別則為 Foo<SomeBaseClass>

var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

將非 SomeBaseClass 的型別作為泛型引數則會導致編譯錯誤:

var foo = Foo<Object>();

使用泛型方法

方法和引數也可以使用型別引數了:

T first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

方法 first<T> 的泛型 T 可以在如下地方使用:

  • 函式的返回值型別 (T)。

  • 引數的型別 (List<T>)。

  • 區域變數的型別 (T tmp)。

你可以查閱 使用泛型函式 獲取更多關於泛型的資訊。

庫和可見性

importlibrary 關鍵字可以幫助你建立一個模組化和可共享的程式碼庫。程式碼庫不僅只是提供 API 而且還起到了封裝的作用:以下劃線(_)開頭的成員僅在程式碼庫中可見。 每個 Dart 程式都是一個函式庫,即便沒有使用關鍵字 library 指定。

Dart 的函式庫可以使用 包工具 來發布和部署。

使用庫

使用 import 來指定名稱空間以便其它庫可以存取。

比如你可以匯入程式碼庫 dart:html 來使用 Dart Web 中相關 API:

import 'dart:html';

import 的唯一引數是用於指定程式碼庫的 URI,對於 Dart 內建的函式庫,使用 dart:xxxxxx 的形式。而對於其它的函式庫,你可以使用一個檔案系統路徑或者以 package:xxxxxx 的形式。 package:xxxxxx 指定的函式庫透過包管理器(比如 pub 工具)來提供:

import 'package:test/test.dart';

指定庫字首

如果你匯入的兩個程式碼庫有衝突的識別符號,你可以為其中一個指定字首。比如如果 library1 和 library2 都有 Element 類,那麼可以這麼處理:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

匯入庫的一部分

如果你只想使用程式碼庫中的一部分,你可以有選擇地匯入程式碼庫。例如:

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

延遲載入庫

延遲載入(也常稱為 延遲載入)允許應用在需要時再去載入程式碼庫,下面是可能使用到延遲載入的場景:

  • 為了減少應用的初始化時間。

  • 處理 A/B 測試,比如測試各種演算法的不同實現。

  • 載入很少會使用到的功能,比如可選的螢幕和對話方塊。

使用 deferred as 關鍵字來標識需要延時載入的程式碼庫:

import 'package:greetings/hello.dart' deferred as hello;

當實際需要使用到庫中 API 時先呼叫 loadLibrary 函式載入庫:

Future<void> greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

在前面的程式碼,使用 await 關鍵字暫停程式碼執行直到庫載入完成。更多關於 asyncawait 的資訊請參考非同步支援

loadLibrary 函式可以呼叫多次也沒關係,程式碼庫只會被載入一次。

當你使用延遲載入的時候需要牢記以下幾點:

  • 延遲載入的程式碼庫中的常量需要在程式碼庫被載入的時候才會匯入,未載入時是不會匯入的。

  • 匯入檔案的時候無法使用延遲載入庫中的型別。如果你需要使用型別,則考慮把介面型別轉移到另一個函式庫中然後讓兩個庫都分別匯入這個介面庫。

  • Dart會隱含地將 loadLibrary() 匯入到使用了 deferred as 名稱空間 的類中。 loadLibrary() 函式返回的是一個 Future

實現庫

查閱 建立依賴庫包 可以獲取有關如何實現庫套件的建議,包括:

  • 如何組織庫的原始檔。

  • 如何使用 export 命令。

  • 何時使用 part 命令。

  • 何時使用 library 命令。

  • 如何使用匯入和匯出命令實現多平臺的函式庫支援。

非同步支援

Dart 程式碼庫中有大量返回 FutureStream 物件的函式,這些函式都是 非同步 的,它們會在耗時操作(比如I/O)執行完畢前直接返回而不會等待耗時操作執行完畢。

asyncawait 關鍵字用於實現非同步程式設計,並且讓你的程式碼看起來就像是同步的一樣。

處理 Future

可以透過下面兩種方式,獲得 Future 執行完成的結果:

使用 asyncawait 的程式碼是非同步的,但是看起來有點像同步程式碼。例如,下面的程式碼使用 await 等待非同步函式的執行結果。

await lookUpVersion();

必須在帶有 async 關鍵字的 非同步函式 中使用 await

Future<void> checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

使用 trycatch 以及 finally 來處理使用 await 導致的例外:

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

你可以在非同步函式中多次使用 await 關鍵字。例如,下面程式碼中等待了三次函式結果:

var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await 表示式的返回值通常是一個 Future 物件;如果不是的話也會自動將其包裹在一個 Future 物件裡。 Future 物件代表一個“承諾”, await 表示式會阻塞直到需要的物件返回。

如果在使用 await 時導致編譯錯誤,請確保 await 在一個非同步函式中使用。例如,如果想在 main() 函式中使用 await,那麼 main() 函式就必須使用 async 關鍵字標識。

void main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

For an interactive introduction to using futures, async, and await, see the asynchronous programming codelab.

宣告非同步函式

非同步函式 是函式體由 async 關鍵字標記的函式。

將關鍵字 async 新增到函式並讓其返回一個 Future 物件。假設有如下返回 String 物件的方法:

String lookUpVersion() => '1.0.0';

將其改為非同步函式,返回值是 Future:

Future<String> lookUpVersion() async => '1.0.0';

注意,函式體不需要使用 Future API。如有必要,Dart 會建立 Future 物件。

如果函式沒有返回有效值,需要設定其返回型別為 Future<void>

關於 Future、asyncawait 的使用介紹,可以參見這個 codelab: asynchronous programming codelab

處理 Stream

如果想從 Stream 中獲取值,可以有兩種選擇:

  • 使用 async 關鍵字和一個 非同步迴圈(使用 await for 關鍵字標識)。

  • 使用 Stream API。詳情參考 函式庫概覽

使用 await for 定義非同步迴圈看起來是這樣的:

await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

表示式 的型別必須是 Stream。執行流程如下:

  1. 等待直到 Stream 返回一個數據。

  2. 使用 1 中 Stream 返回的資料執行迴圈體。

  3. 重複 1、2 過程直到 Stream 資料返回完畢。

使用 breakreturn 陳述式可以停止接收 Stream 資料,這樣就跳出了迴圈並取消註冊監聽 Stream。

**如果在實現非同步 for迴圈時遇到編譯時錯誤,請檢查確保 await for 處於非同步函式中。 ** 例如,要在應用程式的 main() 函式中使用非同步 for迴圈,main() 函式體必須標記為 async

void main() async {
  // ...
  await for (final request in requestServer) {
    handleRequest(request);
  }
  // ...
}

你可以查閱函式庫概覽中有關 dart:async 的部分獲取更多有關非同步程式設計的資訊。

產生器

當你需要延遲地產生一連串的值時,可以考慮使用 產生器函式。Dart 內建支援兩種形式的產生器方法:

  • 同步 產生器:返回一個 Iterable 物件。

  • 非同步 產生器:返回一個 Stream 物件。

透過在函式上加 sync* 關鍵字並將返回值型別設定為 Iterable 來實現一個 同步 產生器函式,在函式中使用 yield 陳述式來傳遞值:

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

實現 非同步 產生器函式與同步類似,只不過關鍵字為 async* 並且返回值為 Stream:

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

如果產生器是遞迴呼叫的,可是使用 yield* 陳述式提升執行效能:

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

可呼叫類

透過實現類別的 call() 方法,允許使用類似函式呼叫的方式來使用該類別的例項。

所有的類都可以定義並模擬 call() 方法,這個方法與普通 函式 是一樣的,支援傳參和定義返回型別等。

在下面的範例中,WannabeFunction 類定義了一個 call() 函式,函式接受三個字串引數,函式體將三個字串拼接,字串間用空格分割,並在結尾附加了一個感嘆號。單擊執行按鈕執行程式碼。

class WannabeFunction {
  String call(String a, String b, String c) => '$a $b $c!';
}

var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');

void main() => print(out);

隔離區

大多數計算機中,甚至在移動平臺上,都在使用多核 CPU。為了有效利用多核效能,開發者一般使用共享記憶體的方式讓執行緒併發地執行。然而,多執行緒共享資料通常會導致很多潛在的問題,並導致程式碼執行出錯。

為了解決多執行緒帶來的併發問題,Dart 使用 isolate 替代執行緒,所有的 Dart 程式碼均執行在一個 isolate 中。每一個 isolate 有它自己的堆記憶體以確保其狀態不被其它 isolate 存取。

所有的 Dart 程式碼都是在一個 isolate 中執行,而非執行緒。每個 isolate 都有一個單獨的執行執行緒,並且不與其他的 isolate 共享任何可變物件。

你可以查閱下面的文件獲取更多相關資訊:

Typedefs

類型別名是參考某一型別的簡便方法,因為其使用關鍵字 typedef,因此通常被稱作 typedef。下面是一個使用 IntList 來宣告和使用類型別名的例子:

typedef IntList = List<int>;
IntList il = [1, 2, 3];

類型別名可以有型別引數:

typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

針對函式,在大多數情況下,我們推薦使用 行內函數型別 替代 typedefs。然而,函式的 typedefs 仍然是有用的:

typedef Compare<T> = int Function(T a, T b);

int sort(int a, int b) => a - b;

void main() {
  assert(sort is Compare<int>); // True!
}

元資料

使用元資料可以為程式碼增加一些額外的資訊。元資料註解以 @ 開頭,其後緊跟一個編譯時常量(比如 deprecated)或者呼叫一個常量建構函式。

Dart 中有兩個註解是所有程式碼都可以使用的: @deprecated@Deprecated@override。你可以查閱 擴充一個類別 獲取有關 @override 的使用範例。下面是使用 @deprecated 的範例:

class Television {
  /// Use [turnOn] to turn the power on instead.
  @Deprecated('Use turnOn instead')
  void activate() {
    turnOn();
  }

  /// Turns the TV's power on.
  void turnOn() {...}
  // ···
}

可以自訂元資料註解。下面的範例定義了一個帶有兩個引數的 @todo 註解:

class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}

使用 @Todo 註解的範例:

@Todo('Dash', 'Implement this function')
void doSomething() {
  print('Do something');
}

元資料可以在 library、class、typedef、type parameter、 constructor、factory、function、field、parameter 或者 variable 宣告之前使用,也可以在 import 或 export 之前使用。可使用反射在執行時獲取元資料資訊。

註釋

Dart 支援單行註釋、多行註釋和文件註釋。

單行註釋

單行註釋以 // 開始。所有在 // 和該行結尾之間的內容均被編譯器忽略。

void main() {
  // TODO: refactor into an AbstractLlamaGreetingFactory?
  print('Welcome to my Llama farm!');
}

多行註釋

多行註釋以 /* 開始,以 */ 結尾。所有在 /**/ 之間的內容均被編譯器忽略(不會忽略文件註釋),多行註釋可以巢狀(Nesting)。

void main() {
  /*
   * This is a lot of work. Consider raising chickens.

  Llama larry = Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}

文件註釋

文件註釋可以是多行註釋,也可以是單行註釋,文件註釋以 /// 或者 /** 開始。在連續行上使用 /// 與多行文件註釋具有相同的效果。

在文件註釋中,除非用中括號括起來,否則分析器會忽略所有文字。使用中括號可以參考類、方法、欄位、最上層變數、函式和引數。括號中的符號會在已記錄的程式元素的詞法域中進行解析。

下面是一個參考其他類和成員的文件註釋:

/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
///
/// Just like any other animal, llamas need to eat,
/// so don't forget to [feed] them some [Food].
class Llama {
  String? name;

  /// Feeds your llama [food].
  ///
  /// The typical llama eats one bale of hay per week.
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

在產生的文件中,[feed] 會成為一個連結,指向 feed 方法的文件, [Food] 會成為一個連結,指向 Food 類別的 API 文件。

解析 Dart 程式碼並產生 HTML 文件,可以使用 Dart 的文件產生工具 dart doc。關於產生文件的範例,請參考 Dart API documentation 檢視關於文件結構的建議,請參考文件: Guidelines for Dart Doc Comments.

總結

本頁概述了 Dart 語言中常用的功能。還有更多特性有待實現,但我們希望它們不會破壞現有程式碼。有關更多資訊,請參考 Dart 語言規範高效 Dart 語言指南

要了解更多關於 Dart 核心函式庫的內容,請參考 Dart 核心函式庫概覽