内容

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,否则值不能为 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 中的空安全性,请阅读健全的空安全性指南

代码示例

#

尝试在下面声明两个变量

  • 一个名为 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 的运算符是 ??,它返回其左侧的表达式,除非该表达式的值为 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}.');
  }
  
}
支持 null 的运算符示例的解决方案

您需要做的就是用 ????= 替换 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()];

代码示例

#

尝试将以下变量设置为指示的值。替换现有的空值。

// 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
var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();

要改为使用级联,你可以从null 缩短级联 (?..) 开始,它保证不会对 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) {
  // 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,它可以使用您给它的函数检查列表中的每个项目(使用它来检查价格 setter 中是否存在任何负价格)。

dart
// 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 有两种函数参数:位置参数和命名参数。位置参数是您可能熟悉的类型

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() 的函数,该函数接受 1 到 5 个整数,然后返回一个用逗号分隔的数字字符串。以下是一些函数调用和返回值的示例

函数调用返回值
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');
}

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

代码示例

#

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

  • 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}');
  }
  
  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 类型,但允许你抛出任何非空对象

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) {
  // Invoking this method might cause an exception. 
  // TODO: Catch and handle them using try-on-catch-finally.
  untrustworthy();
}


// 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 来捕获异常并在记录器上调用方法。

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 的工厂构造函数,使其执行以下操作

  • 如果列表有一个值,则使用该值创建一个 IntegerSingle
  • 如果列表有两个值,则按顺序使用这些值创建一个 IntegerDouble
  • 如果列表有三个值,则按顺序使用这些值创建一个 IntegerTriple
  • 否则,抛出 Error
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!):
void main() {
  final errs = <String>[];

  bool _throwed = false;
  try {
    IntegerHolder.fromList([]);
  } on UnimplementedError {
    print('Test failed. Did you implement the method?');
    return;
  } on Error {
    _throwed = true;
  } catch (e) {
    print('Called IntegerSingle.fromList([]) and got an exception of \n type ${e.runtimeType}.');
    return;
  }
  
  if (!_throwed) {
    errs.add('Called IntegerSingle.fromList([]) and didn\'t throw Error.');
  } 

  try {
    final obj = IntegerHolder.fromList([1]);
    
    if (obj is! IntegerSingle) {
      errs.add('Called IntegerHolder.fromList([1]) and got an object of type \n ${obj.runtimeType} instead of IntegerSingle.');
    } else {
      if (obj.a != 1) {
        errs.add('Called IntegerHolder.fromList([1]) and got an IntegerSingle with \n  an \'a\' value of ${obj.a} instead of the expected (1).');
      }
    }
  } catch (e) {
    print('Called IntegerHolder.fromList([]) and got an exception of \n type ${e.runtimeType}.');
    return;
  }

  try {
    final obj = IntegerHolder.fromList([1, 2]);
    
    if (obj is! IntegerDouble) {
      errs.add('Called IntegerHolder.fromList([1, 2]) and got an object of type \n ${obj.runtimeType} instead of IntegerDouble.');
    } else {
      if (obj.a != 1) {
        errs.add('Called IntegerHolder.fromList([1, 2]) and got an IntegerDouble \n with an \'a\' value of ${obj.a} instead of the expected (1).');
      }
      
      if (obj.b != 2) {
        errs.add('Called IntegerHolder.fromList([1, 2]) and got an IntegerDouble \n with an \'b\' value of ${obj.b} instead of the expected (2).');
      }
    }
  } catch (e) {
    print('Called IntegerHolder.fromList([1, 2]) and got an exception \n of type ${e.runtimeType}.');
    return;
  }

  try {
    final obj = IntegerHolder.fromList([1, 2, 3]);
    
    if (obj is! IntegerTriple) {
      errs.add('Called IntegerHolder.fromList([1, 2, 3]) and got an object of type \n ${obj.runtimeType} instead of IntegerTriple.');
    } else {
      if (obj.a != 1) {
        errs.add('Called IntegerHolder.fromList([1, 2, 3]) and got an IntegerTriple \n with an \'a\' value of ${obj.a} instead of the expected (1).');
      }
      
      if (obj.b != 2) {
        errs.add('Called IntegerHolder.fromList([1, 2, 3]) and got an IntegerTriple \n with an \'a\' value of ${obj.b} instead of the expected (2).');
      }

      if (obj.c != 3) {
        errs.add('Called IntegerHolder.fromList([1, 2, 3]) and got an IntegerTriple \n with an \'a\' value of ${obj.b} instead of the expected (2).');
      }
    }
  } catch (e) {
    print('Called IntegerHolder.fromList([1, 2, 3]) and got an exception \n of type ${e.runtimeType}.');
    return;
  }

  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
工厂构造函数示例的解决方案

在工厂构造函数中,检查列表的长度,然后根据需要创建并返回 IntegerSingleIntegerDoubleIntegerTriple

dart
  factory IntegerHolder.fromList(List<int> list) {
    if (list.length == 1) {
      return IntegerSingle(list[0]);
    } else if (list.length == 2) {
      return IntegerDouble(list[0], list[1]);
    } else if (list.length == 3) {
      return IntegerTriple(list[0], list[1], list[2]);
    } else {
      throw Error();
    } 
  }

重定向构造函数

#

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

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 构造函数

#

如果您的类生成永不更改的对象,则可以将这些对象设为编译时常量。为此,请定义 const 构造函数,并确保所有实例变量都是最终的。

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).');
    }
  } 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 构造函数示例的解决方案

要使构造函数成为 const,您需要使所有属性都成为 final。

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

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

接下来是什么?

#

我们希望您喜欢使用此代码实验室来学习或测试您对 Dart 语言一些最有趣功能的了解。以下是一些关于现在该做什么的建议