Dart 语言快速入门

0
(0)
内容 隐藏

基本概念

  • 可以放在变量里的所有东西都是对象,每个对象都是类的实例。包括数字、函数、null 都是对象。除了 null,所有对象继承自 Object
  • Dart是强类型(Strongly typed)语言,但有类型推导
  • 只有声明为Nullable的变量才可以包含 null,例如 int? 。如果你知道一个表达式永远不会为 null,可以通过 ! 断言它不为 null(为 null 时会抛出异常),例如 int x = nullableButNotNullInt!
  • 通过 Object?Object 显式声明允许任何类型,或者使用 dynamic 将类型检查推迟到运行时
  • Dart支持泛型,例如 List<int>
  • 与Java不同,Dart没有 publicprotectedprivate 关键字。如果标识符以下划线 _ 开头,则它对它的库是私有的
  • 单行注释 //、多行注释 /**/、文档注释 ////**,可以在文档注释中使用方括号引用类、方法、字段、顶级变量、函数、参数

变量 (Variables)

  • Dart使用健全的Null safety
    • 控制变量是否 NullableString? name
    • Nullable 变量默认被初始化为 nullNon-nullable 变量没有初始值。Dart不允许访问未初始化的变量,这会阻止你访问 Nullable 的 Receiver’s Type 中 null 不支持的属性或方法
    • 不能访问具有可空类型的表达式的属性或调用方法,除非它是 null 支持的
  • Nullable 类型的初始值为 null,即便是数字也是。
  • Top-level 和类变量会延迟初始化:初始化代码在第一次使用变量时运行

late 变量

late 修饰符用于:

  • 声明一个 Non-nullable 变量,它在声明后初始化
  • 延迟初始化变量

通常Dart可以检测变量在使用前是否被设置为非空值,但顶级变量和实例变量可能无法确定。如果你确定一个变量在使用前已初始化,但Dart不同意,你可以将其标记为 late

late String description;

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

如果没有初始化late变量,使用时会抛出运行时错误

如果将一个变量标记为 late 但在声明时进行了初始化,那么初始化代码将在第一次使用变量时运行,用于:

  • 变量可能不需要,并且初始化成本很高
  • 正在初始化一个实例变量,且初始化需要访问 this

final 和 const

使用 finalconst 代替 var 或写在类型前,声明不可变变量
final 变量只可设置一次,const 变量是编译期常量(且是隐式的 final)必须在声明时初始化

  • 注意:实例变量可以声明为 final,但不可以是 const
  • 如果 const 变量属于类级别,标记它为 static const

const 关键字不仅用于声明常量,还可以用它创建常量值,以及声明创建常量值的构造函数,任何变量都可以有一个常量值

var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`
//const 变量的初始化表达式中可以省略 const

可以更改非 finalconst 变量的值,即使它曾有一个 const

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

可以定义使用类型检查、强制转换(isas)、集合 if、和扩展运算符(......?)的常量

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

运算符 (Operators)

  • 可以将大部分运算符实现为类成员
  • 对于需要两个操作数的运算符,最左边的操作数决定使用哪种方法
DescriptionOperatorAssociativity
unary postfix_expr_++    _expr_--    ()    []    ?[]    .    ?.    !None
unary prefix-_expr_    !_expr_    ~_expr_    ++_expr_    --_expr_      await&nbsp;_expr_None
multiplicative*    /    %  ~/Left
additive+    -Left
shift<<    >>    >>>Left
bitwise AND&Left
bitwise XOR^Left
bitwise OR|Left
relational and type test>=    >    <=    <    as    is    is!None
equality==    !=None
logical AND&&Left
logical OR||Left
if null??Left
conditional_expr1_&nbsp;?&nbsp;_expr2_&nbsp;:&nbsp;_expr3_Right
cascade..    ?..Left
assignment=    *=    /=   +=   -=   &=   ^=   etc.Right
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder
assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');
OperatorMeaning
++_var__var_&nbsp;=&nbsp;_var_&nbsp;+ 1 (expression value is _var_&nbsp;+ 1)
_var_++_var_&nbsp;=&nbsp;_var_&nbsp;+ 1 (expression value is _var_)
--_var__var_&nbsp;=&nbsp;_var_&nbsp;- 1 (expression value is _var_&nbsp;- 1)
_var_--_var_&nbsp;=&nbsp;_var_&nbsp;- 1 (expression value is _var_)

运算符 == 的工作原理如下:

  • 如果 xynull,两者都为 null 返回 true,否则返回 false
  • 返回以参数 yx 调用 == 方法的结果

类型测试运算符

OperatorMeaning
asTypecast (also used to specify library prefixes)
isTrue if the object has the specified type
is!True if the object doesn’t have the specified type
  • obj is T 为真当 obj 实现了 T 指定的接口
  • 仅当确定对象属于对应类型时才使用 as 运算符

赋值运算符

仅当左值为 null 时才进行赋值:

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

条件表达式

仅当左值为 null 时才对右值求值:

_expr1_ ?? _expr2_
//If _expr1_ is non-null, returns its value; otherwise, evaluates and returns the value of _expr2_.
String playerName(String? name) => name ?? 'Guest';

级联操作符

级联(..?..)允许你对同一个对象进行一系列操作

除了访问实例成员外,还可以在同一个对象上对用实例方法。这通常可以节省创建临时变量的步骤

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

上例中,构造函数返回一个 Paint 对象,级联操作符后面的代码对该对象进行操作,并忽略返回值。
等价于下面的代码

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

如果级联操作的对象可以为 null,那么应该在第一次操作时使用 null-shorting 级联(?..
?.. 开头确保不会对空对象尝试任何级联操作

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

等价于下面的代码

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

还可以进行嵌套级联

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = '[email protected]'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

在使用时要小心,例如下面的代码会失败,因为调用 sb.write() 返回 void,你不能在它之上构造级联

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // Error: method 'write' isn't defined for 'void'.
  • 严格来说,级联的两点符号不是运算符,它只是Dart语法的一部分

其他运算符

OperatorNameMeaning
()Function applicationRepresents a function call
[]Subscript accessRepresents a call to the overridable [] operator; example: fooList[1] passes the int 1 to fooList to access the element at index 1
?[]Conditional subscript accessLike [], but the leftmost operand can be null; example: fooList?[1] passes the int 1 to fooList to access the element at index 1 unless fooList is null (in which case the expression evaluates to null)
.Member accessRefers to a property of an expression; example: foo.bar selects property bar from expression foo
?.Conditional member accessLike ., but the leftmost operand can be null; example: foo?.bar selects property bar from expression foo unless foo is null (in which case the value of foo?.bar is null)
!Null assertion operatorCasts an expression to its underlying non-nullable type, throwing a runtime exception if the cast fails; example: foo!.bar asserts foo is non-null and selects the property bar, unless foo is null in which case a runtime exception is thrown
  • 条件下标访问 ?[]
    • 被访问的变量为 null 时,表达式的结果为 null
  • 条件成员访问 ?.
    • 访问实例对象成员,除非正在访问 null
  • 非空断言运算符 !
    • 将表达式转换为其基础的 Non-nullable 类型,转换失败时抛出运行时异常

元数据 (Metadata)

使用元数据提供有关代码的附加信息。元数据注解以 @ 开头,后跟对编译期常量或对常量构造函数的调用

所有的Dart代码都可以使用如下三个注解:

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

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

你可以定义自己的元数据注解,下面是定义带有两个参数的 @Todo 注解的示例

class Todo {
  final String who;
  final String what;

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

元数据可以出现在库、类、typedef、类型参数、构造器、工厂、函数、字段、参数、变量声明之前
或在 import、export 之前

你可以使用反射在运行时检索元数据

Libraries & imports

  • importlibrary 指令可以帮助构造模块化、可共享的代码库
  • 库不仅提供API,还是一个隐私单元;以下划线开头的标识符仅在库内部可见
  • 每个Dart应用都是一个库,即使它没有使用 library 指令

使用库

import 需要指定库的 URI,对于内置库,URI 使用特殊的 dart: ,对于其他库,可以使用文件系统路径或 package: ,它指定包管理器pub提供的库

import 'dart:html';

import 'package:test/test.dart';

指定库前缀

如果导入两个具有冲突标识符的库,你可以为其中一个或全部库指定一个前缀

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

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

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

导入库的一部分

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

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

延迟加载库

注意:Only dart compile js supports deferred loading. Flutter and the Dart VM don’t support deferred loading.
Flutter 和 Dart VM 不支持延迟加载库

这允许Web应用按需加载库,用于:

  • 减少Web App的初始启动时间
  • 执行A/B测试
  • 加载很少使用的功能,如可选屏幕和对话框
import 'package:greetings/hello.dart' deferred as hello;

当你需要库时,调用 loadLibrary()

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

可以在库上多次调用 loadLibrary(),库仅会被加载一次;使用延迟加载时需要注意:

  • 延迟库中的常量不是导入它的文件中的常量,这些常量在加载库前不存在
  • 不能使用延迟库中的类型,可以考虑将接口类移动到另一个被两个库直接导入的库
  • Dart将 loadLibrary() 隐式插入到使用了 deferred as&nbsp;_namespace_ 的命名空间中,这个函数返回 Future

library 指令

要指定库级别的文档注释或元数据注释,将它们附加到文件开头的 library 声明中

/// A really great test library.
@TestOn('browser')
library;

内置类型

一些类型在Dart中有特殊作用:

  • Object: The superclass of all Dart classes except Null. 所有类的超类,除了Null
  • Enum: The superclass of all enums. 所有枚举类的超类
  • Future and Stream: Used in asynchrony support. 用于异步支持
  • Iterable: Used in for-in loops and in synchronous generator functions. 用于for-in循环和同步生成器函数
  • Never: Indicates that an expression can never successfully finish evaluating. Most often used for functions that always throw an exception. 表示表达式永远无法成功完成求值,常用于总是抛出异常的函数
  • dynamic: Indicates that you want to disable static checking. Usually you should use Object or Object? instead. 表示你希望禁用静态类型检查,通常你应该使用 ObjectObject? 代替它
  • void: Indicates that a value is never used. Often used as a return type.

ObjectObject?NullNever 类在类层次结构中有特殊作用

数值类

int:64位整数值,在原生平台上取值范围从 $-2^{63}$ 到 $2^{63}-1$ ,在Web上表示JavaScript numbers(没有小数部分的64位浮点值,取值范围从 $-2^{53}$ 到 $2^{53}-1$ )

double:64位双精度浮点数,符合 IEEE 754 标准

  • intdouble 都是 num 的子类,num 类型包括基本运算符(加减乘除)以及 abs()ceil()floor() 等方法;按位运算符则在 int 类中定义;如果 num 或其子类中没有你想要的,dart:math 库中可能有
var x = 1;
var hex = 0xDEADBEEF;

var y = 1.1;
var exponents = 1.42e5;

你可以声明一个 num 类型的变量,这个变量可以同时具有整数值和双精度浮点值

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

必要时整数字面量会自动转换为浮点数

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

以下是一些在字符串和数字之间转换的方法

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

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

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

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

数字字面量是编译期常量,操作数是计算结果为数字的编译期常量的算数表达式也是

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

String

  • Dart中的字符串包含一系列 UTF-16 代码单元,可以使用单引号或双引号创建字符串;
  • 可以使用相邻的字符串字面量或 + 运算符连接字符串;
  • 可以使用 ${_expression_} 将表达式的值放入字符串中,如果表达式是标识符,可以省略 {}
var s = 'string interpolation';

assert('Dart has $s, which is very handy.' ==
    'Dart has string interpolation, '
        'which is very handy.');
assert('That deserves all caps. '
        '${s.toUpperCase()} is very handy!' ==
    'That deserves all caps. '
        'STRING INTERPOLATION is very handy!');

注意:运算符 == 测试两个对象是否等价,如果两个字符串包含相同的 UTF-16 代码序列,那么它们就是等价的

使用三引号创建多行字符串

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

var s2 = """This is also a
multi-line string.""";

使用 r 前缀创建原始(RAW)字符串

var s = r'In a raw string, not even \n gets special treatment.';

字符串字面量是编译期常量,只要其中所有插入的表达式都是计算为 null、数字、字符串、布尔值的编译期常量

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

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

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

布尔类型

Dart有一个名为 bool 的类型,只有两个对象属于 bool 类型,truefalse,它们都是编译期常量

Dart的类型安全意味着你不能在 ifassert 中使用非布尔类型的代码

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

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

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

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

字符

Dart字符串是 UTF-16 代码单元的序列,通常表示 Unicode 码点的方法是 \uXXXX ,要指定多于或少于四个十六进制数,请将值放在花括号中,例如 \u{1f606}

如果需要读取和写入单个 Unicode 字符,使用 characters 包在字符串 String 上定义的 characters getter,它返回一个 Characters 对象

import 'package:characters/characters.dart';

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

Symbol

Symbol 对象表示一个 Dart 程序中声明的运算符或标识符,你可能永远不需要用到它,但对于按名称引用标识符的 API 是有用的,要获取标识符的符号,使用符号字面量,一个 # 后跟标识符:

#radix
#bar

Record

Record 类是 Dart 3.0 新增的一种匿名、不可变的聚合类型

与其他集合类一样,它允许你将多个对象捆绑到一个对象中
与其他集合类不同,Record 是固定大小、可包含不同类型、类型化的

记录表达式是以逗号分隔的,固定位置字段或命名字段列表,括在括号中:

var record = ('first', a: 2, b: true, 'last');

Record 类型声明是用括号括起来的,以逗号分隔的类型列表,可以使用它定义返回类型和参数类型

(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

固定位置字段直接放在括号内:

// Record type annotation in a variable declaration:
(String, int) record;

// Initialize it with a record expression:
record = ('A string', 123);

命名字段位于大括号分隔部分内,位于所有固定位置字段之后

// Record type annotation in a variable declaration:
({int a, bool b}) record;

// Initialize it with a record expression:
record = (a: 123, b: true);

命名字段的名称是 Record 类型声明的一部分,具有不同名称命名字段的两个 Record 具有不同类型

({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);

// Compile error! These records don't have the same type.
// recordAB = recordXY;

固定位置字段也可以声明名称,但它们只用于文档,不会影响 Record 的类型

(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

recordAB = recordXY; // OK.

Record 字段

可以通过内置的 Getter 访问记录字段,记录是不可变的,因此没有 Setter

使用命名字段的名称访问对应字段,或使用 $<position> 访问固定位置字段,这将跳过命名字段

var record = ('first', a: 2, b: true, 'last');

print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'

独立的 Record 类型没有类型声明,记录根据其字段的类型进行结构类型化
记录的 shape (其字段集、字段类型、名称-如果有) 唯一确定记录的类型

(num, Object) pair = (42, 'a');

var first = pair.$1; // Static type `num`, runtime type `int`.
var second = pair.$2; // Static type `Object`, runtime type `String`.

如果两个不相关的库创建具有相同字段集的记录,类型系统理解这些记录是相同的类型,即使库没有相互耦合

Record 相等性

如果两条记录具有相同的 shape (字段集),且它们对应的字段具有相同的值,则它们是相等的

由于命名字段的顺序不是记录 shape 的一部分,因此命名字段的顺序不影响相等性

(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);

print(point == color); // Prints 'true'.
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);

print(point == color); // Prints 'false'. Lint: Equals on unrelated types.

记录根据其字段的结构自动定义 hashCode== 方法

模式匹配

使用模式匹配(pattern matching)将函数返回的 Record 解包为局部变量

// Returns multiple values in a record:
(String, int) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

final json = <String, dynamic>{
  'name': 'Dash',
  'age': 10,
  'color': 'blue',
};

// Destructures using a record pattern:
var (name, age) = userInfo(json);

/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

集合

List

在Dart中,数组是 List 对象,可以在集合最后一项之后添加一个逗号,不会有影响

var list = [1, 2, 3];

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

要创建一个编译期常量列表,在列表前添加 const

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

Set

Set 提供项唯一的无序集合

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

要创建空集,需要在 {} 前加上类型参数,或将 {} 分配给 Set 类型的变量

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

如果你忘记了类型参数,Dart会创建一个 Map<dynamic, dynamic>

在集合前添加 const 创建一个编译期常量集合

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

Map

构造方法如下

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

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};
var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

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

注意:在 Dart 中,new 关键字是可选的

如果访问一个不在 Map 中的 Key,你会得到 null

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

在 Map 前添加 const 创建一个编译期常量映射

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

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

运算符

扩展运算符 (Spread operators)

Dart 在 List、Map、Set 字面量中支持扩展运算符(...)和空感知扩展运算符(...?

它们提供一种将多个值插入集合的简洁方法,例如你可以将一个列表的所有值插入到另一个列表

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

如果扩展运算符右侧的表达式可能为 null,则可用空感知扩展运算符来避免异常

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

控制流运算符 (Control-flow operators)

Dart 在 List、Map、Set 字面量中支持 collection ifcollection for

下面是一个创建包含三个或四个项目的列表的示例

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

还支持在其中使用 if-case

var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];

下面是将列表项添加到另一个列表之前使用 for 操作列表项的示例

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

泛型

集合字面量参数化

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

在构造器上使用参数化类型

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

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

Dart 的泛型类型是具体化 (reified) 的,意味着它们在运行时携带类型信息

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

相比之下,Java 中的泛型使用擦除,这意味着在运行时删除泛型类型参数。 在 Java 中,你可以测试一个对象是否是一个 List,但是你不能测试它是否是一个 List<String>

限制参数化类型

常见的用例是限制类型为 Object 的子类(而不是默认的 Object?),以确保其不为 null

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

方法和函数也允许使用类型参数

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

类型别名 typedef

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

类型别名也可以有类型参数

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

大多数情况下,建议对函数使用内联函数类型(inline function types)而不是typedef,但它仍然有用

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

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

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

模式

模式(Patterns)是Dart中的一种语法,就像语句和表达式一样

模式的作用

一般来说,模式可以匹配(match)一个值、解构(destructure)一个值

模式匹配(Matching)

什么匹配取决于使用的是哪种模式,例如常量模式匹配检查值是否等于模式常量

switch (number) {
  // Constant pattern matches if 1 == number.
  case 1:
    print('one');
}

许多模式使用子模式,有时称为外模式(outer)和内模式(inner),模式在其子模式上递归匹配

例如,集合类型模式(collection-type pattern)中的字段可以是变量模式(variable patterns)或常量模式(constant patterns)

const a = 'a';
const b = 'b';
switch (obj) {
  // List pattern [a, b] matches obj first if obj is a list with two fields,
  // then if its fields match the constant subpatterns 'a' and 'b'.
  case [a, b]:
    print('$a, $b');
}

要忽略部分匹配值,可使用通配符模式(wildcard pattern)作为占位符,对于列表模式,可以使用rest element

模式解构(Destructuring)

当对象和模式匹配时,模式可以访问对象的数据并将其分段提取;换句话说,模式解构了对象

var numList = [1, 2, 3];
// List pattern [a, b, c] destructures the three elements from numList...
var [a, b, c] = numList;
// ...and assigns them to new variables.
print(a + b + c);

可以在解构模式中嵌套任何类型的模式

例如,下面的case模式匹配并解构第一个元素为a或b的双元素列表

switch (list) {
  case ['a' || 'b', var c]:
    print(c);
}

可用模式的位置

  • 局部变量声明、赋值
  • forfor-in 循环
  • if-caseswitch-case
  • 集合字面量中的[[0x05 集合#控制流运算符 (Control-flow operators)|控制流]]

声明变量

可以在Dart允许声明局部变量的任何地方使用模式变量声明(pattern variable declaration),该模式匹配声明右侧的值,一旦匹配,它就会解构值并将其绑定到新的局部变量

// Declares new variables a, b, and c.
var (a, [b, c]) = ('str', [1, 2]);

模式变量声明必须以 var 或 final 开头

变量赋值

变量赋值模式(variable assignment pattern) 位于赋值的左侧,它首先解构匹配的对象,然后将值分配给现有变量,而不是绑定新变量

可以使用变量赋值模式交换两个变量的值

var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "right left".

switch语句和表达式

每个case子句都包含一个模式,这适用于switch语句、switch表达式、if-case语句,可以在case中使用任何类型的模式

case模式(Case patterns)是可拒绝的(refutable),case模式解构的值将变为局部变量,作用范围仅限在该case中

switch (obj) {
  // Matches if 1 == obj.
  case 1:
    print('one');

  // Matches if the value of obj is between the constant values of 'first' and 'last'.
  case >= first && <= last:
    print('in range');

  // Matches if obj is a record with two fields, then assigns the fields to 'a' and 'b'.
  case (var a, var b):
    print('a = $a, b = $b');

  default:
}

逻辑或模式(Logical-or patterns)可以让多个case共享一个主体

var isPrimary = switch (color) {
  Color.red || Color.yellow || Color.blue => true,
  _ => false
};
switch (shape) {
  case Square(size: var s) || Circle(size: var s) when s > 0:
    print('Non-empty symmetric shape');
}

forfor-in 循环

可以在循环中使用模式来迭代、解构集合中的值

Map<String, int> hist = {
  'a': 23,
  'b': 100,
};

for (var MapEntry(key: key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

对象模式(object pattern)检查 hist.entries 是否为类型 MapEntry,然后递归到命名字段子模式(named field subpatterns),每次迭代都会调用 MapEntry 上的 key getter 和 value getter,并将结果绑定到局部变量 keycount

将 getter 的返回值绑定到同名变量上是一个常见用例,因此对象模式也可以从变量子模式(variable subpattern)中推断出 getter 的名称

for (var MapEntry(:key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

模式最佳实践

解构多返回值

记录允许函数聚合返回多个值,模式增加了将记录的字段直接解构为局部变量的能力,且与函数调用内联

var (name, age) = userInfo(json);

解构对象

对象模式(Object patterns)匹配对象类型,允许使用对象类公开的 getter 解构它的数据

final Foo myFoo = Foo(one: 'one', two: 2);
var Foo(:one, :two) = myFoo;
print('one $one, two $two');

Algebraic data types

当你有一系列相关类型,有一种操作需要每个类型的特定行为,且希望将行为集中在一个地方,而不是分散到各个类的定义中时,可以使用这个方法

与其将操作实现为每种类型的实例方法,不如将操作的变体保留在切换子类型的单个函数中

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
      Square(length: var l) => l * l,
      Circle(radius: var r) => math.pi * r * r
    };

验证传入的json

map和list模式适用于解构json数据中的键值对

var json = {
  'user': ['Lily', 13]
};
var {'user': [name, age]} = json;

如果你确定json具有你期望的结构,上面的示例就是可行的,但数据通常来源于外部,你需要先验证它的结构

没有模式,验证代码是冗长的:

if (json is Map<String, Object?> &&
    json.length == 1 &&
    json.containsKey('user')) {
  var user = json['user'];
  if (user is List<Object> &&
      user.length == 2 &&
      user[0] is String &&
      user[1] is int) {
    var name = user[0] as String;
    var age = user[1] as int;
    print('User $name is $age years old.');
  }
}

case模式(case pattern)可以实现相同的验证,它最适合用于 if-case 语句

if (json case {'user': [String name, int age]}) {
  print('User $name is $age years old.');
}

这个case模式同时验证:

  • json是一个map,因为它首先必须匹配外部的map模式(map pattern),同时也验证了json不为null
  • json包含 user key
  • user 的值是有两个元素的list
  • list元素的类型是String和int
  • 保存值的新局部变量是String和int

模式类型

逻辑或

subpattern1 || subpattern2

逻辑或模式通过 || 分隔子模式,从左到右求值,一旦一个分支匹配,其余的就不会被评估

逻辑或模式中的子模式可以绑定变量,但分支必须定义同一组变量,因为当模式匹配时只会评估一个分支

逻辑与

subpattern1 && subpattern2

如果左分支不匹配,则不评估右分支

逻辑与模式中的子模式可以绑定变量,但每个子模式中的变量不能重叠,因为如果模式匹配,它们都将被绑定

switch ((1, 2)) {
  // Error, both subpatterns attempt to bind 'b'.
  case (var a, var b) && (var b, var c): // ...
}

关系(Relational)

== expression
< expression

关系模式使用 ==!=<><=, 和 >= 将值与给定常量比较,对于匹配数字范围非常有用

String asciiCharType(int char) {
  const space = 32;
  const zero = 48;
  const nine = 57;

  return switch (char) {
    < space => 'control',
    == space => 'space',
    > space && < zero => 'punctuation',
    >= zero && <= nine => 'digit',
    _ => ''
  };
}

转换(Cast)

foo as String

转换模式允许你在将值传递给另一个子模式之前插入类型转换

(num, Object) record = (1, 's');
var (i as int, s as String) = record;

如果值不具有指定类型,转换模式会抛出异常,与空断言模式一样,这让你可以强制断言某些值的预期类型

空检查

subpattern?

要将空值视为匹配失败而不抛出异常,使用空检查模式;如果值不为空,空检查模式首先匹配,然后再匹配内模式,允许绑定一个变量,变量的类型是匹配的可空值的不可空基类型

String? maybeString = 'nullable with base type String';
switch (maybeString) {
  case var s?:
  // 's' has type non-nullable String here.
}

要在值为 null 时进行匹配,请使用常量模式 null

空断言

subpattern!

如果对象不为空,空断言模式首先匹配,然后匹配值;如果匹配的值为空则抛出异常

List<String?> row = ['user', null];
switch (row) {
  case ['user', var name!]: // ...
  // 'name' is a non-nullable string here.
}

要从变量声明模式中消除空值,请使用空断言模式

(int?, int?) position = (2, 3);

var (x!, y!) = position;

常量

123, null, 'string', math.pi, SomeClass.constant, const Thing(1, 2), const (1 + 2)

当值等于常量时常量模式匹配

switch (number) {
  // Matches if 1 == number.
  case 1: // ...
}

可以直接使用简单字面量和对命名常量的引用作为常量模式

  • 数值字面量
  • 布尔字面量
  • 字符串字面量
  • 命名常量 (someConstantmath.pidouble.infinity)
  • 常量构造函数 (const Point(0, 0))
  • 常量集合字面量 (const []const {1, 2})

更复杂的常量表达式必须加上括号和const前缀 (const (1 + 2))

// List or map pattern:
case [a, b]: // ...

// List or map literal:
case const [a, b]: // ...

变量

var bar, String str, final int _

变量模式将新变量绑定到已匹配或解构的值,通常作为解构模式的一部分出现,以捕获解构的值

switch ((1, 2)) {
  // 'var a' and 'var b' are variable patterns that bind to 1 and 2, respectively.
  case (var a, var b): // ...
  // 'a' and 'b' are in scope in the case body.
}

类型化变量模式(typed variable pattern)仅在匹配值具有声明类型时才匹配,否则失败

switch ((1, 2)) {
  // Does not match.
  case (int a, String b): // ...
}

可以使用通配符模式(wildcard pattern)作为变量模式

标识符

foo, _

标识符模式可能表现为常量模式或可变模式,具体取决于它们出现的上下文

  • 声明上下文:声明一个具有标识符名称的新变量 var (a, b) = (1, 2);
  • 赋值上下文:赋值给具有标识符名称的现有变量 (a, b) = (3, 4);
  • 匹配上下文:视为命名常量模式(除非它的名字是 _
const c = 1;
switch (2) {
  case c:
    print('match $c');
  default:
    print('no match'); // Prints "no match".
}
  • 任何上下文中的通配符标识符:匹配任何值并丢弃它 case [_, var y, _]: print('The middle element is $y');

括号

提高优先级

// `x`, `y`, and `z` are equal to `true`, `true`, and `false`
x || y && z => 'matches true',
(x || y) && z => 'matches false',
// ...

列表List

[subpattern1, subpattern2]

列表模式匹配实现了 List 的值,然后递归将其子模式与列表元素匹配以按位置解构它们

const a = 'a';
const b = 'b';
switch (obj) {
  // List pattern [a, b] matches obj first if obj is a list with two fields,
  // then if its fields match the constant subpatterns 'a' and 'b'.
  case [a, b]:
    print('$a, $b');
}

列表模式要求模式中的元素数量与整个列表匹配。但是,可以使用rest元素(rest element)作为占位符来说明列表中任意数量的元素

剩余元素(Rest element)

列表模式可以包含一个剩余元素(...),它允许匹配任意长度的列表

var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 6 7".
print('$a $b $c $d');

rest元素也可以有一个子模式,它将与列表中其他子模式不匹配的元素收集到一个新列表中:

var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 [3, 4, 5] 6 7".
print('$a $b $rest $c $d');

Map

{"key": subpattern1, someConst: subpattern2}

Map模式匹配实现了Map的值,然后递归地将其子模式与映射的键匹配以解构它们

Map模式不需要模式匹配整个Map,Map模式会忽略映射中包含的与模式不匹配的任何键

记录Record

(subpattern1, subpattern2)

(x: subpattern1, y: subpattern2)

记录模式匹配记录对象并解构其字段;如果值不是与模式具有相同shape的记录,则匹配失败;否则,字段子模式与记录中的相应字段匹配

记录模式要求模式匹配整个记录;要使用模式解构具有命名字段的记录,请在模式中包含字段名称:

var (myString: foo, myNumber: bar) = (myString: 'string', myNumber: 1);

getter 名称可以省略并从字段子模式中的变量模式或标识符模式推断,这些模式对都是等价的:

// Record pattern with variable subpatterns:
var (untyped: untyped, typed: int typed) = record;
var (:untyped, :int typed) = record;

switch (record) {
  case (untyped: var untyped, typed: int typed): // ...
  case (:var untyped, :int typed): // ...
}

// Record pattern wih null-check and null-assert subpatterns:
switch (record) {
  case (checked: var checked?, asserted: var asserted!): // ...
  case (:var checked?, :var asserted!): // ...
}

// Record pattern wih cast subpattern:
var (untyped: untyped as int, typed: typed as String) = record;
var (:untyped as int, :typed as String) = record;

对象

SomeClass(x: subpattern1, y: subpattern2)

对象模式根据给定的类型检查匹配值,使用对象属性上的 getter 来解构数据。如果值不具有相同的类型,它们将被拒绝(refuted)

switch (shape) {
  // Matches if shape is of type Rect, and then against the properties of Rect.
  case Rect(width: var w, height: var h): // ...
}

getter 名称可以省略,从字段子模式中的变量模式或标识符模式推断

// Binds new variables x and y to the values of Point's x and y properties.
var Point(:x, :y) = Point(1, 2);

对象模式不需要模式匹配整个对象;如果一个对象有模式没有解构的额外字段,它仍然可以匹配

通配符(Wildcard)

_

名为 _ 的模式是一个通配符,可以是变量模式或标识符模式,它不会绑定或分配给任何变量

它在需要子模式的地方用作占位符很有用,以便解构后续位置值:

var list = [1, 2, 3];
var [_, two, _] = list;

想要测试值的类型但不将该值绑定到名称时,带有类型注释的通配符名称很有用:

switch (record) {
  case (int _, String _):
    print('First field is int and second is String.');
}

函数

Dart 是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型 Function

这意味着函数可以分配给变量或作为参数传递给其他函数

还可以像调用函数一样调用 Dart 类的实例,参阅可调用对象(Callable objects)

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

尽管建议为公共 API 使用类型注释,但如果省略类型,该函数仍然有效:

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

对于只包含一个表达式的函数,可以使用简写语法:

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

=> _expr_ 语法是 { return _expr_; } 的缩写。=> 有时称为箭头语法(arrow syntax)

注意:箭头 (=>) 和分号 (;) 之间只能出现表达式,不能是语句。例如,不能在此处放置 if 语句,但可以使用条件表达式。

参数

一个函数可以有任意数量的必需位置参数(required positional parameters);这些参数后面可以跟命名参数(named parameters)或可选位置参数(optional positional parameters)(但不能同时跟两个)

注意:某些 API(尤其是 Flutter 小部件构造函数)仅使用命名参数,即使是强制参数也是如此

将参数传递给函数或定义函数参数时,可以使用尾随逗号(trailing commas)

命名参数 Named parameters

命名参数是可选的,除非它们明确标记为 required

定义函数时,使用 {_param1_, _param2_, …} 指定命名参数。 如果不提供默认值或将其标记为 required,则它们的类型必须是可空的,因为它们的默认值将为 null

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

调用函数时,可以使用 paramName: value 指定命名参数

enableFlags(bold: true, hidden: false);

使用 = 为命名参数指定默认值,指定的值必须是编译期常量

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

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

如果希望命名参数是强制性的,要求调用者为参数提供值,请使用 required 注释它们

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

标记为 required 的参数仍然可以为空 const Scrollbar({super.key, required Widget? child});

Dart 允许将命名参数放在参数列表中的任何位置

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

可选位置参数 Optional positional parameters

[] 中包装一组函数参数将它们标记为可选的位置参数。如果不提供默认值,则它们的类型必须是可空的,因为它们的默认值为 null

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

同样使用 = 指定默认值,默认值必须是编译期常量

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

main() 函数

每个应用程序都必须有一个顶级 main() 函数,作为应用程序的入口点

main() 函数返回 void 并有一个可选的 List<String> 参数

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

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

可以使用 args 库(args library)来定义和解析命令行参数

函数作为对象

可以将一个函数作为参数传递给另一个函数

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

var list = [1, 2, 3];

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

还可以将函数分配给变量

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

匿名函数

匿名函数(anonymous function) 有时也称为 lambda 或 闭包(closure)

可以将匿名函数分配给变量,或将它添加到集合中

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

如果函数只包含一个表达式或 return 语句,可以使用箭头语法将其缩短

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

词法作用域

Dart 是一种词法范围语言,这意味着变量的范围是静态确定的,只需查看代码的布局即可

bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

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

词法闭包

闭包是一个函数对象,它可以访问其词法范围内的变量,即使该函数在其原始范围之外使用

函数可以捕获其所在作用域中定义的变量。下面的示例中,makeAdder() 捕获变量 addBy;无论返回的函数走到哪里,它都会记住 addBy

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

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

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

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

测试函数是否相等

下面是测试顶级函数、静态方法和实例方法是否相等的示例

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

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

void main() {
  Function x;

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

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

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

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

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

返回值

所有函数都返回一个值,如果没有指定返回值,则 return null; 隐式附加到函数体

要在函数中返回多个值,请将这些值聚合到一条记录中

(String, int) foo() {
  return ('something', 42);
}

生成器

当您需要延迟生成一系列值时,请考虑使用生成器函数(generator function)

Dart 内置支持两种生成器函数

  • 同步 Synchronous 生成器:返回一个 Iterable 对象
  • 异步 Asynchronous 生成器:返回一个 Stream 对象

要实现同步生成器函数,将函数体标记为 sync*,使用 yield 语句传递值

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

要实现异步生成器函数,将函数体标记为 async*,并使用 yield 语句传递值

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

如果你的生成器是递归的,你可以通过使用 yield* 来提高它的性能

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

控制流

循环 Loops

  • for
  • whiledo while
  • breakcontinue

for 循环

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

Dart 的 for 循环中的闭包捕获索引的值;这避免了 JavaScript 中常见的陷阱

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

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

正如预期的那样,输出先是 0,然后是 1;相反,该示例将在 JavaScript 中打印 2 和 2

在遍历Iterable类型(如 List 或 Set)时,你可能不需要知道当前的迭代计数器 #for-in

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

要处理从 iterable 获得的值,还可以在 for-in 循环中使用模式

for (final Candidate(:name, :yearsExperience) in candidates) {
  print('$name has $yearsExperience of experience.');
}

Iterable 类也有一个 forEach() 方法作为另一个选择

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

whiledo-while

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

breakcontinue

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

如果使用Iterable,可以如下编写前面的示例

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

分支 Branches

  • if
  • if-case
  • switch

if

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

if-case

Dart if 语句支持后跟模式的 case 子句

if (pair case [int x, int y]) return Point(x, y);

如果模式与值匹配,则分支执行模式在范围内定义的任何变量

在前面的示例中,列表模式 [int x, int y] 匹配值,因此分支返回 Point(x, y) 使用模式定义的变量 x 和 y

if (pair case [int x, int y]) {
  print('Was coordinate array $x,$y');
} else {
  throw FormatException('Invalid coordinates.');
}

if-case 语句提供了一种针对单个模式进行匹配和解构的方法;要针对多个模式测试一个值,请使用 switch

switch 语句

可以为 case 使用任何类型的模式

非空 case 子句完成后跳转到 switch 的末尾,不需要 break 语句;结束非空 case 子句的其他有效方式是 continue、throw 或 return 语句

当没有 case 子句匹配时,使用 default 或通配符 _ 子句执行代码

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

空 case 会落入下一个 case,如果不希望空 case 滑落,请使用 break

对于非顺序滑落,可以使用 continue 语句和标签

switch (command) {
  case 'OPEN':
    executeOpen();
    continue newCase; // Continues executing at the newCase label.

  case 'DENIED': // Empty case falls through.
  case 'CLOSED':
    executeClosed(); // Runs for both DENIED and CLOSED,

  newCase:
  case 'PENDING':
    executeNowClosed(); // Runs for both OPEN and PENDING.
}

还可以使用 [[0x08 Patterns#逻辑或|逻辑或模式]] 来允许 case 共享 body

switch 表达式

switch 表达式根据 case 匹配的表达式主体生成一个值

你可以在 Dart 允许表达式的任何地方使用 switch 表达式,除了在表达式语句的开头,如果要在表达式语句的开头使用 switch,请使用 switch 语句

var x = switch (y) { ... };

print(switch (x) { ... });

return switch (x) { ... };

switch 表达式允许你像这样重写 switch 语句:

// Where slash, star, comma, semicolon, etc., are constant variables...
switch (charCode) {
  case slash || star || plus || minus: // Logical-or pattern
    token = operator(charCode);
  case comma || semicolon: // Logical-or pattern
    token = punctuation(charCode);
  case >= digit0 && <= digit9: // Relational and logical-and patterns
    token = number();
  default:
    throw FormatException('Invalid');
}

变成一个表达式,像这样:

token = switch (charCode) {
  slash || star || plus || minus => operator(charCode),
  comma || semicolon => punctuation(charCode),
  >= digit0 && <= digit9 => number(),
  _ => throw FormatException('Invalid')
};

switch 表达式的语法不同于 switch 语句语法:

  • case 不以 case 关键字开头
  • case 主体是单个表达式而不是一系列语句
  • 每个 case 都必须有一个主体,空 case 没有隐式的滑落
  • 使用 => 而不是 : 将 case 模式与其主体分开
  • case 由逗号分隔,且允许尾随逗号
  • 默认 case 只能用通配符 _ 表示

Exhaustiveness 检查

详尽检查是一个 feature,如果值进入 switch 可能不匹配任何 case,则会报告编译期错误

// Non-exhaustive switch on bool?, missing case to match null possibility:
switch (nullableBool) {
  case true:
    print('yes');
  case false:
    print('no');
}

枚举(enums)和密封类型(sealed types)对于 switch 特别有用,因为即使没有默认情况,它们的可能值也是已知的并且完全可枚举

在类上使用 sealed 修饰符 以在该类的子类型上启用详尽(exhaustiveness)检查:

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
      Square(length: var l) => l * l,
      Circle(radius: var r) => math.pi * r * r
    };

如果有人要添加一个新的 Shape 子类,这个 switch 表达式将是不完整的;详尽检查会通知您缺少的子类型;这允许您以某种functional algebraic datatype style样式使用 Dart

when 子句(Guard clause)

使用关键字 when 在 case 子句之后设置可选的保护子句

保护子句可以跟在 if case 之后,也可以跟在 switch 语句和表达式之后

switch (pair) {
  case (int a, int b) when a > b:
    print('First element greater');
  case (int a, int b):
    print('First element not greater');
}

Guards 在匹配后评估任意布尔表达式;这允许你对 case 主体是否应执行添加进一步的约束

当保护子句的计算结果为 false 时,会继续匹配下一个 case,而不是退出整个 switch

异常处理

Exceptions

与 Java 不同,Dart 的所有异常都是非检查型异常。方法不声明它们可能抛出哪些异常,并且你不需要捕获任何异常

Dart 提供了 ExceptionError 类型,以及许多预定义的子类型。当然,你可以定义自己的异常。不过,Dart 可以抛出任何非空对象——而不仅仅是 Exception 和 Error 对象——作为异常

Throw

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

还可以抛出任意对象

throw 'Out of llamas!';

因为抛出异常是一个表达式,所以你可以在 => 语句中抛出异常,也可以在任何其他允许表达式的地方抛出异常

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

Catch

捕获异常会阻止异常传播(除非重新抛出异常)捕获异常让你有机会处理它

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

要处理可能抛出不止一种异常的代码,可以指定多个 catch 子句。第一个匹配抛出对象类型的 catch 子句处理异常。如果 catch 子句未指定类型,则该子句可以处理任何类型的抛出对象:

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

如前面的代码所示,可以使用 oncatch 或两者

  • 当需要指定异常类型时使用 on
  • 当异常处理程序需要异常对象时使用 catch

可以为 catch() 指定一个或两个参数

  • 第一个是抛出的异常
  • 第二个是堆栈跟踪(StackTrace 对象)
try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

要部分处理异常,同时允许它传播,请使用 rethrow 关键字

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

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

Finally

要确保无论是否抛出异常,某些代码都会运行,请使用 finally 子句

finally 子句在任何匹配的 catch 子句之后运行,如果没有 catch 子句与异常匹配,则在 finally 子句运行后传播异常

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}
try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

断言 Assert

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

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

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

要将消息附加到断言,请添加一个字符串作为断言的第二个参数(可选使用尾随逗号)

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

assert 的第一个参数可以是任何解析为布尔值的表达式;如果表达式的值为真,则断言成功并继续执行;如果为假,则断言失败并抛出异常(AssertionError

断言到底什么时候起作用?这取决于您使用的工具和框架:

  • Flutter 在调试模式下启用断言
  • 仅用于开发的工具(例如 webdev serve)通常默认启用断言
  • 一些工具,例如 dart run 和 dart compile js 通过命令行标志支持断言:--enable-asserts

在生产代码中,断言被忽略,并且不评估断言的参数

Dart 是一种面向对象的语言,具有类和基于 Mixin 的继承 (Mixin-based inheritance),每个对象都是一个类的实例,除 Null 之外的所有类都派生自 Object

基于 Mixin 的继承意味着虽然每个类(除了顶级类,Object?)都只有一个超类,但是一个类体可以在多个类层次结构中重复使用

扩展方法(Extension methods)是一种在不更改类或创建子类的情况下向类添加功能的方法,类修饰符(Class modifiers)允许您控制库如何对类进行子类型化

使用类成员

var p = Point(2, 2);

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

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

使用 ?. 代替 . 可以在最左边的操作数为空时避免异常:

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

使用构造器

构造器的名称可以是 _ClassName_ 或 _ClassName_._identifier_

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

以下代码具有相同的效果,但在构造器名称之前使用了可选的 new 关键字:

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

一些类提供常量构造器;要使用常量构造器创建编译时常量,请将 const 关键字放在构造函数名称之前:

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

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

在常量上下文中,可以省略构造函数或文字前的常量;例如,这段代码创建了一个 const 映射:

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

您可以省略除第一次使用 const 关键字以外的所有内容:

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

如果常量构造器在常量上下文之外并且在没有 const 的情况下被调用,它会创建一个非常量对象:

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

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

获取对象的类型

要在运行时获取对象的类型,可以使用 Object 属性 runtimeType,它返回一个 Type 对象

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

请使用类型测试运算符而不是 runtimeType 来测试对象的类型;在生产环境中,object is Typeobject.runtimeType == Type 更稳定

实例变量

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

所有未初始化的实例变量的值都为 null,所有实例变量都会生成一个隐式的 getter 方法

final 实例变量和没有初始值的 late final 实例变量也会生成隐式 setter 方法

如果在声明时初始化非 late 实例变量,则该值会在实例被创建时设置,也就是在构造器和初始化列表执行之前,所以,非 late 实例变量初始化时不能访问 this

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

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

实例变量可以是 final 的,在这种情况下,它们只能被赋值一次

使用构造器参数或构造器初始化列表initializer list,在声明时初始化 final 的非 late 实例变量

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

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

如果需要在构造函数体启动后为最终实例变量赋值,可以使用以下方法之一:

  • 使用工厂构造器factory constructor
  • 使用 late final,但要小心:没有初始化器的 late final 会向 API 添加一个 setter

隐式接口

每个类都隐式定义了一个接口,其中包含该类的所有实例成员以及它实现的任何接口;如果你想创建一个支持类 B 的 API 而不继承 B 的实现的类 A,类 A 应该实现 B 的接口

一个类通过在 implements 子句中声明它们然后提供接口所需的 API 来实现一个或多个接口

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

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

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

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

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

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

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}
class Point implements Comparable, Location {...}

类变量和方法

使用 static 关键字来实现类范围的变量和方法

静态变量

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

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

静态变量在使用之前不会被初始化

静态方法

import 'dart:math';

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

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

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

注意:对于常见或广泛使用的实用程序和功能,请考虑使用顶级函数而不是静态方法。

可以使用静态方法作为编译时常量;例如,可以将静态方法作为参数传递给常量构造函数

构造器

通过创建与其类同名的函数来声明构造函数(加上可选的附加标识符);最常见的构造函数形式,即生成构造函数,创建一个类的新实例:

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

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

初始化形式参数 (Initializing formal parameters)

将构造函数参数分配给实例变量的模式非常普遍,Dart 具有初始化形式参数以使其变得容易

初始化参数也可用于初始化不可为 null 或 final 实例变量,它们都必须被初始化或提供默认值

class Point {
  final double x;
  final double y;

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

初始化形式引入的变量是隐式 final 的,并且作用域仅在初始化列表的范围内

默认构造器

如果不声明构造函数,则提供默认构造函数;默认构造函数没有参数并调用超类中的无参数构造函数

构造函数不被继承

子类不从它们的超类继承构造函数,没有声明构造函数的子类只有默认(无参数,无名称)构造函数

命名构造器

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

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

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

请记住,构造函数不是继承的,这意味着超类的命名构造函数不会被子类继承;如果要使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数

调用非默认超类构造器

默认情况下,子类中的构造函数调用超类的未命名、无参数构造函数;超类的构造函数在构造函数主体的开头被调用

如果还使用了初始化列表,则它会在调用超类之前执行;综上,执行顺序如下:

  1. initializer list
  2. superclass’s no-arg constructor
  3. main class’s no-arg constructor

如果超类没有未命名、无参数的构造函数,则您必须手动调用超类中的构造函数之一。在冒号 (:) 之后、构造函数主体(如果有)之前指定超类构造函数:

class Person {
  String? firstName;

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

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

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

因为超类构造函数的参数是在调用构造函数之前计算的,所以参数可以是表达式,例如函数调用:

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

警告:超类构造函数的参数无权访问 this

超类初始化参数

为避免手动将每个参数传递给构造函数的 super 调用,可以使用 super-initializer parameters 将参数转发给指定的或默认的超类构造函数

此功能不能与重定向构造函数一起使用;Super-initializer parameters 与初始化形式参数具有相似的语法和语义:

class Vector2d {
  final double x;
  final double y;

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

class Vector3d extends Vector2d {
  final double z;

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

Super-initializer parameters cannot be positional if the super-constructor invocation already has positional arguments, but they can always be named:

class Vector2d {
  // ...

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

class Vector3d extends Vector2d {
  // ...

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

初始化列表 (Initializer list)

除了调用超类构造函数之外,还可以在构造函数主体运行之前初始化实例变量。用逗号分隔初始值设定项。

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

在开发期间,可以通过在初始化列表中使用断言来验证输入。

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

重定向构造器

重定向构造器主体为空,冒号后调用另一个构造器

class Point {
  double x, y;

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

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

常量构造器

如果你的类产生永不改变的对象,你可以使这些对象成为编译期常量

定义一个 const 构造函数并确保所有实例变量都是 final 的

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

  final double x, y;

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

Constant constructors don’t always create constants. For details, see the section on using constructors. [[0x12 类与对象#使用构造器]]

工厂构造器

使用 factory 关键字实现不总是创建其类新实例的构造器;例如,工厂构造函数可能会从缓存中返回一个实例,或者它可能会返回一个子类型的实例

工厂构造函数的另一个用例是使用无法在初始化列表中处理的逻辑来初始化final变量

处理 final 变量的延迟初始化的另一种方法是使用 late final (carefully!)

在以下示例中,Logger 工厂构造函数从缓存返回对象,而 Logger.fromJson 工厂构造函数从 JSON 对象初始化最终变量

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

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

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

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

  Logger._internal(this.name);

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

工厂构造函数无法访问 this

像调用任何其他构造函数一样调用工厂构造函数:

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

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

这篇文章有用吗?

点击星号为它评分!

平均评分 0 / 5. 投票数: 0

到目前为止还没有投票!成为第一位评论此文章。

很抱歉,这篇文章对您没有用!

让我们改善这篇文章!

告诉我们我们如何改善这篇文章?

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注