目录

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('在此处返回格式化的字符串');
}


// 测试你的解决方案(不要编辑!):
void main() {
  assert(stringify(2, 3) == '2 3',
      "你的 stringify 方法返回 '${stringify(2, 3)}' 而不是 '2 3'");
  print('成功!');
}
字符串插值示例的解决方案

xy 都是简单值,Dart 的字符串插值将处理将其转换为字符串表示形式。你只需使用 $ 运算符在单引号内引用它们,并在它们之间添加一个空格:

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

可空变量

#

Dart 强制执行健全的空安全。这意味着值不能为 null,除非你声明它们可以为 null。换句话说,类型默认为不可空。

例如,考虑以下代码。在空安全的情况下,此代码会返回错误。 int 类型的变量不能具有 null 值:

dart
int a = null; // 无效。

创建变量时,在类型后添加 ? 以指示变量可以为 null

dart
int? a = null; // 有效。

你可以简化这段代码,因为在所有版本的 Dart 中, null 都是未初始化变量的默认值:

dart
int? a; // a 的初始值为 null。

要了解有关 Dart 中空安全的更多信息,请阅读 健全的空安全指南

代码示例

#

在此 DartPad 中声明两个变量:

  • 一个名为 name 的可空 String ,其值为 'Jane'
  • 一个名为 address 的可空 String ,其值为 null

忽略 DartPad 中的所有初始错误。

// TODO:在此处声明两个变量


// 测试你的解决方案(不要编辑!):
void main() {
  try {
    if (name == 'Jane' && address == null) {
      // 验证“name”是否可空
      name = null;
      print('成功!');
    } else {
      print('不太正确,请再试一次!');
    }
  } catch (e) {
    print('异常:${e.runtimeType}');
  }
}
可空变量示例的解决方案

将两个变量声明为 String 后跟 ? 。然后,将 'Jane' 分配给 name 并使 address 未初始化:

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

空感知运算符

#

Dart 提供了一些方便的运算符来处理可能为 null 的值。一个是 ??= 赋值运算符,它仅当变量当前为 null 时才为变量赋值:

dart
int? a; // = null
a ??= 3;
print(a); // <-- 打印 3。

a ??= 5;
print(a); // <-- 仍然打印 3。

另一个空感知运算符是 ?? ,它返回左侧的表达式,除非该表达式的值为 null,在这种情况下,它会计算并返回右侧的表达式:

dart
print(1 ?? 3); // <-- 打印 1。
print(null ?? 12); // <-- 打印 12。

代码示例

#

尝试替换 ??=?? 运算符以在以下代码段中实现所描述的行为。

忽略 DartPad 中的所有初始错误。

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

// 替换一个运算符,使 'a string' 分配给 baz。
String? baz = foo /* TODO */ bar;

void updateSomeVars() {
  // 替换一个运算符,使 'a string' 分配给 bar。
  bar /* TODO */ 'a string';
}


// 测试你的解决方案(不要编辑!):
void main() {
  try {
    updateSomeVars();

    if (foo != 'a string') {
      print('看起来 foo 以某种方式最终获得了错误的值。');
    } else if (bar != 'a string') {
      print('看起来 bar 最终获得了错误的值。');
    } else if (baz != 'a string') {
      print('看起来 baz 最终获得了错误的值。');
    } else {
      print('成功!');
    }
  } catch (e) {
    print('异常:${e.runtimeType}。');
  }

}
空感知运算符示例的解决方案

在本练习中,你只需要用 ????= 替换 TODO 注释即可。阅读上面的文本以确保你理解两者,然后试一试:

dart
// 替换一个运算符,使 'a string' 分配给 baz。
String? baz = foo ?? bar;

void updateSomeVars() {
  // 替换一个运算符,使 'a string' 分配给 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:尝试在此处有条件地访问 `toUpperCase` 方法。
}


// 测试你的解决方案(不要编辑!):
void main() {
  try {
    String? one = upperCaseIt(null);
    if (one != null) {
      print('看起来你没有为 null 输入返回 null。');
    } else {
      print('str 为 null 时成功!');
    }
  } catch (e) {
    print('尝试调用 upperCaseIt(null) 并出现异常:\n ${e.runtimeType}。');
  }

  try {
    String? two = upperCaseIt('a string');
    if (two == null) {
      print('看起来即使 str 有值,你也返回 null。');
    } else if (two != 'A STRING') {
      print('尝试 upperCaseIt(\'a string\'),但没有得到 \'A STRING\' 作为响应。');
    } else {
      print('str 不为 null 时成功!');
    }
  } catch (e) {
    print('尝试调用 upperCaseIt(\'a string\') 并出现异常:\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 值。

// 将其分配给一个按顺序包含 'a'、'b' 和 'c' 的列表:
final aListOfStrings = null;

// 将其分配给一个包含 3、4 和 5 的集合:
final aSetOfInts = null;

// 将其分配给一个 String 到 int 的映射,以便 aMapOfStringsToInts['myKey'] 返回 12:
final aMapOfStringsToInts = null;

// 将其分配给一个空的 List<double>:
final anEmptyListOfDouble = null;

// 将其分配给一个空的 Set<String>:
final anEmptySetOfString = null;

// 将其分配给一个空的 double 到 int 的映射:
final anEmptyMapOfDoublesToInts = null;


// 测试你的解决方案(不要编辑!):
void main() {
  final errs = <String>[];

  if (aListOfStrings is! List<String>) {
    errs.add('aListOfStrings 应具有 List<String> 类型。');
  } else if (aListOfStrings.length != 3) {
    errs.add('aListOfStrings 包含 ${aListOfStrings.length} 个项目,\n而不是预期的 3 个。');
  } else if (aListOfStrings[0] != 'a' || aListOfStrings[1] != 'b' || aListOfStrings[2] != 'c') {
    errs.add('aListOfStrings 不包含正确的值(\'a\'、\'b\'、\'c\')。');
  }

  if (aSetOfInts is! Set<int>) {
    errs.add('aSetOfInts 应具有 Set<int> 类型。');
  } else if (aSetOfInts.length != 3) {
    errs.add('aSetOfInts 包含 ${aSetOfInts.length} 个项目,\n而不是预期的 3 个。');
  } else if (!aSetOfInts.contains(3) || !aSetOfInts.contains(4) || !aSetOfInts.contains(5)) {
    errs.add('aSetOfInts 不包含正确的值 (3、4、5)。');
  }

  if (aMapOfStringsToInts is! Map<String, int>) {
    errs.add('aMapOfStringsToInts 应具有 Map<String, int> 类型。');
  } else if (aMapOfStringsToInts['myKey'] != 12) {
    errs.add('aMapOfStringsToInts 不包含正确的值(\'myKey\': 12)。');
  }

  if (anEmptyListOfDouble is! List<double>) {
    errs.add('anEmptyListOfDouble 应具有 List<double> 类型。');
  } else if (anEmptyListOfDouble.isNotEmpty) {
    errs.add('anEmptyListOfDouble 应为空。');
  }

  if (anEmptySetOfString is! Set<String>) {
    errs.add('anEmptySetOfString 应具有 Set<String> 类型。');
  } else if (anEmptySetOfString.isNotEmpty) {
    errs.add('anEmptySetOfString 应为空。');
  }

  if (anEmptyMapOfDoublesToInts is! Map<double, int>) {
    errs.add('anEmptyMapOfDoublesToInts 应具有 Map<double, int> 类型。');
  } else if (anEmptyMapOfDoublesToInts.isNotEmpty) {
    errs.add('anEmptyMapOfDoublesToInts 应为空。');
  }

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

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

在每个等号后添加列表、集合或映射字面量。记住为空的声明指定类型,因为它们无法推断出来。

dart
// 将其分配给一个按顺序包含 'a'、'b' 和 'c' 的列表:
final aListOfStrings = ['a', 'b', 'c'];

// 将其分配给一个包含 3、4 和 5 的集合:
final aSetOfInts = {3, 4, 5};

// 将其分配给一个 String 到 int 的映射,以便 aMapOfStringsToInts['myKey'] 返回 12:
final aMapOfStringsToInts = {'myKey': 12};

// 将其分配给一个空的 List<double>:
final anEmptyListOfDouble = <double>[];

// 将其分配给一个空的 Set<String>:
final anEmptySetOfString = <String>{};

// 将其分配给一个空的 double 到 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;

  // 返回上述值的乘积:
  int get product => TODO();

  // 将 value1 加 1:
  void incrementValue1() => TODO();

  // 返回一个包含列表中每个项目并以逗号分隔的字符串(例如 'a,b,c'):
  String joinWithCommas(List<String> strings) => TODO();
}


// 测试你的解决方案(不要编辑!):
void main() {
  final obj = MyClass();
  final errs = <String>[];

  try {
    final product = obj.product;

    if (product != 30) {
      errs.add('product 属性返回 $product \n 而不是预期值 (30)。');
    }
  } catch (e) {
    print('尝试使用 MyClass.product,但遇到异常:\n ${e.runtimeType}。');
    return;
  }

  try {
    obj.incrementValue1();

    if (obj.value1 != 3) {
      errs.add('调用 incrementValue 后,value1 为 ${obj.value1} \n 而不是预期值 (3)。');
    }
  } catch (e) {
    print('尝试使用 MyClass.incrementValue1,但遇到异常:\n ${e.runtimeType}。');
    return;
  }

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

    if (joined != 'one,two,three') {
      errs.add('尝试调用 joinWithCommas([\'one\', \'two\', \'three\']) \n 并收到 $joined 而不是预期值 (\'one,two,three\')。');
    }
  } catch (e) {
    print('尝试使用 MyClass.joinWithCommas,但遇到异常:\n ${e.runtimeType}。');
    return;
  }

  if (errs.isEmpty) {
    print('成功!');
  } else {
    errs.forEach(print);
  }
}
箭头语法示例的解决方案

对于 product,你可以使用 * 将三个值相乘。对于 incrementValue1 ,你可以使用递增运算符 (++)。对于 joinWithCommas ,使用 List 类中找到的 join 方法。

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

  // 返回上述值的乘积:
  int get product => value1 * value2 * value3;

  // 将 value1 加 1:
  void incrementValue1() => value1++;

  // 返回一个包含列表中每个项目并以逗号分隔的字符串(例如 '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
var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();

要改为使用级联,你可以从 空值短路 级联 (?..) 开始,它保证不会在 null 对象上尝试任何级联操作。使用级联可以缩短代码并使 button 变量变得不必要:

dart
querySelector('#confirm')
  ?..text = 'Confirm'
  ..classes.add('important')
  ..onClick.listen((e) => 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) {
  // 创建一个将更新和返回 obj 的单一语句:
  return TODO('obj..');
}


// 测试你的解决方案(不要编辑!):
void main() {
  BigObject obj;

  try {
    obj = fillBigObject(BigObject());
  } catch (e) {
    print('在运行 fillBigObject 时捕获到 ${e.runtimeType} 类型的异常');
    return;
  }

  final errs = <String>[];

  if (obj.anInt != 1) {
    errs.add(
        'anInt 的值为 ${obj.anInt} \n 而不是预期的 (1)。');
  }

  if (obj.aString != 'String!') {
    errs.add(
        'aString 的值为 \'${obj.aString}\' \n 而不是预期的 (\'String!\')。');
  }

  if (obj.aList.length != 1) {
    errs.add(
        'aList 的长度为 ${obj.aList.length} \n 而不是预期的值 (1)。');
  } else {
    if (obj.aList[0] != 3.0) {
      errs.add(
          'aList 中的值为 ${obj.aList[0]} \n 而不是预期的 (3.0)。');
    }
  }

  if (!obj._done) {
    errs.add('看起来 allDone() 没有被调用。');
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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);
  }

  // 计算属性。
  int get count {
    return _values.length;
  }
}

代码示例

#

假设你有一个购物车类,它保留一个私有的 List<double> 价格列表。 添加以下内容:

  • 一个名为 total 的 getter,返回价格总和
  • 一个 setter,用新的列表替换旧列表,只要新列表不包含任何负价格(在这种情况下,setter 应抛出 InvalidPriceException )。

忽略 DartPad 中的所有初始错误。

class InvalidPriceException {}

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

  // TODO:在此处添加“total”getter:

  // TODO:在此处添加“prices”setter:
}


// 测试你的解决方案(不要编辑!):
void main() {
  var foundException = false;

  try {
    final cart = ShoppingCart();
    cart.prices = [12.0, 12.0, -23.0];
  } on InvalidPriceException {
    foundException = true;
  } catch (e) {
    print('尝试设置负价格并收到 ${e.runtimeType} \n 而不是 InvalidPriceException。');
    return;
  }

  if (!foundException) {
    print('尝试设置负价格 \n 但没有收到 InvalidPriceException。');
    return;
  }

  final secondCart = ShoppingCart();

  try {
    secondCart.prices = [1.0, 2.0, 3.0];
  } catch (e) {
    print('尝试使用有效列表设置价格,\n 但收到异常:${e.runtimeType}。');
    return;
  }

  if (secondCart._prices.length != 3) {
    print('尝试使用包含三个值的列表设置价格,\n 但 _prices 最终的长度为 ${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('尝试使用包含三个值 (1、2、3) 的列表设置价格,\n 但价格列表中最终包含不正确的数值 ($vals)。');
    return;
  }

  var sum = 0.0;

  try {
    sum = secondCart.total;
  } catch (e) {
    print('尝试获取总计,但收到异常:${e.runtimeType}。');
    return;
  }

  if (sum != 6.0) {
    print('将价格设置为 (1、2、3) 后,total 返回 $sum 而不是 6。');
    return;
  }

  print('成功!');
}
getter 和 setter 示例的解决方案

本练习中两个函数非常实用。一个是 fold ,它可以将列表简化为单个值(使用它来计算总计)。另一个是 any ,它可以使用你提供的函数检查列表中的每个项目(使用它来检查 prices setter 中是否存在任何负价格)。

dart
// 在此处添加“total”getter:
double get total => _prices.fold(0, (e, t) => e + t);

// 在此处添加“prices”setter:
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); // <-- 打印 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();
}


// 测试你的解决方案(不要编辑!):
void main() {
  final errs = <String>[];

  try {
    final value = joinWithCommas(1);

    if (value != '1') {
      errs.add('尝试调用 joinWithCommas(1) \n 并得到 $value 而不是预期的 (\'1\')。');
    }
  } on UnimplementedError {
    print('尝试调用 joinWithCommas 但失败。\n 你实现此方法了吗?');
    return;
  } catch (e) {
    print('尝试调用 joinWithCommas(1),\n 但遇到异常:${e.runtimeType}。');
    return;
  }

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

    if (value != '1,2,3') {
      errs.add('尝试调用 joinWithCommas(1, 2, 3) \n 并得到 $value 而不是预期的 (\'1,2,3\')。');
    }
  } on UnimplementedError {
    print('尝试调用 joinWithCommas 但失败。\n 你实现此方法了吗?');
    return;
  } catch (e) {
    print('尝试调用 joinWithCommas(1, 2 ,3),\n 但遇到异常:${e.runtimeType}。');
    return;
  }

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

    if (value != '1,2,3,4,5') {
      errs.add('尝试调用 joinWithCommas(1, 2, 3, 4, 5) \n 并得到 $value 而不是预期的 (\'1,2,3,4,5\')。');
    }
  } on UnimplementedError {
    print('尝试调用 joinWithCommas 但失败。\n 你实现此方法了吗?');
    return;
  } catch (e) {
    print('尝试调用 stringify(1, 2, 3, 4 ,5),\n 但遇到异常:${e.runtimeType}。');
    return;
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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');
  // 命名参数可以放在参数列表中的任何位置
  printName('John', middleName: 'Who', 'Smith');
}

正如你可能预期的那样,可空命名参数的默认值为 null ,但你可以提供自定义默认值。

如果参数的类型是不可空的,那么你必须要么提供默认值(如下面的代码所示),要么将参数标记为 required (如 构造函数部分 所示)。

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

函数不能同时具有可选位置参数和命名参数。

代码示例

#

MyDataObject 类添加 copyWith() 实例方法。它应该采用三个命名可空参数:

  • 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:在此处添加你的 copyWith 方法:
}


// 测试你的解决方案(不要编辑!):
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('调用 copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0),\n 新对象的 anInt 为 ${copy.anInt} 而不是预期的值 (12)。');
    }

    if (copy.aString != 'New!') {
      errs.add('调用 copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0),\n 新对象的 aString 为 ${copy.aString} 而不是预期的值 (\'New!\')。');
    }

    if (copy.aDouble != 3) {
      errs.add('调用 copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0),\n 新对象的 aDouble 为 ${copy.aDouble} 而不是预期的值 (3)。');
    }
  } catch (e) {
    print('调用 copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0) \n 并出现异常:${e.runtimeType}');
  }

  try {
    final copy = source.copyWith();

    if (copy.anInt != 1) {
      errs.add('调用 copyWith(),新对象的 anInt 为 ${copy.anInt} \n 而不是预期的值 (1)。');
    }

    if (copy.aString != 'Old!') {
      errs.add('调用 copyWith(),新对象的 aString 为 ${copy.aString} \n 而不是预期的值 (\'Old!\')。');
    }

    if (copy.aDouble != 2) {
      errs.add('调用 copyWith(),新对象的 aDouble 为 ${copy.aDouble} \n 而不是预期的值 (2)。');
    }
  } catch (e) {
    print('调用 copyWith() 并出现异常:${e.runtimeType}');
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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 {
  // 特定异常
  buyMoreLlamas();
} on Exception catch (e) {
  // 其他任何异常
  print('未知异常:$e');
} catch (e) {
  // 未指定类型,处理所有异常
  print('真正未知的异常:$e');
}

try 关键字的工作方式与大多数其他语言中的方式相同。使用 on 关键字按类型过滤特定异常,使用 catch 关键字获取对异常对象的引用。

如果无法完全处理异常,请使用 rethrow 关键字来传播异常:

dart
try {
  breedMoreLlamas();
} catch (e) {
  print('我当时只是想繁殖羊驼!');
  rethrow;
}

要执行代码,无论是否抛出异常,请使用 finally

dart
try {
  breedMoreLlamas();
} catch (e) {
  // ... 处理异常 ...
} finally {
  // 始终清理,即使抛出异常。
  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);
}

// 调用 logException 记录异常,并在完成后调用 doneLogging。
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();
  }
}

// 测试你的解决方案(不要编辑!):
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 抛出 Exception,但记录了不同的类型:\n ${logger.lastType}。');
    }

    if (logger.lastMessage != '') {
      errs.add('Untrustworthy 抛出没有消息的 Exception,但仍然记录了消息:\n \'${logger.lastMessage}\'。');
    }

    if (!logger.done) {
      errs.add('Untrustworthy 抛出 Exception,\n 之后没有调用 doneLogging()。');
    }
  } catch (e) {
    print('Untrustworthy 抛出异常,并且 tryFunction 未处理类型为 \n ${e.runtimeType} 的异常。');
  }

  logger = MyLogger();

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

    if (logger.lastType != ExceptionWithMessage) {
      errs.add('Untrustworthy 抛出 ExceptionWithMessage(\'Hey!\'),但记录了不同的类型:\n ${logger.lastType}。');
    }

    if (logger.lastMessage != 'Hey!') {
      errs.add('Untrustworthy 抛出 ExceptionWithMessage(\'Hey!\'),但记录了不同的消息:\n \'${logger.lastMessage}\'。');
    }

    if (!logger.done) {
      errs.add('Untrustworthy 抛出 ExceptionWithMessage(\'Hey!\'),\n 之后没有调用 doneLogging()。');
    }
  } catch (e) {
    print('Untrustworthy 抛出 ExceptionWithMessage(\'Hey!\'),\n tryFunction 未处理类型为 ${e.runtimeType} 的异常。');
  }

  logger = MyLogger();
  bool caughtStringException = false;

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

  if (!caughtStringException) {
    errs.add('Untrustworthy 抛出字符串,tryFunction() 未正确处理。');
  }

  logger = MyLogger();

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

    if (logger.lastType != null) {
      errs.add('Untrustworthy 未抛出 Exception,\n 但仍然记录了异常:${logger.lastType}。');
    }

    if (logger.lastMessage != '') {
      errs.add('Untrustworthy 未抛出没有消息的 Exception,\n 但仍然记录了消息:\'${logger.lastMessage}\'。');
    }

    if (!logger.done) {
      errs.add('Untrustworthy 未抛出 Exception,\n 但之后没有调用 doneLogging()。');
    }
  } catch (e) {
    print('Untrustworthy 未抛出异常,\n 但 tryFunction 仍然未处理类型为 ${e.runtimeType} 的异常。');
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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:在此处创建构造函数。
}


// 测试你的解决方案(不要编辑!):
void main() {
  final errs = <String>[];

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

    if (obj.anInt != 1) {
      errs.add('调用 MyClass(1, \'two\', 3) 并得到一个 anInt 为 ${obj.anInt} \n 而不是预期值 (1) 的对象。');
    }

    if (obj.anInt != 1) {
      errs.add('调用 MyClass(1, \'two\', 3) 并得到一个 aString 为 \'${obj.aString}\' \n 而不是预期值 (\'two\') 的对象。');
    }

    if (obj.anInt != 1) {
      errs.add('调用 MyClass(1, \'two\', 3) 并得到一个 aDouble 为 ${obj.aDouble} \n 而不是预期值 (3) 的对象。');
    }
  } catch (e) {
    print('调用 MyClass(1, \'two\', 3) 并出现类型为 \n ${e.runtimeType} 的异常。');
  }

  if (errs.isEmpty) {
    print('成功!');
  } else {
    errs.forEach(print);
  }
}
`this` 示例的解决方案

本练习有一个单行解决方案。使用 this.anIntthis.aStringthis.aDouble 作为参数按此顺序声明构造函数。

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

初始化列表

#

有时,在实现构造函数时,需要在构造函数体执行之前进行一些设置。例如,最终字段必须在构造函数体执行之前具有值。在初始化列表中执行此工作,初始化列表位于构造函数的签名和其主体之间:

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:在此处创建一个带有初始化列表的构造函数:
  FirstTwoLetters(String word)

}


// 测试你的解决方案(不要编辑!):
void main() {
  final errs = <String>[];

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

    if (result.letterOne != 'M') {
      errs.add('调用 FirstTwoLetters(\'My String\') 并得到一个 letterOne 等于 \'${result.letterOne}\' \n 而不是预期值 (\'M\') 的对象。');
    }

    if (result.letterTwo != 'y') {
      errs.add('调用 FirstTwoLetters(\'My String\') 并得到一个 letterTwo 等于 \'${result.letterTwo}\' \n 而不是预期值 (\'y\') 的对象。');
    }
  } catch (e) {
    errs.add('调用 FirstTwoLetters(\'My String\') 并出现类型为 \n ${e.runtimeType} 的异常。');
  }

  bool caughtException = false;

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

  if (!caughtException) {
    errs.add('调用 FirstTwoLetters(\'\') 但没有从失败的断言中得到异常。');
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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:在此处创建一个名为“Color.black”的命名构造函数:

}


// 测试你的解决方案(不要编辑!):
void main() {
  final errs = <String>[];

  try {
    final result = Color.black();

    if (result.red != 0) {
      errs.add('调用 Color.black() 并得到一个 red 等于 \n ${result.red} 而不是预期值 (0) 的 Color。');
    }

    if (result.green != 0) {
      errs.add('调用 Color.black() 并得到一个 green 等于 \n ${result.green} 而不是预期值 (0) 的 Color。');
    }

    if (result.blue != 0) {
      errs.add('调用 Color.black() 并得到一个 blue 等于 \n ${result.blue} 而不是预期值 (0) 的 Color。');
    }
  } catch (e) {
    print('调用 Color.black() 并出现类型为 \n ${e.runtimeType} 的异常。');
    return;
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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();

  // 实现此工厂构造函数。
  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);
}

// 测试你的解决方案(从此处到文件末尾不要编辑):
void main() {
  final errs = <String>[];

  // 运行 5 次测试以查看哪些值具有有效的整数持有者
  for (var tests = 0; tests < 5; tests++) {
    if (!testNumberOfArgs(errs, tests)) return;
  }

  // 目标是没有 1 到 3 的值的错误,
  // 但 0 和 4 的值有错误。
  // 如果 1 到 3 的值有错误并且
  // 0 和 4 的值没有错误,则 testNumberOfArgs 方法会向 errs 数组添加内容
  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";
    // 如果你想实时查看结果,请取消注释下一行
    // 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('调用 ${callTxt} 但没有抛出 Error。');
        }
      default:
        errs.add('调用 $callTxt 并收到 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(
          "此 IntegerHolder 类型 (${obj.runtimeType}) 不受支持。");
    }

    if (found != expectedValues[i]) {
      errs.add(
          "调用 $callText 并得到一个 ${obj.runtimeType} " +
              "其索引为 $i 的属性值为 $found " +
              "而不是预期的 (${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("列表必须包含 1 到 3 个项目。此列表包含 ${list.length} 个项目。");
  }

重定向构造函数

#

有时,构造函数的唯一目的是重定向到同一类中的另一个构造函数。重定向构造函数的主体为空,构造函数调用出现在冒号 (:) 之后。

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

  // 此类的主要构造函数。
  Automobile(this.make, this.model, this.mpg);

  // 委托给主要构造函数。
  Automobile.hybrid(String make, String model) : this(make, model, 60);

  // 委托给命名构造函数
  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:在此处创建一个名为“black”的命名构造函数
  // 并将其重定向到调用现有的构造函数
}


// 测试你的解决方案(不要编辑!):
void main() {
  final errs = <String>[];

  try {
    final result = Color.black();

    if (result.red != 0) {
      errs.add('调用 Color.black() 并得到一个 red 等于 \n ${result.red} 而不是预期值 (0) 的 Color。');
    }

    if (result.green != 0) {
      errs.add('调用 Color.black() 并得到一个 green 等于 \n ${result.green} 而不是预期值 (0) 的 Color。');
    }

    if (result.blue != 0) {
      errs.add('调用 Color.black() 并得到一个 blue 等于 \n ${result.blue} 而不是预期值 (0) 的 Color。');
    }
  } catch (e) {
    print('调用 Color.black() 并出现类型为 ${e.runtimeType} 的异常。');
    return;
  }

  if (errs.isEmpty) {
    print('成功!');
  } 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. 语法将参数值自动分配给相同名称的对象属性。
  • 使用 const 关键字在构造函数声明中的 Recipe 之前声明为常量。

忽略 DartPad 中的所有初始错误。

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

  // TODO:在此处创建一个常量构造函数"

}


// 测试你的解决方案(不要编辑!):
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('调用 Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n 并得到一个 ingredient 列表长度为 ${obj.ingredients.length} 而不是预期长度 (3) 的对象。');
    }

    if (obj.calories != 120) {
      errs.add('调用 Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n 并得到一个卡路里值为 ${obj.calories} 而不是预期值 (120) 的对象。');
    }

    if (obj.milligramsOfSodium != 200) {
      errs.add('调用 Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n 并得到一个 milligramsOfSodium 值为 ${obj.milligramsOfSodium} 而不是预期值 (200) 的对象。');
    }
  } catch (e) {
    print('尝试调用 Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n 并收到 null。');
  }

  if (errs.isEmpty) {
    print('成功!');
  } else {
    errs.forEach(print);
  }
}
常量构造函数示例的解决方案

要使构造函数成为常量,需要将所有属性设为 final。

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

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

下一步?

#

我们希望你喜欢使用本教程来学习或测试你对 Dart 语言一些最有趣特性的知识。

接下来你可以尝试: