Dart 速查表
- 字串插值
- 可為 null 的變數
- 可感知空值的運算子
- 條件屬性存取
- 集合字面值
- 箭頭語法
- 串聯
- Getter 和 Setter
- 可選的位置參數
- 具名參數
- 例外
- 在建構子中使用 this
- 初始化清單
- 具名建構子
- 工廠建構子
- 重新導向建構子
- 常數建構子
- 下一步?
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!');
}
字串插值範例的解決方案
x
和 y
都是簡單的值,而 Dart 的字串插值會處理將它們轉換為字串表示法。您只需要使用 $
運算子,在單引號內參考它們,並在它們之間加上空格即可
String stringify(int x, int y) {
return '$x $y';
}
可為 null 的變數
#Dart 會強制執行健全的空值安全。這表示除非您說它們可以,否則值不能為 null。換句話說,類型預設為不可為 null。
例如,請考慮下列程式碼。在空值安全的情況下,此程式碼會傳回錯誤。類型為 int
的變數不能有值 null
int a = null; // INVALID.
建立變數時,請在類型中加入 ?
,以指出變數可以為 null
int? a = null; // Valid.
您可以簡化該程式碼一些,因為在所有 Dart 版本中,null
都是未初始化變數的預設值
int? a; // The initial value of a is null.
若要深入瞭解 Dart 中的空值安全,請閱讀健全的空值安全指南。
程式碼範例
#在此 DartPad 中宣告兩個變數
- 一個名為
name
且值為'Jane'
的可為 null 的String
。 - 一個名為
address
且值為null
的可為 null 的String
。
忽略 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}');
}
}
可為 null 的變數範例的解決方案
將這兩個變數宣告為 String
,後面加上 ?
。然後,將 'Jane'
指派給 name
,並讓 address
保持未初始化
String? name = 'Jane';
String? address;
可感知空值的運算子
#Dart 提供一些方便的運算子,用於處理可能為 null 的值。其中一個是 ??=
指派運算子,只有在變數目前為 null 時,才會將值指派給變數
int? a; // = null
a ??= 3;
print(a); // <-- Prints 3.
a ??= 5;
print(a); // <-- Still prints 3.
另一個可感知空值的運算子是 ??
,除非左邊運算式的值為 null,否則它會傳回左邊的運算式,如果左邊運算式的值為 null,則會評估並傳回右邊的運算式
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
註解取代為 ??
或 ??=
。請閱讀上面的文字,以確保您了解這兩者,然後嘗試一下
// 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 的物件的屬性或方法的存取,請在點號 (.
) 前面加上問號 (?
)
myObject?.someProperty
上述程式碼等同於下列程式碼
(myObject != null) ? myObject.someProperty : null
您可以在單一運算式中串連多次使用 ?.
myObject?.someProperty?.someMethod()
如果 myObject
或 myObject.someProperty
為 null,上述程式碼會傳回 null (且永遠不會呼叫 someMethod()
)。
程式碼範例
#下列函式會將可為 null 的字串當作參數。嘗試使用條件屬性存取,使其傳回 str
的大寫版本,如果 str
為 null
,則傳回 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()
。使用對等的方法來將字串轉換為大寫!
String? upperCaseIt(String? str) {
return str?.toUpperCase();
}
集合字面值
#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>
。
或者您可以自行指定類型
final aListOfInts = <int>[];
final aSetOfInts = <int>{};
final aMapOfIntToDouble = <int, double>{};
當您使用子類型的內容初始化清單,但仍然希望清單為 List<BaseType>
時,指定類型很方便
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
}
集合字面值範例的解決方案
在每個等號後面加入清單、集合或對應字面值。請記得指定空白宣告的類型,因為它們無法被推斷。
// 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()
方法的此呼叫
bool hasEmpty = aListOfStrings.any((s) {
return s.isEmpty;
});
以下是撰寫該程式碼的更簡單方式
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
方法。
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(',');
}
串聯
#若要在同一個物件上執行一連串的作業,請使用串聯 (..
)。我們都看過類似這樣的運算式
myObject.someMethod()
它會在 myObject
上叫用 someMethod()
,且運算式的結果是 someMethod()
的傳回值。
以下是使用串聯的相同運算式
myObject..someMethod()
雖然它仍然會在 myObject
上叫用 someMethod()
,但運算式的結果不是傳回值,而是 myObject
的參考!
使用串聯,您可以將原本需要個別陳述式的作業鏈結在一起。例如,請考慮下列程式碼,它會使用條件成員存取運算子 (?.
) 來讀取 button
的屬性,如果它不是 null
var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();
若要改為使用串聯,您可以從空值短路串聯 (?..
) 開始,這可確保不會對 null
物件嘗試任何串聯作業。使用串聯可以縮短程式碼並讓 button
變數變得不必要
querySelector('#confirm')
?..text = 'Confirm'
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();
程式碼範例
#使用串聯來建立單一陳述式,該陳述式會將 BigObject
的 anInt
、aString
和 aList
屬性分別設定為 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
開始,然後添加另一個級聯 (..
) 並開始下一個賦值。
BigObject fillBigObject(BigObject obj) {
return obj
..anInt = 1
..aString = 'String!'
..aList.add(3)
..allDone();
}
Getter 和 Setter
#當您需要比簡單的欄位允許的更多屬性控制時,可以隨時定義 getter 和 setter。
例如,您可以確保屬性的值是有效的
class MyClass {
int _aProperty = 0;
int get aProperty => _aProperty;
set aProperty(int value) {
if (value >= 0) {
_aProperty = value;
}
}
}
您還可以使用 getter 來定義計算屬性
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
,它可以使用您提供的函式檢查列表中的每個項目(使用它來檢查價格 setter 中是否有任何負價格)。
// Add a "total" getter here:
double get total => _prices.fold(0, (e, t) => e + t);
// Add a "prices" setter here:
set prices(List<double> value) {
if (value.any((p) => p < 0)) {
throw InvalidPriceException();
}
_prices = value;
}
可選的位置參數
#Dart 有兩種函式參數:位置參數和具名參數。位置參數是您可能熟悉的那種
int sumUp(int a, int b, int c) {
return a + b + c;
}
// ···
int total = sumUp(1, 2, 3);
使用 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,除非您提供另一個預設值
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);
}
}
位置參數範例的解決方案
如果呼叫者未提供 b
、c
、d
和 e
參數,則這些參數為 null。因此,重要的是在將這些引數添加到最終字串之前,檢查它們是否為 null
。
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
。
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 的具名參數的預設值為 null
,但您可以提供自訂預設值。
如果參數的類型不可為 null,則您必須提供預設值(如下列程式碼所示)或將參數標記為 required
(如 建構子區段 中所示)。
void printName(String firstName, String lastName, {String middleName = ''}) {
print('$firstName $middleName $lastName');
}
函式不能同時具有可選的位置參數和具名參數。
程式碼範例
#將 copyWith()
實例方法添加到 MyDataObject
類別。它應該採用三個可為 null 的具名參數
int? newInt
String? newString
double? newDouble
您的 copyWith()
方法應根據目前的實例傳回一個新的 MyDataObject
,並將前述參數(如果有)中的資料複製到物件的屬性中。例如,如果 newInt
不為 null,則將其值複製到 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}');
}
if (errs.isEmpty) {
print('Success!');
} else {
errs.forEach(print);
}
}
具名參數範例的解決方案
copyWith
方法在許多類別和函式庫中都會出現。您的方法應該執行一些操作:使用可選的具名參數,建立 MyDataObject
的新實例,並使用參數中的資料來填寫它(如果參數為 null,則使用目前實例中的資料)。這是一個練習更多 ??
運算符的好機會!
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 提供 Exception
和 Error
類型,但您可以使用任何非 null 的物件
throw Exception('Something bad happened.');
throw 'Waaaaaaah!';
處理例外時,請使用 try
、on
和 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');
}
try
關鍵字的工作方式與大多數其他語言相同。使用 on
關鍵字按類型篩選特定的例外,並使用 catch
關鍵字取得例外物件的參照。
如果您無法完全處理例外,請使用 rethrow
關鍵字傳播例外
try {
breedMoreLlamas();
} catch (e) {
print('I was just trying to breed llamas!');
rethrow;
}
要執行程式碼而不論是否拋出例外,請使用 finally
try {
breedMoreLlamas();
} catch (e) {
// ... handle exception ...
} finally {
// Always clean up, even if an exception is thrown.
cleanLlamaStalls();
}
程式碼範例
#實作下面的 tryFunction()
。它應該執行一個不可信任的方法,然後執行以下操作
- 如果
untrustworthy()
拋出ExceptionWithMessage
,則使用例外類型和訊息呼叫logger.logException
(嘗試使用on
和catch
)。 - 如果
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();
} on ExceptionWithMessage catch (e) {
logger.logException(e.runtimeType, e.message);
} on Exception catch (e) {
logger.logException(e.runtimeType);
} finally {
logger.doneLogging();
}
}
// 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
,然後使用 on
、catch
和 finally
來捕獲例外並在記錄器上呼叫方法。
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
class MyColor {
int red;
int green;
int blue;
MyColor(this.red, this.green, this.blue);
}
final color = MyColor(80, 80, 128);
此技術也適用於具名參數。屬性名稱會成為參數的名稱
class MyColor {
...
MyColor({required this.red, required this.green, required this.blue});
}
final color = MyColor(red: 80, green: 80, blue: 80);
在上述程式碼中,red
、green
和 blue
被標記為 required
,因為這些 int
值不能為 null。如果您新增預設值,則可以省略 required
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.anInt
、this.aString
和 this.aDouble
作為其依序排列的參數來宣告建構子。
MyClass(this.anInt, this.aString, this.aDouble);
初始化清單
#有時,當您實作建構子時,您需要在建構子主體執行之前執行一些設定。例如,final 欄位必須在建構子主體執行之前具有值。在初始化程式清單中執行此工作,該清單位於建構子的簽章及其主體之間
Point.fromJson(Map<String, double> json)
: x = json['x']!,
y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}
初始化程式清單也是放置斷言的好地方,斷言只會在開發期間執行
NonNegativePoint(this.x, this.y)
: assert(x >= 0),
assert(y >= 0) {
print('I just made a NonNegativePoint: ($x, $y)');
}
程式碼範例
#完成下面的 FirstTwoLetters
建構子。使用初始化程式清單將 word
中的前兩個字元賦予 letterOne
和 LetterTwo
屬性。如需額外加分,請新增一個 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]
。
FirstTwoLetters(String word)
: assert(word.length >= 2),
letterOne = word[0],
letterTwo = word[1];
具名建構子
#為了讓類別有多個建構子,Dart 支援具名建構子
class Point {
double x, y;
Point(this.x, this.y);
Point.origin()
: x = 0,
y = 0;
}
若要使用具名建構子,請使用其完整名稱來調用它
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():
開始。在初始化程式清單(冒號之後)中,將 red
、green
和 blue
設為 0
。
Color.black()
: red = 0,
green = 0,
blue = 0;
工廠建構子
#Dart 支援工廠建構子,它們可以傳回子類型或甚至 null。若要建立工廠建構子,請使用 factory
關鍵字
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]}).");
}
}
}
工廠建構子範例的解決方案
在工廠建構子內部,檢查列表的長度,然後建立並傳回適當的 IntegerSingle
、IntegerDouble
或 IntegerTriple
。
使用下列程式碼區塊取代 TODO();
。
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.");
}
重新導向建構子
#有時,建構子的唯一目的是重新導向到同一類別中的另一個建構子。重新導向建構子的主體為空,且建構子呼叫會出現在冒號 (:
) 之後。
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)
。
Color.black() : this(0, 0, 0);
常數建構子
#如果您的類別產生永不變更的物件,您可以將這些物件設為編譯時常數。若要執行此操作,請定義 const
建構子並確保所有實例變數都是 final。
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final int x;
final int y;
const ImmutablePoint(this.x, this.y);
}
程式碼範例
#修改 Recipe
類別,使其能夠常數化其實例,並建立一個常數建構子,執行下列操作
- 有三個參數:
ingredients
、calories
和milligramsOfSodium
(依序)。 - 使用
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).');
}
} 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。
class Recipe {
final List<String> ingredients;
final int calories;
final double milligramsOfSodium;
const Recipe(this.ingredients, this.calories, this.milligramsOfSodium);
}
下一步?
#我們希望您喜歡使用本教學課程來學習或測試您對 Dart 語言一些最有趣的功能的知識。
您可以嘗試的下一步包括
- 嘗試其他 Dart 教學課程。
- 閱讀Dart 語言導覽。
- 使用 DartPad.
- 取得 Dart SDK.
除非另有說明,否則本網站上的文件反映的是 Dart 3.6.0。頁面最後更新時間為 2024-08-04。檢視來源或回報問題。