跳到主要内容

Operators(运算符)

运算符用于计算新值、比较数据、组合条件或完成赋值。Dart 的运算符大多与常见编程语言相似,但整数除法 ~/、空值运算符、严格的布尔条件和级联写法具有鲜明的 Dart 特点。

学习 Dart 运算符时,重点需要掌握以下问题:

  • /~/ 有什么区别?
  • ????=?.! 分别在什么时候使用?
  • isas 如何影响类型检查?
  • &&|| 为什么不能用于任意对象?
  • ==identical() 判断的是不是同一件事?

算术运算符

Dart 提供常见的加、减、乘、除和取模运算:

运算符作用示例结果
+加法5 + 2 == 7
-减法5 - 2 == 3
*乘法5 * 2 == 10
/普通除法5 / 2 == 2.5
~/整数除法5 ~/ 2 == 2
%取模5 % 2 == 1
void main() {
const total = 7;

print(total + 2); // 9
print(total / 2); // 3.5
print(total ~/ 2); // 3
print(total % 2); // 1
}

/ 的结果是 double~/ 会先完成除法,再把结果向零方向截断,因此结果是整数:

void main() {
print(7 ~/ 2); // 3
print(-7 ~/ 2); // -3
}

TypeScript 对比:TypeScript 的 number 不区分整数和浮点数,7 / 2 的结果同样是 3.5,但没有与 Dart ~/ 完全对应的运算符。对有限数值使用 Math.trunc(7 / 2) 可以得到相近结果。

自增、自减与一元运算符

一元运算符只操作一个值。常用的一元运算符包括取反、数值变号、自增和自减:

void main() {
var count = 2;

print(-count); // -2
print(!false); // true

print(count++); // 2:先返回旧值,再加 1
print(count); // 3

print(++count); // 4:先加 1,再返回新值
print(count--); // 4:先返回旧值,再减 1
print(count); // 3
}

前缀形式 ++count 先修改再产生结果,后缀形式 count++ 先产生结果再修改。两者单独作为一行时效果相同;嵌入复杂表达式时容易降低可读性,应谨慎使用。

比较与相等性

关系运算符比较两个值的大小,结果都是 bool

void main() {
const score = 85;

print(score > 60); // true
print(score >= 85); // true
print(score < 100); // true
print(score <= 80); // false
}

相等运算符 == 判断两个值是否相等,!= 表示不相等:

void main() {
print('Dart' == 'Dart'); // true
print(3 != 4); // true
}

对于自定义对象,== 的具体含义由对象类型决定。类没有重写 == 时,会沿用 Object 的默认实现,只有两个引用指向同一个对象才相等。

identical() 专门判断两个引用是否指向同一个对象。它和可能被重写的 == 不是同一种判断:

void main() {
final first = <int>[1, 2];
final second = <int>[1, 2];
final alias = first;

print(first == second); // false:List 默认不按元素比较
print(identical(first, alias)); // true
print(identical(first, second)); // false
}

TypeScript 对比:TypeScript 中两个普通对象使用 === 比较时主要判断引用是否相同。Dart 的 identical() 更接近这种身份判断,而 Dart 的 == 可以由类重写,因此不能简单等同于 TypeScript 的 ===

不要连续书写范围比较

Dart 不支持数学式的连续比较:

void main() {
const age = 20;

// 18 <= age < 65; // 编译错误
final isWorkingAge = age >= 18 && age < 65;

print(isWorkingAge); // true
}

18 <= age 已经产生 bool,不能再把这个布尔值与 65 比较。范围条件需要拆成两部分,并使用 && 连接。

逻辑运算符

Dart 使用 !&&|| 组合布尔值:

运算符作用何时为 true
!逻辑非操作数为 false
&&逻辑与左右两侧都为 true
||逻辑或左右两侧至少有一侧为 true
void main() {
const isSignedIn = true;
const hasPermission = false;

print(isSignedIn && hasPermission); // false
print(isSignedIn || hasPermission); // true
print(!isSignedIn); // false
}

&&|| 会短路求值:当左侧已经可以确定结果时,右侧不会执行。

bool checkPermission() {
print('Checking permission');
return true;
}

void main() {
const isSignedIn = false;
final canOpenPage = isSignedIn && checkPermission();

print(canOpenPage); // false
}

上例不会输出 Checking permission,因为左侧为 false 后,整个 && 表达式必然为 false

Dart 的条件必须是 bool,不能把数字、字符串或对象当作“真值”使用:

void main() {
const items = <String>[];

// if (items.length) {} // 编译错误:int 不是 bool
if (items.isNotEmpty) {
print('The list has items.');
}
}

TypeScript 对比:TypeScript 和 JavaScript 的 &&|| 可以接收任意值,并可能返回某个操作数。Dart 两侧必须是 bool,结果也一定是 bool,不存在把空字符串、0 或对象自动转换为布尔值的规则。

赋值与复合赋值

基础赋值运算符 = 把右侧的值保存到左侧变量中。复合赋值运算符会先计算,再把结果写回原变量:

void main() {
var total = 10;

total += 5; // 等价于 total = total + 5
total *= 2; // 等价于 total = total * 2
total -= 4;

print(total); // 26
}

常见复合赋值形式包括 +=-=*=/=~/=%=,位运算也有对应的复合赋值形式。

需要注意,复合赋值仍然要满足变量的静态类型:

void main() {
var count = 4; // int

count ~/= 2;
// count /= 2; // 编译错误:/ 的结果是 double

print(count); // 2
}

仅在值为 null 时赋值:??=

??= 只在变量当前为 null 时执行赋值:

void main() {
String? nickname;

nickname ??= 'Dart learner';
nickname ??= 'Another name';

print(nickname); // Dart learner
}

第二次 ??= 不会覆盖已有值。这适合为可空变量补充默认值。

空安全相关运算符

Dart 提供一组与可空类型配合使用的运算符。它们用途不同,不能互相替换。

提供备用值:??

left ?? right 会在左侧非空时返回左侧,否则计算并返回右侧:

void main() {
String? nickname;
final displayName = nickname ?? 'Anonymous';

print(displayName); // Anonymous
}

&&|| 一样,?? 也会短路求值。左侧非空时,右侧表达式不会执行。

安全访问成员:?.?[]

?. 在接收者非空时访问成员;接收者为 null 时,整个表达式的结果为 null

void main() {
String? title;
final length = title?.length;

print(length); // null
}

?[] 对索引访问执行相同的空值检查:

void main() {
List<String>? topics;
final firstTopic = topics?[0];

print(firstTopic); // null
}

?[] 只处理接收者为 null 的情况。如果列表非空但没有索引 0,访问仍会抛出范围错误。

断言值非空:!

后缀 ! 告诉类型系统:“这里的值在运行时一定不是 null”。表达式的静态类型会变成对应的不可空类型:

int titleLength(String? title) {
return title!.length;
}

void main() {
print(titleLength('Dart')); // 4
}

如果实际值是 null! 会在运行时抛出错误。它不会检查业务条件,也不会提供默认值,因此只有在程序逻辑已经保证非空、但分析器无法推断时才应使用。能够通过显式判空或 ?? 表达意图时,通常不需要 !

TypeScript 对比:两种语言都使用 ?.?? 和后缀 ! 表达相似意图。关键差异是 TypeScript 的非空断言 ! 会在生成 JavaScript 时被移除,而 Dart 的 ! 包含运行时空值检查,值为 null 时会失败。

条件表达式

条件表达式 condition ? value1 : value2 根据布尔条件从两个值中选择一个:

void main() {
const score = 72;
final result = score >= 60 ? 'passed' : 'failed';

print(result); // passed
}

它适合表达简短的二选一逻辑。分支包含多条语句或嵌套条件时,使用 if 通常更清晰。

?? 只判断左侧是否为 null,而条件表达式可以判断任意布尔条件:

final label = name ?? 'Anonymous';
final level = score >= 90 ? 'A' : 'B';

类型检查与类型转换

is 判断对象是否具有指定类型,is! 表示不具有该类型:

void main() {
Object value = 'dart';

if (value is String) {
print(value.toUpperCase()); // DART
}
}

if 分支内,分析器根据 value is Stringvalue 提升为 String,因此可以直接访问字符串成员。

as 执行类型转换。对象不属于目标类型时,会在运行时抛出类型错误:

void main() {
Object value = 'Dart';
final language = value as String;

print(language.length); // 4
}

如果只是要根据类型决定是否执行某段逻辑,优先使用 is 和类型提升。只有程序确实要求该值必须是某种类型时,才使用 as 表达这一前提。

TypeScript 对比:TypeScript 的 value as string 主要是编译期类型断言,本身不会生成运行时类型检查。Dart 的 value as String 会在运行时验证对象类型,转换不成立时会失败。

索引访问

[] 用于按照索引或键读取值,[]= 用于写入值:

void main() {
final topics = ['variables', 'operators'];
final scores = <String, int>{'Dart': 90};

print(topics[1]); // operators
topics[0] = 'types';

print(scores['Dart']); // 90
scores['TypeScript'] = 85;
}

列表索引从 0 开始。读取不存在的列表索引会抛出范围错误;读取不存在的 Map 键通常返回 null,所以 scores['Dart'] 的静态类型是 int?

位运算与移位

位运算直接处理整数的二进制位,常用于权限标记、协议字段和底层数据处理。

运算符作用
&按位与
|按位或
^按位异或
~按位取反
<<左移
>>有符号右移
>>>无符号右移
void main() {
const read = 4;
const write = 2;
const permissions = read | write;

print(permissions); // 6
print(permissions & read); // 4
print(1 << 3); // 8
print(16 >> 2); // 4
}

普通业务代码通常不需要位运算。使用时应给掩码起有意义的名字,避免在表达式中散落难以理解的数字。

级联写法

级联写法让多个操作共享同一个接收者。它使用 ..?..,整个级联表达式的结果仍然是最初的对象:

void main() {
final buffer = StringBuffer()
..write('Dart')
..write(' ')
..write('operators');

print(buffer.toString()); // Dart operators
}

这里每次 write() 都作用于同一个 StringBuffer。级联不要求方法返回当前对象,因此它与普通的方法链不同。

如果接收者可能为 null,可以在第一个级联位置使用 ?..。接收者为 null 时,整个级联都会跳过:

void appendTopic(StringBuffer? buffer) {
buffer
?..write('Dart')
..write(' operators');
}

严格来说,..?.. 是 Dart 的级联记法,不是普通运算符;理解时应关注“后续调用共享接收者”这一行为。

TypeScript 对比:TypeScript 没有与 Dart 级联完全对应的语法。普通方法链要求前一个方法返回可继续调用的对象,而 Dart 级联始终把后续操作发送给最初的接收者。

运算符优先级

优先级决定表达式如何分组。例如,乘法的优先级高于加法:

void main() {
print(2 + 3 * 4); // 14
print((2 + 3) * 4); // 20
}

常用运算符从较高优先级到较低优先级,可以按下面的顺序理解:

  1. 一元和成员访问,例如 !valuevalue.membervalue[index]
  2. 乘除,例如 */~/%
  3. 加减,例如 +-
  4. 移位和位运算,例如 <<&^|
  5. 关系、类型检查和相等性,例如 <is==
  6. 逻辑运算,例如 &&||
  7. 空值运算 ??
  8. 条件表达式 ?:
  9. 赋值,例如 =+=??=

不必死记整张优先级表。表达式混合了多类运算符,或分组方式不能一眼看清时,使用括号明确意图:

final canAccess = isSignedIn && (isAdmin || hasPermission);

运算符重载

Dart 允许类为一部分运算符定义自己的行为。例如,可以让两个二维向量使用 + 相加:

class Vector {
const Vector(this.x, this.y);

final int x;
final int y;

Vector operator +(Vector other) {
return Vector(x + other.x, y + other.y);
}


bool operator ==(Object other) {
return other is Vector && other.x == x && other.y == y;
}


int get hashCode => Object.hash(x, y);
}

void main() {
const first = Vector(1, 2);
const second = Vector(3, 4);

print(first + second == const Vector(4, 6)); // true
}

运算符方法使用 operator 声明。并非所有运算符都能重载,例如 =, &&||?? 不能由类重新定义。

重载应保持运算符通常具有的直觉含义。重写 == 时还必须同时提供一致的 hashCode:如果两个对象通过 == 判断相等,它们的哈希值也必须相同。

常见误区

误区一:使用 / 得到整数结果

7 / 2 的结果是 3.5,需要向零截断的整数结果时使用 7 ~/ 2

误区二:把非布尔值直接写进条件

Dart 没有 JavaScript 风格的隐式真值转换。列表是否为空要写 items.isNotEmpty,字符串是否为空要写 text.isNotEmpty

误区三:用 ! 代替空值处理

后缀 ! 只是断言值非空,断言错误会在运行时失败。值确实可能为空时,应使用判空、?.??

误区四:认为 == 一定比较对象身份

== 可以被类重写。需要明确判断是否为同一个对象时,使用 identical()

误区五:混合运算符却依赖读者记忆优先级

即使表达式按照规则可以正确计算,清晰的括号通常更容易维护。尤其是同时出现 &&|| 时,应让分组意图一目了然。

小结

  • / 返回 double~/ 返回向零截断后的整数结果。
  • &&|| 只接受 bool,并采用短路求值。
  • ?? 提供空值备用项,??= 仅在变量为 null 时赋值。
  • ?.?[] 安全访问可空对象,后缀 ! 则执行运行时非空断言。
  • is 用于类型检查并可触发类型提升,as 转换失败时会在运行时报错。
  • == 可以被类型重写;identical() 判断对象身份。
  • 级联让多个操作共享同一个接收者,并不等同于普通方法链。
  • 表达式较复杂时使用括号明确分组,不要只依赖运算符优先级。