跳到主內容

Dart 速查表

Dart 語言的設計旨在讓來自其他語言的程式設計師易於學習,但它也具有一些獨特的特性。本教程將引導你瞭解這些最重要的語言特性。

本教程中的嵌入式編輯器包含部分已完成的程式碼片段。你可以使用這些編輯器透過補全程式碼並點選 執行 按鈕來測試你的知識。編輯器還包含完善的測試程式碼;請勿編輯測試程式碼,但你可以隨意研究它以瞭解測試。

如果你需要幫助,請展開每個 DartPad 下方的 ... 的解決方案 下拉選單,以獲取解釋和答案。

字串插值

#

要將表示式的值放入字串中,請使用 ${expression}。如果表示式是識別符號,則可以省略 {}

以下是使用字串插值的一些示例

字串結果
'${3 + 2}''5'
'${"word".toUpperCase()}''WORD'
'$myObject'myObject.toString() 的值

練習

#

以下函式接受兩個整數作為引數。使其返回一個包含這兩個整數並用空格分隔的字串。例如,stringify(2, 3) 應該返回 '2 3'

String stringify(int x, int y) {
  TODO('Return a formatted string here');
}


// Tests your solution (Don't edit!):
void main() {
  assert(stringify(2, 3) == '2 3',
      "Your stringify method returned '${stringify(2, 3)}' instead of '2 3'");
  print('Success!');
}
字串插值示例的解決方案

xy 都是簡單值,Dart 的字串插值會處理將它們轉換為字串表示。你只需使用 $ 運算子在單引號內引用它們,並在它們之間新增一個空格

dart
String stringify(int x, int y) {
  return '$x $y';
}

可空變數

#

Dart 強制執行健全的空安全。這意味著除非你宣告允許為空,否則值不能為 null。換句話說,型別預設不可為空。

例如,考慮以下程式碼。在空安全下,這段程式碼會報錯。int 型別的變數不能具有 null

dart
int a = null; // INVALID.

建立變數時,在型別後面新增 ? 以指示該變數可以為 null

dart
int? a = null; // Valid.

你可以稍微簡化一下程式碼,因為在所有 Dart 版本中,null 都是未初始化變數的預設值

dart
int? a; // The initial value of a is null.

要了解有關 Dart 中空安全的更多資訊,請閱讀健全空安全指南

練習

#

在此 DartPad 中宣告兩個變數

  • 一個名為 name 的可空 String,值為 'Jane'
  • 一個名為 address 的可空 String,值為 null

忽略 DartPad 中的所有初始錯誤。

// TODO: Declare the two variables here


// Tests your solution (Don't edit!):
void main() {
  try {
    if (name == 'Jane' && address == null) {
      // Verify that "name" is nullable.
      name = null;
      print('Success!');
    } else {
      print('Not quite right, try again!');
    }
  } catch (e) {
    print('Exception: ${e.runtimeType}');
  }
}
可空變數示例的解決方案

將這兩個變數宣告為 String 後跟 ?。然後,將 'Jane' 賦值給 name,並讓 address 保持未初始化

dart
String? name = 'Jane';
String? address;

空感知運算子

#

Dart 提供了一些便捷的運算子來處理可能為 null 的值。其中之一是 ??= 賦值運算子,它僅在該變數當前為 null 時才向其賦值

dart
int? a; // = null
a ??= 3;
print(a); // <-- Prints 3.

a ??= 5;
print(a); // <-- Still prints 3.

另一個空感知運算子是 ??,它返回其左側表示式的值,除非該表示式的值為 null,在這種情況下它會評估並返回其右側表示式的值

dart
print(1 ?? 3); // <-- Prints 1.
print(null ?? 12); // <-- Prints 12.

練習

#

嘗試替換 ??=?? 運算子,以在以下程式碼片段中實現所描述的行為。

忽略 DartPad 中的所有初始錯誤。

String? foo = 'a string';
String? bar; // = null

// Substitute an operator that makes 'a string' be assigned to baz.
String? baz = foo /* TODO */ bar;

void updateSomeVars() {
  // Substitute an operator that makes 'a string' be assigned to bar.
  bar /* TODO */ 'a string';
}


// Tests your solution (Don't edit!):
void main() {
  try {
    updateSomeVars();

    if (foo != 'a string') {
      print('Looks like foo somehow ended up with the wrong value.');
    } else if (bar != 'a string') {
      print('Looks like bar ended up with the wrong value.');
    } else if (baz != 'a string') {
      print('Looks like baz ended up with the wrong value.');
    } else {
      print('Success!');
    }
  } catch (e) {
    print('Exception: ${e.runtimeType}.');
  }
}
空感知運算子示例的解決方案

你在此練習中所需做的只是將 TODO 註釋替換為 ????=。閱讀上文以確保你理解兩者,然後嘗試一下

dart
// Substitute an operator that makes 'a string' be assigned to baz.
String? baz = foo ?? bar;

void updateSomeVars() {
  // Substitute an operator that makes 'a string' be assigned to bar.
  bar ??= 'a string';
}

條件屬性訪問

#

為了保護對可能為 null 的物件的屬性或方法的訪問,請在點 (.) 前面加上問號 (?)

dart
myObject?.someProperty

前面的程式碼等同於以下程式碼

dart
(myObject != null) ? myObject.someProperty : null

你可以在一個表示式中將多個 ?. 鏈式連線起來

dart
myObject?.someProperty?.someMethod()

如果 myObjectmyObject.someProperty 為 null,則前面的程式碼返回 null(並且永遠不會呼叫 someMethod())。

練習

#

以下函式接受一個可空字串作為引數。嘗試使用條件屬性訪問使其返回 str 的大寫版本,如果 strnull 則返回 null

String? upperCaseIt(String? str) {
  // TODO: Try conditionally accessing the `toUpperCase` method here.
}


// Tests your solution (Don't edit!):
void main() {
  try {
    String? one = upperCaseIt(null);
    if (one != null) {
      print('Looks like you\'re not returning null for null inputs.');
    } else {
      print('Success when str is null!');
    }
  } catch (e) {
    print('Tried calling upperCaseIt(null) and got an exception: \n ${e.runtimeType}.');
  }

  try {
    String? two = upperCaseIt('a string');
    if (two == null) {
      print('Looks like you\'re returning null even when str has a value.');
    } else if (two != 'A STRING') {
      print('Tried upperCaseIt(\'a string\'), but didn\'t get \'A STRING\' in response.');
    } else {
      print('Success when str is not null!');
    }
  } catch (e) {
    print('Tried calling upperCaseIt(\'a string\') and got an exception: \n ${e.runtimeType}.');
  }
}
條件屬性訪問示例的解決方案

如果此練習要求你條件性地將字串轉換為小寫,你可以這樣做:str?.toLowerCase()。使用等效的方法將字串轉換為大寫!

dart
String? upperCaseIt(String? str) {
  return str?.toUpperCase();
}

集合字面量

#

Dart 對列表、對映和集合有內建支援。你可以使用字面量建立它們

dart
final aListOfStrings = ['one', 'two', 'three'];
final aSetOfStrings = {'one', 'two', 'three'};
final aMapOfStringsToInts = {'one': 1, 'two': 2, 'three': 3};

Dart 的型別推斷可以為你這些變數分配型別。在這種情況下,推斷的型別是 List<String>Set<String>Map<String, int>

或者你可以自己指定型別

dart
final aListOfInts = <int>[];
final aSetOfInts = <int>{};
final aMapOfIntToDouble = <int, double>{};

當你使用子型別的內容初始化列表,但仍希望列表為 List<BaseType> 時,指定型別會很方便

dart
final aListOfBaseType = <BaseType>[SubType(), SubType()];

練習

#

嘗試將以下變數設定為指定的值。替換現有的 null 值。

// Assign this a list containing 'a', 'b', and 'c' in that order:
final aListOfStrings = null;

// Assign this a set containing 3, 4, and 5:
final aSetOfInts = null;

// Assign this a map of String to int so that aMapOfStringsToInts['myKey'] returns 12:
final aMapOfStringsToInts = null;

// Assign this an empty List<double>:
final anEmptyListOfDouble = null;

// Assign this an empty Set<String>:
final anEmptySetOfString = null;

// Assign this an empty Map of double to int:
final anEmptyMapOfDoublesToInts = null;


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  if (aListOfStrings is! List<String>) {
    errs.add('aListOfStrings should have the type List<String>.');
  } else if (aListOfStrings.length != 3) {
    errs.add('aListOfStrings has ${aListOfStrings.length} items in it, \n rather than the expected 3.');
  } else if (aListOfStrings[0] != 'a' || aListOfStrings[1] != 'b' || aListOfStrings[2] != 'c') {
    errs.add('aListOfStrings doesn\'t contain the correct values (\'a\', \'b\', \'c\').');
  }

  if (aSetOfInts is! Set<int>) {
    errs.add('aSetOfInts should have the type Set<int>.');
  } else if (aSetOfInts.length != 3) {
    errs.add('aSetOfInts has ${aSetOfInts.length} items in it, \n rather than the expected 3.');
  } else if (!aSetOfInts.contains(3) || !aSetOfInts.contains(4) || !aSetOfInts.contains(5)) {
    errs.add('aSetOfInts doesn\'t contain the correct values (3, 4, 5).');
  }

  if (aMapOfStringsToInts is! Map<String, int>) {
    errs.add('aMapOfStringsToInts should have the type Map<String, int>.');
  } else if (aMapOfStringsToInts['myKey'] != 12) {
    errs.add('aMapOfStringsToInts doesn\'t contain the correct values (\'myKey\': 12).');
  }

  if (anEmptyListOfDouble is! List<double>) {
    errs.add('anEmptyListOfDouble should have the type List<double>.');
  } else if (anEmptyListOfDouble.isNotEmpty) {
    errs.add('anEmptyListOfDouble should be empty.');
  }

  if (anEmptySetOfString is! Set<String>) {
    errs.add('anEmptySetOfString should have the type Set<String>.');
  } else if (anEmptySetOfString.isNotEmpty) {
    errs.add('anEmptySetOfString should be empty.');
  }

  if (anEmptyMapOfDoublesToInts is! Map<double, int>) {
    errs.add('anEmptyMapOfDoublesToInts should have the type Map<double, int>.');
  } else if (anEmptyMapOfDoublesToInts.isNotEmpty) {
    errs.add('anEmptyMapOfDoublesToInts should be empty.');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }

  // ignore_for_file: unnecessary_type_check
}
集合字面量示例的解決方案

在每個等號 (=) 後新增列表、集合或對映字面量。請記住為空宣告指定型別,因為它們無法被推斷。

dart
// Assign this a list containing 'a', 'b', and 'c' in that order:
final aListOfStrings = ['a', 'b', 'c'];

// Assign this a set containing 3, 4, and 5:
final aSetOfInts = {3, 4, 5};

// Assign this a map of String to int so that aMapOfStringsToInts['myKey'] returns 12:
final aMapOfStringsToInts = {'myKey': 12};

// Assign this an empty List<double>:
final anEmptyListOfDouble = <double>[];

// Assign this an empty Set<String>:
final anEmptySetOfString = <String>{};

// Assign this an empty Map of double to int:
final anEmptyMapOfDoublesToInts = <double, int>{};

箭頭語法

#

你可能在 Dart 程式碼中見過 => 符號。這種箭頭語法是定義一個函式的方式,該函式執行其右側的表示式並返回其值。

例如,考慮對 List 類的 any() 方法的此呼叫

dart
bool hasEmpty = aListOfStrings.any((s) {
  return s.isEmpty;
});

以下是編寫該程式碼的更簡單方法

dart
bool hasEmpty = aListOfStrings.any((s) => s.isEmpty);

練習

#

嘗試完成以下使用箭頭語法的語句。

class MyClass {
  int value1 = 2;
  int value2 = 3;
  int value3 = 5;

  // Returns the product of the above values:
  int get product => TODO();

  // Adds 1 to value1:
  void incrementValue1() => TODO();

  // Returns a string containing each item in the
  // list, separated by commas (e.g. 'a,b,c'):
  String joinWithCommas(List<String> strings) => TODO();
}


// Tests your solution (Don't edit!):
void main() {
  final obj = MyClass();
  final errs = <String>[];

  try {
    final product = obj.product;

    if (product != 30) {
      errs.add('The product property returned $product \n instead of the expected value (30).');
    }
  } catch (e) {
    print('Tried to use MyClass.product, but encountered an exception: \n ${e.runtimeType}.');
    return;
  }

  try {
    obj.incrementValue1();

    if (obj.value1 != 3) {
      errs.add('After calling incrementValue, value1 was ${obj.value1} \n instead of the expected value (3).');
    }
  } catch (e) {
    print('Tried to use MyClass.incrementValue1, but encountered an exception: \n ${e.runtimeType}.');
    return;
  }

  try {
    final joined = obj.joinWithCommas(['one', 'two', 'three']);

    if (joined != 'one,two,three') {
      errs.add('Tried calling joinWithCommas([\'one\', \'two\', \'three\']) \n and received $joined instead of the expected value (\'one,two,three\').');
    }
  } catch (e) {
    print('Tried to use MyClass.joinWithCommas, but encountered an exception: \n ${e.runtimeType}.');
    return;
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
箭頭語法示例的解決方案

對於乘積,你可以使用 * 將三個值相乘。對於 incrementValue1,你可以使用增量運算子 (++)。對於 joinWithCommas,請使用 List 類中的 join 方法。

dart
class MyClass {
  int value1 = 2;
  int value2 = 3;
  int value3 = 5;

  // Returns the product of the above values:
  int get product => value1 * value2 * value3;

  // Adds 1 to value1:
  void incrementValue1() => value1++;

  // Returns a string containing each item in the
  // list, separated by commas (e.g. 'a,b,c'):
  String joinWithCommas(List<String> strings) => strings.join(',');
}

級聯操作

#

要對同一物件執行一系列操作,請使用級聯操作 (..)。我們都見過這樣的表示式

dart
myObject.someMethod()

它在 myObject 上呼叫 someMethod(),表示式的結果是 someMethod() 的返回值。

這是使用級聯的相同表示式

dart
myObject..someMethod()

雖然它仍在 myObject 上呼叫 someMethod(),但表示式的結果不是返回值——它是對 myObject 的引用!

使用級聯操作,你可以將原本需要單獨語句的操作鏈式連線起來。例如,考慮以下程式碼,它使用條件成員訪問運算子 (?.) 來讀取 button 的屬性(如果它不為 null

dart
final button = web.document.querySelector('#confirm');
button?.textContent = 'Confirm';
button?.classList.add('important');
button?.onClick.listen((e) => web.window.alert('Confirmed!'));
button?.scrollIntoView();

為了改用級聯操作,你可以從空短路級聯 (?..) 開始,它保證不會對 null 物件嘗試任何級聯操作。使用級聯操作可以縮短程式碼並使 button 變數變得不必要

dart
web.document.querySelector('#confirm')
  ?..textContent = 'Confirm'
  ..classList.add('important')
  ..onClick.listen((e) => web.window.alert('Confirmed!'))
  ..scrollIntoView();

練習

#

使用級聯操作建立一個語句,將 BigObjectanIntaStringaList 屬性分別設定為 1'String!'[3.0],然後呼叫 allDone()

class BigObject {
  int anInt = 0;
  String aString = '';
  List<double> aList = [];
  bool _done = false;

  void allDone() {
    _done = true;
  }
}

BigObject fillBigObject(BigObject obj) {
  // Create a single statement that will update and return obj:
  return TODO('obj..');
}


// Tests your solution (Don't edit!):
void main() {
  BigObject obj;

  try {
    obj = fillBigObject(BigObject());
  } catch (e) {
    print('Caught an exception of type ${e.runtimeType} \n while running fillBigObject');
    return;
  }

  final errs = <String>[];

  if (obj.anInt != 1) {
    errs.add(
        'The value of anInt was ${obj.anInt} \n rather than the expected (1).');
  }

  if (obj.aString != 'String!') {
    errs.add(
        'The value of aString was \'${obj.aString}\' \n rather than the expected (\'String!\').');
  }

  if (obj.aList.length != 1) {
    errs.add(
        'The length of aList was ${obj.aList.length} \n rather than the expected value (1).');
  } else {
    if (obj.aList[0] != 3.0) {
      errs.add(
          'The value found in aList was ${obj.aList[0]} \n rather than the expected (3.0).');
    }
  }

  if (!obj._done) {
    errs.add('It looks like allDone() wasn\'t called.');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
級聯操作示例的解決方案

此練習的最佳解決方案以 obj.. 開頭,幷包含四個鏈式賦值操作。從 return obj..anInt = 1 開始,然後新增另一個級聯 (..) 並開始下一個賦值。

dart
BigObject fillBigObject(BigObject obj) {
  return obj
    ..anInt = 1
    ..aString = 'String!'
    ..aList.add(3)
    ..allDone();
}

Getter 和 Setter

#

每當您需要對屬性的控制超出簡單欄位所允許的範圍時,就可以定義 getter 和 setter。

例如,你可以確保屬性的值有效

dart
class MyClass {
  int _aProperty = 0;

  int get aProperty => _aProperty;

  set aProperty(int value) {
    if (value >= 0) {
      _aProperty = value;
    }
  }
}

你還可以使用 getter 來定義計算屬性

dart
class MyClass {
  final List<int> _values = [];

  void addValue(int value) {
    _values.add(value);
  }

  // A computed property.
  int get count {
    return _values.length;
  }
}

練習

#

假設你有一個購物車類,它維護一個私有的價格 List<double>。新增以下內容

  • 一個名為 total 的 getter,它返回價格的總和
  • 一個 setter,用新列表替換舊列表,前提是新列表不包含任何負價格(如果包含,則 setter 應丟擲 InvalidPriceException)。

忽略 DartPad 中的所有初始錯誤。

class InvalidPriceException {}

class ShoppingCart {
  List<double> _prices = [];

  // TODO: Add a "total" getter here:

  // TODO: Add a "prices" setter here:
}


// Tests your solution (Don't edit!):
void main() {
  var foundException = false;

  try {
    final cart = ShoppingCart();
    cart.prices = [12.0, 12.0, -23.0];
  } on InvalidPriceException {
    foundException = true;
  } catch (e) {
    print('Tried setting a negative price and received a ${e.runtimeType} \n instead of an InvalidPriceException.');
    return;
  }

  if (!foundException) {
    print('Tried setting a negative price \n and didn\'t get an InvalidPriceException.');
    return;
  }

  final secondCart = ShoppingCart();

  try {
    secondCart.prices = [1.0, 2.0, 3.0];
  } catch(e) {
    print('Tried setting prices with a valid list, \n but received an exception: ${e.runtimeType}.');
    return;
  }

  if (secondCart._prices.length != 3) {
    print('Tried setting prices with a list of three values, \n but _prices ended up having length ${secondCart._prices.length}.');
    return;
  }

  if (secondCart._prices[0] != 1.0 || secondCart._prices[1] != 2.0 || secondCart._prices[2] != 3.0) {
    final vals = secondCart._prices.map((p) => p.toString()).join(', ');
    print('Tried setting prices with a list of three values (1, 2, 3), \n but incorrect ones ended up in the price list ($vals) .');
    return;
  }

  var sum = 0.0;

  try {
    sum = secondCart.total;
  } catch (e) {
    print('Tried to get total, but received an exception: ${e.runtimeType}.');
    return;
  }

  if (sum != 6.0) {
    print('After setting prices to (1, 2, 3), total returned $sum instead of 6.');
    return;
  }

  print('Success!');
}
Getter 和 Setter 示例的解決方案

此練習有兩個方便的函式。一個是 fold,它可以將列表歸約為單個值(用它來計算總和)。另一個是 any,它可以檢查列表中每個專案是否符合你提供的函式(用它來檢查 prices setter 中是否存在任何負價格)。

dart
/// The total price of the shopping cart.
double get total => _prices.fold(0, (e, t) => e + t);

/// Set [prices] to the [value] list of item prices.
set prices(List<double> value) {
  if (value.any((p) => p < 0)) {
    throw InvalidPriceException();
  }

  _prices = value;
}

可選位置引數

#

Dart 有兩種函式引數:位置引數和命名引數。位置引數是你可能熟悉的型別

dart
int sumUp(int a, int b, int c) {
  return a + b + c;
}
  // ···
  int total = sumUp(1, 2, 3);

在 Dart 中,你可以透過將這些位置引數用括號括起來使它們成為可選引數

dart
int sumUpToFive(int a, [int? b, int? c, int? d, int? e]) {
  int sum = a;
  if (b != null) sum += b;
  if (c != null) sum += c;
  if (d != null) sum += d;
  if (e != null) sum += e;
  return sum;
}
  // ···
  int total = sumUpToFive(1, 2);
  int otherTotal = sumUpToFive(1, 2, 3, 4, 5);

可選位置引數始終位於函式引數列表的末尾。它們的預設值為 null,除非你提供了另一個預設值

dart
int sumUpToFive(int a, [int b = 2, int c = 3, int d = 4, int e = 5]) {
  // ···
}

void main() {
  int newTotal = sumUpToFive(1);
  print(newTotal); // <-- prints 15
}

練習

#

實現一個名為 joinWithCommas() 的函式,它接受一到五個整數,然後返回一個由這些數字用逗號分隔組成的字串。以下是函式呼叫和返回值的一些示例

函式呼叫返回值
joinWithCommas(1)'1'
joinWithCommas(1, 2, 3)'1,2,3'
joinWithCommas(1, 1, 1, 1, 1)'1,1,1,1,1'

String joinWithCommas(int a, [int? b, int? c, int? d, int? e]) {
  return TODO();
}


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  try {
    final value = joinWithCommas(1);

    if (value != '1') {
      errs.add('Tried calling joinWithCommas(1) \n and got $value instead of the expected (\'1\').');
    }
  } on UnimplementedError {
    print('Tried to call joinWithCommas but failed. \n Did you implement the method?');
    return;
  } catch (e) {
    print('Tried calling joinWithCommas(1), \n but encountered an exception: ${e.runtimeType}.');
    return;
  }

  try {
    final value = joinWithCommas(1, 2, 3);

    if (value != '1,2,3') {
      errs.add('Tried calling joinWithCommas(1, 2, 3) \n and got $value instead of the expected (\'1,2,3\').');
    }
  } on UnimplementedError {
    print('Tried to call joinWithCommas but failed. \n Did you implement the method?');
    return;
  } catch (e) {
    print('Tried calling joinWithCommas(1, 2 ,3), \n but encountered an exception: ${e.runtimeType}.');
    return;
  }

  try {
    final value = joinWithCommas(1, 2, 3, 4, 5);

    if (value != '1,2,3,4,5') {
      errs.add('Tried calling joinWithCommas(1, 2, 3, 4, 5) \n and got $value instead of the expected (\'1,2,3,4,5\').');
    }
  } on UnimplementedError {
    print('Tried to call joinWithCommas but failed. \n Did you implement the method?');
    return;
  } catch (e) {
    print('Tried calling stringify(1, 2, 3, 4 ,5), \n but encountered an exception: ${e.runtimeType}.');
    return;
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
位置引數示例的解決方案

如果呼叫者未提供 bcde 引數,則它們為 null。那麼,重要的是在將這些引數新增到最終字串之前檢查它們是否為 null

dart
String joinWithCommas(int a, [int? b, int? c, int? d, int? e]) {
  var total = '$a';
  if (b != null) total = '$total,$b';
  if (c != null) total = '$total,$c';
  if (d != null) total = '$total,$d';
  if (e != null) total = '$total,$e';
  return total;
}

命名引數

#

透過在引數列表末尾使用大括號語法,你可以定義具有名稱的引數。

命名引數是可選的,除非它們被顯式標記為 required

dart
void printName(String firstName, String lastName, {String? middleName}) {
  print('$firstName ${middleName ?? ''} $lastName');
}

void main() {
  printName('Dash', 'Dartisan');
  printName('John', 'Smith', middleName: 'Who');
  // Named arguments can be placed anywhere in the argument list.
  printName('John', middleName: 'Who', 'Smith');
}

正如你所料,可空命名引數的預設值為 null,但你可以提供自定義的預設值。

如果引數的型別不可為空,則你必須提供一個預設值(如以下程式碼所示)或將引數標記為 required(如建構函式部分所示)。

dart
void printName(String firstName, String lastName, {String middleName = ''}) {
  print('$firstName $middleName $lastName');
}

函式不能同時擁有可選位置引數和命名引數。

練習

#

copyWith() 例項方法新增到 MyDataObject 類。它應接受三個命名、可空引數

  • int? newInt
  • String? newString
  • double? newDouble

你的 copyWith() 方法應基於當前例項返回一個新的 MyDataObject,並將前述引數(如果有)中的資料複製到物件的屬性中。例如,如果 newInt 非空,則將其值複製到 anInt 中。

忽略 DartPad 中的所有初始錯誤。

class MyDataObject {
  final int anInt;
  final String aString;
  final double aDouble;

  MyDataObject({
     this.anInt = 1,
     this.aString = 'Old!',
     this.aDouble = 2.0,
  });

  // TODO: Add your copyWith method here:
}


// Tests your solution (Don't edit!):
void main() {
  final source = MyDataObject();
  final errs = <String>[];

  try {
    final copy = source.copyWith(newInt: 12, newString: 'New!', newDouble: 3.0);

    if (copy.anInt != 12) {
      errs.add('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0), \n and the new object\'s anInt was ${copy.anInt} rather than the expected value (12).');
    }

    if (copy.aString != 'New!') {
      errs.add('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0), \n and the new object\'s aString was ${copy.aString} rather than the expected value (\'New!\').');
    }

    if (copy.aDouble != 3) {
      errs.add('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0), \n and the new object\'s aDouble was ${copy.aDouble} rather than the expected value (3).');
    }
  } catch (e) {
    print('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0) \n and got an exception: ${e.runtimeType}');
  }

  try {
    final copy = source.copyWith();

    if (copy.anInt != 1) {
      errs.add('Called copyWith(), and the new object\'s anInt was ${copy.anInt} \n rather than the expected value (1).');
    }

    if (copy.aString != 'Old!') {
      errs.add('Called copyWith(), and the new object\'s aString was ${copy.aString} \n rather than the expected value (\'Old!\').');
    }

    if (copy.aDouble != 2) {
      errs.add('Called copyWith(), and the new object\'s aDouble was ${copy.aDouble} \n rather than the expected value (2).');
    }
  } catch (e) {
    print('Called copyWith() and got an exception: ${e.runtimeType}');
  }

  try {
    final sourceWithoutDefaults = MyDataObject(
      anInt: 520,
      aString: 'Custom!',
      aDouble: 20.25,
    );
    final copy = sourceWithoutDefaults.copyWith();

    if (copy.anInt == 1) {
      errs.add('Called `copyWith()` on an object with a non-default `anInt` value (${sourceWithoutDefaults.anInt}), but the new object\'s `anInt` was the default value of ${copy.anInt}.');
    }

    if (copy.aString == 'Old!') {
      errs.add('Called `copyWith()` on an object with a non-default `aString` value (\'${sourceWithoutDefaults.aString}\'), but the new object\'s `aString` was the default value of \'${copy.aString}\'.');
    }

    if (copy.aDouble == 2.0) {
      errs.add('Called copyWith() on an object with a non-default `aDouble` value (${sourceWithoutDefaults.aDouble}), but the new object\'s `aDouble` was the default value of ${copy.aDouble}.');
    }
  } catch (e) {
    print('Called copyWith() and got an exception: ${e.runtimeType}');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
命名引數示例的解決方案

copyWith 方法出現在許多類和庫中。你的方法應該做幾件事:使用可選命名引數,建立一個新的 MyDataObject 例項,並使用引數中的資料(如果引數為 null,則使用當前例項中的資料)填充它。這是練習 ?? 運算子的機會!

dart
MyDataObject copyWith({int? newInt, String? newString, double? newDouble}) {
  return MyDataObject(
    anInt: newInt ?? this.anInt,
    aString: newString ?? this.aString,
    aDouble: newDouble ?? this.aDouble,
  );
}

異常

#

Dart 程式碼可以丟擲和捕獲異常。與 Java 不同,Dart 的所有異常都是未檢查的。方法不宣告它們可能丟擲哪些異常,你也不必捕獲任何異常。

Dart 提供了 ExceptionError 型別,但你可以丟擲任何非 null 物件

dart
throw Exception('Something bad happened.');
throw 'Waaaaaaah!';

處理異常時使用 tryoncatch 關鍵詞

dart
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');
}

try 關鍵詞的作用與其他大多數語言相同。使用 on 關鍵詞按型別篩選特定異常,使用 catch 關鍵詞獲取異常物件的引用。

如果你無法完全處理該異常,請使用 rethrow 關鍵詞來傳播異常

dart
try {
  breedMoreLlamas();
} catch (e) {
  print('I was just trying to breed llamas!');
  rethrow;
}

無論是否丟擲異常,要執行程式碼,請使用 finally

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

練習

#

在下方實現 tryFunction()。它應該執行一個不可靠的方法,然後執行以下操作

  • 如果 untrustworthy() 丟擲 ExceptionWithMessage,則使用異常型別和訊息呼叫 logger.logException(嘗試使用 oncatch)。
  • 如果 untrustworthy() 丟擲 Exception,則使用異常型別呼叫 logger.logException(嘗試為此使用 on)。
  • 如果 untrustworthy() 丟擲任何其他物件,則不要捕獲該異常。
  • 在所有異常都被捕獲和處理後,呼叫 logger.doneLogging(嘗試使用 finally)。
typedef VoidFunction = void Function();

class ExceptionWithMessage {
  final String message;
  const ExceptionWithMessage(this.message);
}

// Call logException to log an exception, and doneLogging when finished.
abstract class Logger {
  void logException(Type t, [String? msg]);
  void doneLogging();
}

void tryFunction(VoidFunction untrustworthy, Logger logger) {
  try {
    untrustworthy();
  } // Write your logic here
}

// Tests your solution (Don't edit!):
class MyLogger extends Logger {
  Type? lastType;
  String lastMessage = '';
  bool done = false;

  void logException(Type t, [String? message]) {
    lastType = t;
    lastMessage = message ?? lastMessage;
  }

  void doneLogging() => done = true;
}

void main() {
  final errs = <String>[];
  var logger = MyLogger();

  try {
    tryFunction(() => throw Exception(), logger);

    if ('${logger.lastType}' != 'Exception' && '${logger.lastType}' != '_Exception') {
      errs.add('Untrustworthy threw an Exception, but a different type was logged: \n ${logger.lastType}.');
    }

    if (logger.lastMessage != '') {
      errs.add('Untrustworthy threw an Exception with no message, but a message \n was logged anyway: \'${logger.lastMessage}\'.');
    }

    if (!logger.done) {
      errs.add('Untrustworthy threw an Exception, \n and doneLogging() wasn\'t called afterward.');
    }
  } catch (e) {
    print('Untrustworthy threw an exception, and an exception of type \n ${e.runtimeType} was unhandled by tryFunction.');
  }

  logger = MyLogger();

  try {
    tryFunction(() => throw ExceptionWithMessage('Hey!'), logger);

    if (logger.lastType != ExceptionWithMessage) {
      errs.add('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), but a \n different type was logged: ${logger.lastType}.');
    }

    if (logger.lastMessage != 'Hey!') {
      errs.add('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), but a \n different message was logged: \'${logger.lastMessage}\'.');
    }

    if (!logger.done) {
      errs.add('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), \n and doneLogging() wasn\'t called afterward.');
    }
  } catch (e) {
    print('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), \n and an exception of type ${e.runtimeType} was unhandled by tryFunction.');
  }

  logger = MyLogger();
  bool caughtStringException = false;

  try {
    tryFunction(() => throw 'A String', logger);
  } on String {
    caughtStringException = true;
  }

  if (!caughtStringException) {
    errs.add('Untrustworthy threw a string, and it was incorrectly handled inside tryFunction().');
  }

  logger = MyLogger();

  try {
    tryFunction(() {}, logger);

    if (logger.lastType != null) {
      errs.add('Untrustworthy didn\'t throw an Exception, \n but one was logged anyway: ${logger.lastType}.');
    }

    if (logger.lastMessage != '') {
      errs.add('Untrustworthy didn\'t throw an Exception with no message, \n but a message was logged anyway: \'${logger.lastMessage}\'.');
    }

    if (!logger.done) {
      errs.add('Untrustworthy didn\'t throw an Exception, \n but doneLogging() wasn\'t called afterward.');
    }
  } catch (e) {
    print('Untrustworthy didn\'t throw an exception, \n but an exception of type ${e.runtimeType} was unhandled by tryFunction anyway.');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
異常示例的解決方案

這個練習看起來很棘手,但它實際上是一個大的 try 語句。在 try 內部呼叫 untrustworthy,然後使用 oncatchfinally 來捕獲異常並在 logger 上呼叫方法。

dart
void tryFunction(VoidFunction untrustworthy, Logger logger) {
  try {
    untrustworthy();
  } on ExceptionWithMessage catch (e) {
    logger.logException(e.runtimeType, e.message);
  } on Exception {
    logger.logException(Exception);
  } finally {
    logger.doneLogging();
  }
}

在建構函式中使用 this

#

Dart 提供了一個方便的快捷方式,用於在建構函式中為屬性賦值:在宣告建構函式時使用 this.propertyName

dart
class MyColor {
  int red;
  int green;
  int blue;

  MyColor(this.red, this.green, this.blue);
}

final color = MyColor(80, 80, 128);

此技術也適用於命名引數。屬性名成為引數名

dart
class MyColor {
  // ...

  MyColor({required this.red, required this.green, required this.blue});
}

final color = MyColor(red: 80, green: 80, blue: 80);

在前面的程式碼中,redgreenblue 被標記為 required,因為這些 int 值不能為 null。如果你新增預設值,則可以省略 required

dart
MyColor([this.red = 0, this.green = 0, this.blue = 0]);
// or
MyColor({this.red = 0, this.green = 0, this.blue = 0});

練習

#

MyClass 新增一個單行建構函式,該函式使用 this. 語法接收併為類的所有三個屬性賦值。

忽略 DartPad 中的所有初始錯誤。

class MyClass {
  final int anInt;
  final String aString;
  final double aDouble;

  // TODO: Create the constructor here.
}


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  try {
    final obj = MyClass(1, 'two', 3);

    if (obj.anInt != 1) {
      errs.add('Called MyClass(1, \'two\', 3) and got an object with anInt of ${obj.anInt} \n instead of the expected value (1).');
    }

    if (obj.anInt != 1) {
      errs.add('Called MyClass(1, \'two\', 3) and got an object with aString of \'${obj.aString}\' \n instead of the expected value (\'two\').');
    }

    if (obj.anInt != 1) {
      errs.add('Called MyClass(1, \'two\', 3) and got an object with aDouble of ${obj.aDouble} \n instead of the expected value (3).');
    }
  } catch (e) {
    print('Called MyClass(1, \'two\', 3) and got an exception \n of type ${e.runtimeType}.');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
this 示例的解決方案

此練習有一個單行解決方案。按照 this.anIntthis.aStringthis.aDouble 的順序宣告建構函式作為其引數。

dart
MyClass(this.anInt, this.aString, this.aDouble);

初始化列表

#

有時在實現建構函式時,你需要在建構函式體執行之前進行一些設定。例如,final 欄位在建構函式體執行之前必須有值。在初始化列表中完成此工作,該列表位於建構函式的簽名與其主體之間

dart
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

初始化列表也是放置斷言的方便位置,斷言僅在開發期間執行

dart
NonNegativePoint(this.x, this.y) : assert(x >= 0), assert(y >= 0) {
  print('I just made a NonNegativePoint: ($x, $y)');
}

練習

#

完成下面的 FirstTwoLetters 建構函式。使用初始化列表將 word 中的前兩個字元賦值給 letterOneLetterTwo 屬性。作為額外加分項,新增一個 assert 來捕獲少於兩個字元的單詞。

忽略 DartPad 中的所有初始錯誤。

class FirstTwoLetters {
  final String letterOne;
  final String letterTwo;

  // TODO: Create a constructor with an initializer list here:
  FirstTwoLetters(String word)

}


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  try {
    final result = FirstTwoLetters('My String');

    if (result.letterOne != 'M') {
      errs.add('Called FirstTwoLetters(\'My String\') and got an object with \n letterOne equal to \'${result.letterOne}\' instead of the expected value (\'M\').');
    }

    if (result.letterTwo != 'y') {
      errs.add('Called FirstTwoLetters(\'My String\') and got an object with \n letterTwo equal to \'${result.letterTwo}\' instead of the expected value (\'y\').');
    }
  } catch (e) {
    errs.add('Called FirstTwoLetters(\'My String\') and got an exception \n of type ${e.runtimeType}.');
  }

  bool caughtException = false;

  try {
    FirstTwoLetters('');
  } catch (e) {
    caughtException = true;
  }

  if (!caughtException) {
    errs.add('Called FirstTwoLetters(\'\') and didn\'t get an exception \n from the failed assertion.');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
初始化列表示例的解決方案

需要進行兩次賦值:letterOne 應該被賦值為 word[0]letterTwo 應該被賦值為 word[1]

dart
  FirstTwoLetters(String word)
      : assert(word.length >= 2),
        letterOne = word[0],
        letterTwo = word[1];

命名建構函式

#

為了允許類有多個建構函式,Dart 支援命名建構函式

dart
class Point {
  double x, y;

  Point(this.x, this.y);

  Point.origin() : x = 0, y = 0;
}

要使用命名建構函式,請使用其完整名稱呼叫它

dart
final myPoint = Point.origin();

練習

#

Color 類一個名為 Color.black 的建構函式,它將所有三個屬性設定為零。

忽略 DartPad 中的所有初始錯誤。

class Color {
  int red;
  int green;
  int blue;

  Color(this.red, this.green, this.blue);

  // TODO: Create a named constructor called "Color.black" here:
}


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  try {
    final result = Color.black();

    if (result.red != 0) {
      errs.add('Called Color.black() and got a Color with red equal to \n ${result.red} instead of the expected value (0).');
    }

    if (result.green != 0) {
      errs.add('Called Color.black() and got a Color with green equal to \n ${result.green} instead of the expected value (0).');
    }

    if (result.blue != 0) {
  errs.add('Called Color.black() and got a Color with blue equal to \n ${result.blue} instead of the expected value (0).');
    }
  } catch (e) {
    print('Called Color.black() and got an exception of type \n ${e.runtimeType}.');
    return;
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
命名建構函式示例的解決方案

你的建構函式宣告應以 Color.black(): 開頭。在初始化列表(冒號後)中,將 redgreenblue 設定為 0

dart
Color.black() : red = 0, green = 0, blue = 0;

工廠建構函式

#

Dart 支援工廠建構函式,它可以返回子型別甚至 null。要建立工廠建構函式,請使用 factory 關鍵詞

dart
class Square extends Shape {}

class Circle extends Shape {}

class Shape {
  Shape();

  factory Shape.fromTypeName(String typeName) {
    if (typeName == 'square') return Square();
    if (typeName == 'circle') return Circle();

    throw ArgumentError('Unrecognized $typeName');
  }
}

練習

#

替換名為 IntegerHolder.fromList 的工廠建構函式中的 TODO(); 行,以返回以下內容

  • 如果列表有一個值,則使用該值建立一個 IntegerSingle 例項。
  • 如果列表有兩個值,則按順序使用這些值建立一個 IntegerDouble 例項。
  • 如果列表有三個值,則按順序使用這些值建立一個 IntegerTriple 例項。
  • 否則,丟擲 Error

如果你成功,控制檯應顯示 Success!

class IntegerHolder {
  IntegerHolder();

  // Implement this factory constructor.
  factory IntegerHolder.fromList(List<int> list) {
    TODO();
  }
}

class IntegerSingle extends IntegerHolder {
  final int a;

  IntegerSingle(this.a);
}

class IntegerDouble extends IntegerHolder {
  final int a;
  final int b;

  IntegerDouble(this.a, this.b);
}

class IntegerTriple extends IntegerHolder {
  final int a;
  final int b;
  final int c;

  IntegerTriple(this.a, this.b, this.c);
}

// Tests your solution (Don't edit from this point to end of file):
void main() {
  final errs = <String>[];

  // Run 5 tests to see which values have valid integer holders.
  for (var tests = 0; tests < 5; tests++) {
    if (!testNumberOfArgs(errs, tests)) return;
  }

  // The goal is no errors with values 1 to 3,
  // but have errors with values 0 and 4.
  // The testNumberOfArgs method adds to the errs array if
  // the values 1 to 3 have an error and
  // the values 0 and 4 don't have an error.
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

bool testNumberOfArgs(List<String> errs, int count) {
  bool _threw = false;
  final ex = List.generate(count, (index) => index + 1);
  final callTxt = "IntegerHolder.fromList(${ex})";
  try {
    final obj = IntegerHolder.fromList(ex);
    final String vals = count == 1 ? "value" : "values";
    // Uncomment the next line if you want to see the results realtime
    // print("Testing with ${count} ${vals} using ${obj.runtimeType}.");
    testValues(errs, ex, obj, callTxt);
  } on Error {
    _threw = true;
  } catch (e) {
    switch (count) {
      case (< 1 && > 3):
        if (!_threw) {
          errs.add('Called ${callTxt} and it didn\'t throw an Error.');
        }
      default:
        errs.add('Called $callTxt and received an Error.');
    }
  }
  return true;
}

void testValues(List<String> errs, List<int> expectedValues, IntegerHolder obj,
    String callText) {
  for (var i = 0; i < expectedValues.length; i++) {
    int found;
    if (obj is IntegerSingle) {
      found = obj.a;
    } else if (obj is IntegerDouble) {
      found = i == 0 ? obj.a : obj.b;
    } else if (obj is IntegerTriple) {
      found = i == 0
          ? obj.a
          : i == 1
              ? obj.b
              : obj.c;
    } else {
      throw ArgumentError(
          "This IntegerHolder type (${obj.runtimeType}) is unsupported.");
    }

    if (found != expectedValues[i]) {
      errs.add(
          "Called $callText and got a ${obj.runtimeType} " +
          "with a property at index $i value of $found " +
          "instead of the expected (${expectedValues[i]}).");
    }
  }
}
工廠建構函式示例的解決方案

在工廠建構函式內部,檢查列表的長度,然後酌情建立並返回一個 IntegerSingleIntegerDoubleIntegerTriple 例項。

TODO(); 替換為以下程式碼塊。

dart
  switch (list.length) {
    case 1:
      return IntegerSingle(list[0]);
    case 2:
      return IntegerDouble(list[0], list[1]);
    case 3:
      return IntegerTriple(list[0], list[1], list[2]);
    default:
      throw ArgumentError("List must between 1 and 3 items. This list was ${list.length} items.");
  }

重定向建構函式

#

有時,建構函式唯一目的是重定向到同一類中的另一個建構函式。重定向建構函式的主體為空,建構函式調用出現在冒號 (:) 之後。

dart
class Automobile {
  String make;
  String model;
  int mpg;

  // The main constructor for this class.
  Automobile(this.make, this.model, this.mpg);

  // Delegates to the main constructor.
  Automobile.hybrid(String make, String model) : this(make, model, 60);

  // Delegates to a named constructor
  Automobile.fancyHybrid() : this.hybrid('Futurecar', 'Mark 2');
}

練習

#

還記得上面的 Color 類嗎?建立一個名為 black 的命名建構函式,但不是手動分配屬性,而是將其重定向到預設建構函式,引數為零。

忽略 DartPad 中的所有初始錯誤。

class Color {
  int red;
  int green;
  int blue;

  Color(this.red, this.green, this.blue);

  // TODO: Create a named constructor called "black" here
  // and redirect it to call the existing constructor
}


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  try {
    final result = Color.black();

    if (result.red != 0) {
      errs.add('Called Color.black() and got a Color with red equal to \n ${result.red} instead of the expected value (0).');
    }

    if (result.green != 0) {
      errs.add('Called Color.black() and got a Color with green equal to \n ${result.green} instead of the expected value (0).');
    }

    if (result.blue != 0) {
  errs.add('Called Color.black() and got a Color with blue equal to \n ${result.blue} instead of the expected value (0).');
    }
  } catch (e) {
    print('Called Color.black() and got an exception of type ${e.runtimeType}.');
    return;
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
重定向建構函式示例的解決方案

你的建構函式應重定向到 this(0, 0, 0)

dart
Color.black() : this(0, 0, 0);

常量建構函式

#

如果你的類生成永不改變的物件,你可以使這些物件成為編譯時常量。為此,請定義一個 const 建構函式,並確保所有例項變數都是 final 的。

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

  final int x;
  final int y;

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

練習

#

修改 Recipe 類,使其例項可以成為常量,並建立一個常量建構函式,該函式執行以下操作

  • 有三個引數:ingredientscaloriesmilligramsOfSodium(按此順序)。
  • 使用 this. 語法自動將引數值賦值給同名的物件屬性。
  • 是常量,在建構函式宣告中的 Recipe 之前使用 const 關鍵詞。

忽略 DartPad 中的所有初始錯誤。

class Recipe {
  List<String> ingredients;
  int calories;
  double milligramsOfSodium;

  // TODO: Create a const constructor here.

}


// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];

  try {
    const obj = Recipe(['1 egg', 'Pat of butter', 'Pinch salt'], 120, 200);

    if (obj.ingredients.length != 3) {
      errs.add('Called Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and got an object with ingredient list of length ${obj.ingredients.length} rather than the expected length (3).');
    }

    if (obj.calories != 120) {
      errs.add('Called Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and got an object with a calorie value of ${obj.calories} rather than the expected value (120).');
    }

    if (obj.milligramsOfSodium != 200) {
      errs.add('Called Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and got an object with a milligramsOfSodium value of ${obj.milligramsOfSodium} rather than the expected value (200).');
    }

    try {
      obj.ingredients.add('Sugar to taste');
      errs.add('Tried adding an item to the \'ingredients\' list of a const Recipe and didn\'t get an error due to it being unmodifiable.');
    } on UnsupportedError catch (_) {
      // We expect an `UnsupportedError` due to
      // `ingredients` being a const, unmodifiable list.
    }
  } catch (e) {
    print('Tried calling Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and received a null.');
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
常量建構函式示例的解決方案

要使建構函式為 const,你需要將所有屬性設為 final。

dart
class Recipe {
  final List<String> ingredients;
  final int calories;
  final double milligramsOfSodium;

  const Recipe(this.ingredients, this.calories, this.milligramsOfSodium);
}

接下來是什麼?

#

我們希望你喜歡使用本教程來學習或測試你對 Dart 語言一些最有趣特性的瞭解。

接下來你可以嘗試