07.Dart运算符、流程控制语句、异常

185 阅读8分钟

运算符

Dart 支持下表的操作符。你可以将这些运算符实现为 一个类的成员

描述运算符
一元后缀表达式++ 表达式-- () [] . ?.
一元前缀-表达式 !表达式 ~表达式 ++表达式 --表达式
乘除法* / % ~/
加减法+ -
位运算<< >> >>>
二进制与&
二进制异或^
二进制或``
关系和类型测试>= > <= < as is is!
相等判断== !=
逻辑与&&
逻辑或``
空判断??
条件表达式表达式 1 ? 表达式 2 : 表达式 3
级联..    ?..``.. ?..
赋值= *= /= += -= &= ^= 等等……

上述运算符优先级是对 Dart 解析器行为的效仿。更准确的描述,请参阅 Dart 语言规范 中的语法。

一旦你使用了运算符,就创建了表达式。下面是一些运算符表达式的示例:

a++
a + b
a = b
a == b
c ? a : b
a is T

在 运算符表 中,运算符的优先级按先后排列,即第一行优先级最高,最后一行优先级最低,而同一行中,最左边的优先级最高,最右边的优先级最低。例如:% 运算符优先级高于 == ,而 == 高于 &&。根据优先级规则,那么意味着以下两行代码执行的效果相同:

// 括号改善可读性
if ((n % i == 0) && (d % i == 0)) ...

// 同上
if (n % i == 0 && d % i == 0) ..

算术运算符

Dart 支持常用的算术运算符:

运算符描述
+
-表达式一元负, 也可以作为反转(反转表达式的符号)
*
/
~/除并取整
%取模

示例:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
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');

Dart 还支持自增自减操作。

Operator++varvar = var + 1 (表达式的值为 var + 1)
var++var = var + 1 (表达式的值为 var)
--varvar = var – 1 (表达式的值为 var – 1)
var--var = var – 1 (表达式的值为 var)

示例:

int a;
int b;

a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1

a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0

a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1

a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b); // -1 != 0

关系运算符

下表列出了关系运算符及含义:

Operator==相等
!=不等
>大于
<小于
>=大于等于
<=小于等于

要判断两个对象 x 和 y 是否表示相同的事物使用 == 即可。(在极少数情况下,可能需要使用 identical() 函数来确定两个对象是否完全相同。)。下面是 == 运算符的一些规则:

  1. 假设有变量 x 和 y,且 x 和 y 至少有一个为 null,则当且仅当 x 和 y 均为 null 时 x == y 才会返回 true,否则只有一个为 null 则返回 false。
  2. x.==(y) 将会返回值,这里不管有没有 y,即 y 是可选的。也就是说 == 其实是 x 中的一个方法,并且可以被重写。详情请查阅重写运算符

下面的代码给出了每一种关系运算符的示例:

assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);

重写运算符示例:

class Person {
  final String firstName, lastName;

  Person(this.firstName, this.lastName);

  // 重写 hashCode,实现策略源于  Effective Java,
  // 第11章。
  @override
  int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
  }

  // 如果重写了 hashCode,通常应该从新实现 == 操作符。
  @override
  bool operator ==(dynamic other) {
    if (other is! Person) return false;
    Person person = other;
    return (person.firstName == firstName &&
        person.lastName == lastName);
  }
}

void main() {
  var p1 = Person('Bob', 'Smith');
  var p2 = Person('Bob', 'Smith');
  var p3 = 'not a person';
  assert(p1.hashCode == p2.hashCode);
  assert(p1 == p2);
  assert(p1 != p3);
}

类型判断运算符

asisis! 运算符是在运行时判断对象类型的运算符。

OperatorMeaning
as类型转换(也用作指定 类前缀))
is如果对象是指定类型则返回 true
is!如果对象是指定类型则返回 false

当且仅当 obj 实现了 T 的接口,obj is T 才是 true。例如 obj is Object 总为 true,因为所有类都是 Object 的子类。

仅当你确定这个对象是该类型的时候,你才可以使用 as 操作符可以把对象转换为特定的类型。例如:

(employee as Person).firstName = 'Bob';

如果你不确定这个对象类型是不是 T,请在转型前使用 is T 检查类型。

if (employee is Person) {
  // Type check
  employee.firstName = 'Bob';
}

上述两种方式是有区别的:如果 employee 为 null 或者不为 Person 类型,则第一种方式将会抛出异常,而第二种不会。

赋值运算符

可以使用 = 来赋值,同时也可以使用 ??= 来为值为 null 的变量赋值。

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

像 += 这样的赋值运算符将算数运算符和赋值运算符组合在了一起。

=–=/=%=>>=^=
+=*=~/=<<=&=`=`

下表解释了符合运算符的原理:

场景复合运算等效表达式
假设有运算符 opa op= ba = a op b
示例:a += ba = a + b

下面的例子展示了如何使用赋值以及复合赋值运算符:

var a = 2; // Assign using =
a *= 3; // Assign and multiply: a = a * 3
assert(a == 6);

逻辑运算符

使用逻辑运算符你可以反转或组合布尔表达式。

运算符描述
!表达式对表达式结果取反(即将 true 变为 false,false 变为 true)
``逻辑或
&&逻辑与

下面是使用逻辑表达式的示例:

if (!done && (col == 0 || col == 3)) {
  // ...Do something...
}

按位和移位运算符

在 Dart 中,二进制位运算符可以操作二进制的某一位,但仅适用于整数。

运算符描述
&按位与
``按位或
^按位异或
~表达式按位取反(即将 “0” 变为 “1”,“1” 变为 “0”)
<<位左移
>>位右移

下面是使用按位和移位运算符的示例:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right

条件表达式

Dart 有两个特殊的运算符可以用来替代 if-else 语句:

条件 ? 表达式 1 : 表达式 2
如果条件为 true,执行表达式 1并返回执行结果,否则执行表达式 2 并返回执行结果。

表达式 1 ?? 表达式 2
如果表达式 1 为非 null 则返回其值,否则执行表达式 2 并返回其值。

根据布尔表达式确定赋值时,请考虑使用 ? 和 :

var visibility = isPublic ? 'public' : 'private';

如果赋值是根据判定是否为 null 则考虑使用 ??

String playerName(String? name) => name ?? 'Guest';

上述示例还可以写成至少下面两种不同的形式,只是不够简洁:

// Slightly longer version uses ?: operator.
String playerName(String? name) => name != null ? name : 'Guest';

// Very long version uses if-else statement.
String playerName(String? name) {
  if (name != null) {
    return name;
  } else {
    return 'Guest';
  }
}

级联运算符

级联运算符 (..?..) 可以让你在同一个对象上连续调用多个对象的变量或方法。 除了调用函数, 还可以访问同一对象上的字段属性。 这通常可以节省创建临时变量的步骤, 同时编写出更流畅的代码。 如果级联操作的对象可以为空,则对第一个操作使用?..

querySelector('#confirm') // 获取对象。
  ?..text = 'Confirm' // 调用成员变量。
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

第一句调用函数 querySelector() , 返回获取到的对象。 获取的对象依次执行级联运算符后面的代码, 代码执行后的返回值会被忽略。

上面的代码等价于:

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

级联运算符可以嵌套,例如:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

在返回对象的函数中谨慎使用级联操作符。 例如,下面的代码是错误的:

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // Error: 'void' 没有定义 'write' 函数。

sb.write() 函数调用返回 void, 不能在 void 对象上创建级联操作。

?.. 运行在 2.12 和以上的 版本 中可用。
严格来说 .. 级联操作并非一个运算符而是 Dart 的特殊语法。

其他运算符

大多数其它的运算符,已经在其它的示例中使用过:

运算符名字描述
()使用方法代表调用一个方法
[]访问 List访问 List 中特定位置的元素
.访问成员成员访问符
?.条件访问成员与上述成员访问符类似,但是左边的操作对象不能为 null,例如 foo?.bar,如果 foo 为 null 则返回 null ,否则返回 bar

更多关于 .?. 和 .. 运算符介绍,请参考.

流程控制语句

你可以使用下面的语句来控制 Dart 代码的执行流程:

  • if 和 else
  • for 循环
  • while 和 do-while 循环
  • break 和 continue
  • switch 和 case
  • assert

使用 try-catch 和 throw 也能影响控制流,详情参考异常部分。

If 和 Else

Dart 支持 if - else 语句,其中 else 是可选的,比如下面的例子。你也可以参考条件表达式

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

不同于 JavaScript,Dart 的 if 语句中的条件必须是布尔值而不能为其它类型。详情请查阅布尔值

For 循环

你可以使用标准的 for 循环进行迭代。例如:

///StringBuffer的优势:
///String每更新一次就会new一个新的对象出来,更新次数上去之后,内存开销太大。
///而StringBuffer类型在更新(.append等操作)的过程中始终只有一个对象,开销大大减小。
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));
}
callbacks.forEach((c) => c());

上述代码执行后会输出 0 和 1,但是如果在 JavaScript 中执行同样的代码则会输出两个 2

如果要遍历的对象是一个可迭代对象(例如 List 或 Set),并且你不需要知道当前的遍历索引,则可以使用 for-in 方法进行 遍历

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

若想练习使用 for-in,请参考 遍历集合 codelab

可迭代对象同时可以使用 forEach() 方法作为另一种选择:

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

While 和 Do-While

while 循环会在执行循环体前先判断条件:

while (!isDone()) {
  doSomething();
}

do-while 循环则会 先执行一遍循环体 再判断条件:

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

Break 和 Continue

使用 break 可以中断循环:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

使用 continue 可以跳过本次循环直接进入下一次循环:

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

如果你正在使用诸如 List 或 Set 之类的 Iterable 对象,你可以用以下方式重写上述例子:

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

Switch 和 Case

Switch 语句在 Dart 中使用 == 来比较整数、字符串或编译时常量,比较的两个对象必须是同一个类型且不能是子类并且没有重写 == 操作符。 枚举类型非常适合在 Switch 语句中使用。
Dart 中的 Switch 语句仅适用于有限的情况,比如使用解释器和扫描器的场景。 每一个非空的 case 子句都必须有一个 break 语句,也可以通过 continuethrow 或者 return 来结束非空 case 语句。

不匹配任何 case 语句的情况下,会执行 default 子句中的代码:

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

下面的例子忽略了 case 子句的 break 语句,因此会产生错误:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // ERROR: Missing break

  case 'CLOSED':
    executeClosed();
    break;
}

但是,Dart 支持空的 case 语句,允许其以 fall-through 的形式执行。

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // Empty case falls through.
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

在非空 case 语句中想要实现 fall-through 的形式,可以使用 continue 语句配合 label 的方式实现:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // Continues executing at the nowClosed label.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

每个 case 子句都可以有局部变量且仅在该 case 语句内可见。

断言

在开发过程中,可以在条件表达式为 false 时使用 — assert(条件, 可选信息); — 语句来打断代码的执行。你可以在本文中找到大量使用 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 的第二个参数可以为其添加一个字符串消息。

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

assert 的第一个参数可以是值为布尔值的任何表达式。如果表达式的值为 true,则断言成功,继续执行。如果表达式的值为 false,则断言失败,抛出一个 AssertionError 异常。

如何判断 assert 是否生效?assert 是否生效依赖开发工具和使用的框架:

  • Flutter 在调试模式时生效。
  • 一些开发工具比如 dartdevc 通常情况下是默认生效的。
  • 其他一些工具,比如 dart run以及 dart2js 通过在运行 Dart 程序时添加命令行参数 --enable-asserts 使 assert 生效。

在生产环境代码中,断言会被忽略,与此同时传入 assert 的参数不被判断。

异常

Dart 代码可以抛出和捕获异常。异常表示一些未知的错误情况,如果异常没有捕获则会被抛出从而导致抛出异常的代码终止执行。

与 Java 不同的是,Dart 的所有异常都是非必检异常,方法不必声明会抛出哪些异常,并且你也不必捕获任何异常。

Dart 提供了 Exception 和 Error 两种类型的异常以及它们一系列的子类,你也可以定义自己的异常类型。但是在 Dart 中可以将任何非 null 对象作为异常抛出而不局限于 Exception 或 Error 类型。

抛出异常

下面是关于抛出或者 引发 异常的示例:

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

你也可以抛出任意的对象:

throw 'Out of llamas!';

优秀的代码通常会抛出 Error 或 Exception 类型的异常。 因为抛出异常是一个表达式,所以可以在 => 语句中使用,也可以在其他使用表达式的地方抛出异常:

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

捕获异常

捕获异常可以避免异常继续传递(重新抛出异常除外)。捕获一个异常可以给你处理它的机会:

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

对于可以抛出多种异常类型的代码,也可以指定多个 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');
}

如上述代码所示可以使用 on 或 catch 来捕获异常,使用 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 语句始终执行,如果没有指定 catch 语句来捕获异常,则异常会在执行完 finally 语句后抛出:

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

finally 语句会在任何匹配的 catch 语句后执行:

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

你可以阅读 Dart 核心库概览的 异常 章节获取更多相关信息。