Operators(运算符)
运算符用于计算新值、比较数据、组合条件或完成赋值。Dart 的运算符大多与常见编程语言相似,但整数除法 ~/、空值运算符、严格的布尔条件和级联写法具有鲜明的 Dart 特点。
学习 Dart 运算符时,重点需要掌握以下问题:
/和~/有什么区别???、??=、?.和!分别在什么时候使用?is与as如何影响类型检查?&&和||为什么不能用于任意对象?==与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 String 将 value 提升为 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
}
常用运算符从较高优先级到较低优先级,可以按下面的顺序理解:
- 一元和成员访问,例如
!value、value.member、value[index]。 - 乘除,例如
*、/、~/、%。 - 加减,例如
+、-。 - 移位和位运算,例如
<<、&、^、|。 - 关系、类型检查和相等性,例如
<、is、==。 - 逻辑运算,例如
&&、||。 - 空值运算
??。 - 条件表达式
?:。 - 赋值,例如
=、+=、??=。
不必死记整张优先级表。表达式混合了多类运算符,或分组方式不能一眼看清时,使用括号明确意图:
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()判断对象身份。- 级联让多个操作共享同一个接收者,并不等同于普通方法链。
- 表达式较复杂时使用括号明确分组,不要只依赖运算符优先级。