☰
Dart运算符全面解析:空安全、整除取余与实战避坑指南
2026/9/29 16:58:40 网站建设 项目流程

今天进入 Dart 打卡的第四天,主题是运算符。别小看这个基础章节,它是你以后写表达式、写条件判断、写算法逻辑绕不开的地基,Flutter 页面、服务端接口、命令行脚本,最后跑起来全靠这些符号来“运转”。很多新手学到这里就是简单过一遍,结果等到真正写项目时,遇到int / int返回double、字符串比较用==、取余出现负数这类问题时,才开始一脸懵。这篇笔记我不会把官方文档抄一遍,而是按实际写代码会碰到的场景来拆:Dart 运营商有哪些、哪些跟 Java/Python 不一样、哪些地方容易翻车,最后附上我当天踩过的几个真实错误,希望对正在学 Dart 的你有点用。

1. 先搞清楚 Dart 运算符到底有哪些

1.1 用一张分类表把运算符家族认全

Dart 的运算符数量不算少,但如果按功能分类来看,其实跟 C、Java 系语言九成相似,剩下的一成是 Dart 自己的特色。我习惯把它们分成九组来记:

分类运算符典型使用场景
算术运算符+-*/~/%加减乘除、整除、取余
自增自减++--循环计数、累加器
比较运算符==!=><>=<=条件判断、排序
类型判断asisis!类型转换、类型校验
赋值运算符=+=-=等变量赋值、累加
逻辑运算符!&&||多条件组合
位运算符&|^~<<>>权限标记、图像处理、底层计算
三目运算符? :简单的 if-else 简写
空安全运算符????=?.!可空变量处理,Dart 2.12 后的核心重点

还有两个我个人非常喜欢但容易被忽略的:一个是级联运算符..,可以连续调用同一个对象的多个方法;另一个是下标运算符[]和[]=,在实现自定义容器类的时候会用到。表格里看着多,实际每天写的就那么十几个,熟能生巧。

1.2 和 Java、Python 相比,Dart 运算符的几个“性格差异”

我是在学过一点 Java 之后才转 Dart 的,所以对“习惯迁移”带来的坑很有发言权。最典型的三个差异:

第一,/的行为。Dart 里两个int相除,结果是double,想得到整数必须用~/。Java 里两个int相除直接丢掉小数部分;Python 里/是小数除法,//才是整除。这套“类型结果”直觉如果不调整,很容易写出隐式类型错误的代码。

第二,%取余的结果。Dart 的%保证结果永远是非负的(只要除数不是 0),比如-7 % 3得到的是2,而不是 Java、C 里的-1。这一点在做环形数组、循环索引、时间换算的时候特别好用,但如果你是从 Java 转过来的,第一次看到-7 % 3 == 2一定会愣一下。

第三,字符串比较。Dart 里两个字符串直接用==比较的是内容,不是引用地址。Java 里==比较引用,内容比较得用equals()。我刚转 Dart 的时候在代码里写了好几个String.equals的坏味道,编译不过才反应过来。

另外,Dart 从 2.12 开始强制空安全,?、??、??=这些运算符在别的语言里不常见,但在 Dart 里几乎是每天的必修课。我把它们单独放在第四节讲,因为它们真的值得单独讲。

2. 算术运算:最常见的五个符号,反而坑最多

2.1 四则运算和类型推断:int / int的结果不是整数

先看一段最基础的代码:

void main() { int a = 10; int b = 3; print(a + b); // 13 print(a - b); // 7 print(a * b); // 30 print(a / b); // 3.3333333333333335,类型是 double }

这里最反直觉的就是最后一行:10 / 3的结果并不是3,而是一个double。Dart 设计这个规则是为了保证除法的数学语义更准确,避免像 Java 那样悄无声息地丢精度。但副作用是,很多初学者写了int result = a / b;之后遇到编译错误,因为右边是double,左边声明int根本放不下。

如果你真的想要整数结果,有两个方案:用整除运算符~/,或者对结果调用.toInt()。我个人的建议是能整除就用~/,因为(a / b).toInt()等于先算一次浮点除法再转整数,既有性能损耗,又容易让人误解意图。

还有一种组合场景,int和double混在一起运算:

double x = 5; int y = 2; print(x + y); // 7.0,类型是 double

规则很简单:只要表达式中出现double,结果大概率就是double。这在写物理计算、金融金额计算时尤其要注意,不要以为1.5 * 2会返回整数 3,它返回的是3.0。

2.2 整除与取余:~/和%一定要分清

~/是 Dart 的特色运算符,官方叫 truncating division,中文可以理解成“取整除法”。它做的事情是:先做普通除法,再往零的方向取整。比如:

print(7 ~/ 2); // 3 print(-7 ~/ 2); // -3,注意是向零取整,不是向下取整 print(-5 ~/ 2); // -2

这一点和 Python 不一样,Python 的//是向下取整,-7 // 2得到的是-4。如果你之前主写 Python,这里很容易踩坑。Dart 的~/更接近 C/Java 的整数除法截断语义,只是换了个更明确的符号。

再来看%。Dart 的%遵守欧几里得模的定义:a % b的结果r永远满足0 <= r < b.abs()。用大白话说,就是结果无论如何都不会是负数:

print(7 % 3); // 1 print(-7 % 3); // 2,不是 -1 print(7 % -3); // 1,不是 -2 print(-7 % -3); // 2

这个特性在做“环形索引”时特别香。比如你有 7 个颜色的轮播图,当前下标是5,向后移 3 个位置,直接用(5 + 3) % 7就行;如果需要向前移,比如从下标2向前移 5 个位置,直接(2 - 5) % 7得到的结果也是安全的非负索引,不需要额外加7再取余。

配套一个非常经典的实战场景:把总秒数换算成时分秒。

int totalSeconds = 3725; int hours = totalSeconds ~/ 3600; int minutes = (totalSeconds % 3600) ~/ 60; int seconds = totalSeconds % 60; print('$hours 小时 $minutes 分钟 $seconds 秒'); // 1 小时 2 分钟 5 秒

这里%扮演“剥离高层单位”的角色,~/扮演“提取当前层单位”的角色,两个配在一起,逻辑非常顺。如果你需要的是 C/Java 那种带符号的余数,Dart 的num类型也提供了.remainder()方法,比如(-7).remainder(3) == -1。日常写业务代码我很少用它,但如果你在写底层算法,知道有这个东西总好过临时去翻文档。

2.3++与--:自增自减的前后置区别

++和--的规则在 Dart 里和 Java、C 完全一致:写在前面的叫前自增,先加再用;写在后面的叫后自增,先用再加。

var i = 0; print(i++); // 输出 0,此时 i 变成 1 print(++i); // 输出 2,此时 i 的值从 1 变成 2 之后再输出

这个差异在for循环里不明显,因为你通常单独写i++,不关心它的返回值。但它一旦嵌到表达式中就很容易出问题,比如:

var j = 5; var k = j++ + ++j;

第一眼你能看出k是多少吗?答案是12:第一个j++先取值 5,j变 6;然后++j先把j变成 7,再取值 7,所以5 + 7 = 12。写这样的代码编译没问题,但维护起来非常头疼,代码评审时大概率会被要求改成多行。我的建议是:自增自减尽量独立成行,不要塞进复杂的复合表达式里,可读性比“看起来聪明”重要得多。

3. 比较、类型判断和逻辑运算:写出不会崩的条件

3.1 比较运算符与 String 的等值比较

比较运算符这一组看起来很平淡,无非是==、!=、>、<、>=、<=。真正容易出错的是两部分:字符串的等值比较和浮点数比较。

先说字符串。在 Dart 里:

String a = 'hello'; String b = 'hello'; print(a == b); // true,Dart 的 String 比较的是内容

这是很多 Java 转 Dart 的人需要“重学”的地方。Java 的==比较两个 String 的引用地址,内容相等要用equals();Dart 直接让==语义变成了“内容相等”。从使用体验来说,Dart 的设计更符合直觉,也减少了一种常见的 bug。

浮点数比较则是另一个坑。大家都知道0.1 + 0.2在 IEEE 754 下并不精确等于0.3,Dart 也不例外:

print(0.1 + 0.2 == 0.3); // false,结果是 0.30000000000000004

如果你的业务是金额计算,千万不要用==去判断浮点数相等,建议用“差值绝对值小于某个极小值”的思路,或者干脆把金额转成整数分再运算。我在做支付相关逻辑时,规定所有金额一律用整数分存储,彻底避开浮点精度问题。

3.2is、is!、as:类型三兄弟的正确用法

Dart 是强类型语言,但很多场景下你拿到的数据是动态的,比如从 JSON 里解析出来的Map,value的类型可能是int、String、List,甚至null。这个时候类型判断运算符就派上用场了:

  • is用来判断“是某种类型”,类似 Java 的instanceof
  • is!是它的反义,“不是某种类型”
  • as用来做强制类型转换,失败会抛TypeError

一个典型的例子:

Object data = 'Dart'; if (data is String) { print(data.length); // 在判断分支里,data 被自动收窄为 String } String text = data as String; // 强转,如果 data 不是 String 就抛异常

注意is后面的“自动收窄”是 Dart 很实用的特性:一旦通过data is String判断,在这个分支内就不需要再写一次(data as String),编译器自动把你当成String用,省代码又安全。

as更适合用在“我有把握类型是对的”的场景,比如final num age = json['age'] as num;。如果 JSON 数据里age字段缺失或者类型不对,它会直接抛异常。这其实是你想要的行为——让错误尽早暴露,而不是带着错误的类型数据往下走。如果你没把握,更稳妥的写法是先判断再使用:

final value = json['age']; if (value is int) { // 处理 int } else if (value is double) { // 处理 double }

另外提醒一句,as只能用于类型兼容的转换,不能用它把String转int,或者把num转String,那是强扭的瓜,运行时必定抛错。

3.3 逻辑运算符与短路求值:它不是普通布尔运算

逻辑运算符&&、||、!谁都会写,但“短路求值”这个特性才是最实用的。

&&一旦发现左边是false,右边根本不会执行;||一旦发现左边是true,右边也根本不会执行。这个特性可以用来做安全的链式判空:

bool canPlay = isLogin && user != null && user.isVip;

这段代码只有isLogin为true才会去判断user != null,只有user != null才会访问user.isVip。如果写成没有短路特性的写法,就得嵌套好几层if。

反向的用法也很有用:用||给默认条件兜底。

String name = inputName; if (name == null || name.isEmpty) { name = '默认昵称'; }

注意,Dart 的if条件必须是bool类型,这一点和 JavaScript 截然不同。在 JS 里你可以写if (name),空字符串、0、NaN 都会被当成false;Dart 里这样直接编译报错,你必须显式写成if (name != null && name.isNotEmpty)。这个“严格真值判断”对从 JS 转过来的人需要一点适应期,但适应之后你会发现代码里少了很多隐晦的 bug。

4. 赋值、空安全与位运算:三个容易被忽略的高频点

4.1 赋值与复合赋值:左边一定得是变量

基本的=没什么好说的,值得提的是复合赋值运算符+=、-=、*=、/=、~/=、%=、<<=、>>=、&=、|=、^=。它们的作用是把右边的运算结果再赋回左边的变量:

int score = 10; score += 20; // 30 score -= 5; // 25 score ~/= 2; // 12

有一类错误特别隐蔽:赋值表达式是有返回值的,所以你可以写出int b = (a += 3);这种代码,a会变成原来的值加 3,b也拿到这个新值。逻辑上没问题,但可读性极差,我建议不要这样写。

还有一个日常高频陷阱:=左边必须是变量,不能是常量。比如:

const int maxCount = 10; maxCount += 1; // 编译错误,常量不能被赋值

这看起来像是废话,但我在项目里真的见过有人把复合赋值用在const字段上,然后一脸困惑地找半天原因。

4.2 空安全运算符:??、??=、?.、!

从 Dart 2.12 开始,空安全成为语言的一部分。这组运算符必须熟练掌握,因为 Flutter、Dart 后端项目里到处都是它们。

??叫“空合并运算符”,意思是“如果左边是 null,就取右边;否则取左边”。它是最常用的一种“给默认值”写法:

String? inputName; // 注意问号,声明可空类型 String displayName = inputName ?? '游客';

??=则是“如果变量当前是空,才给它赋值”:

String? nickname; nickname ??= '默认昵称'; print(nickname); // 默认昵称 nickname = '小明'; nickname ??= '不会覆盖'; print(nickname); // 小明

这个操作符特别适合懒初始化场景,比如某个配置只有第一次访问时才需要计算默认值,后面不能随便覆盖。

?.叫空安全调用,它保证在对象为null时不会抛异常,而是直接返回null:

String? name; print(name?.length); // 输出 null,不会崩 // 等价写法: if (name != null) { print(name.length); }

最后一个!是“强制解包”,告诉编译器“我确定这个变量不是 null,直接把它当非空使用”:

String? name; name = 'Dart'; print(name!.length); // 我这里确定不是 null,才用 !

!用起来一时爽,但如果变量真的是null,运行时立刻抛Null check operator used on a null value。我的建议是:能用??和?.就不用!,!留给你有绝对把握且无法通过空安全判断覆盖的场景。

4.3 位运算符与权限标记实战

位运算符日常业务代码里用得不算多,但它们在某些场景下非常高效。Dart 的位运算符有:&按位与、|按位或、^按位异或、~按位取反、<<左移、>>右移。

相比 Java,Dart没有>>>无符号右移。这个差异在移植代码时要特别注意,如果遇到需要无符号位移的场景,得用其他方式绕。

最经典的应用是权限标记。假设一个帖子系统里有三种权限:读、写、执行,用二进制位来表示:

const int read = 1 << 0; // 0001 const int write = 1 << 1; // 0010 const int execute = 1 << 2; // 0100 int permission = read | write; // 0011,有读和写权限 bool canRead = (permission & read) == read; // true bool canExecute = (permission & execute) == execute; // false

这种写法的好处是:一个int就能表示多种权限的任意组合,判断权限时只要一个&就行,比一堆bool字段省内存也更清晰。

还有一个高频场景是从颜色值里提取通道。比如 ARGB 颜色0xFF336699,要拿到红色通道就是(color >> 16) & 0xFF,绿色是(color >> 8) & 0xFF,蓝色是color & 0xFF。做图像处理或自定义绘制的 Flutter 开发者应该很熟悉这套操作。

5. 三目运算符、级联运算符与优先级

5.1 三目运算符的正确打开方式

三目运算符condition ? expr1 : expr2是 if-else 的压缩版,适合那种“二选一”的赋值:

final String status = score >= 60 ? '及格' : '不及格'; final int pageSize = isMobile ? 10 : 20;

三目运算符虽然能写嵌套,但我不建议。嵌套三目的可读性非常差,下面这种代码看三秒才能反应过来:

final result = a > b ? (b > c ? 'b' : 'c') : (a > c ? 'a' : 'c');

在 Dart 3 里,如果你想做这种多分支选择,可以用更现代的switch表达式去替代,结构会清晰很多。如果你还是 Dart 2.x,建议碰到这种场景老老实实写 if-else。

另外有个细节:三目运算符的两个分支最好保持类型一致。如果你写final x = condition ? 1 : 'one';,编译器会推断出一个公共父类型,后续使用可能带来类型限制。Day04 阶段你只要记住这是个隐患,写代码时尽量让两个分支是同一类型即可。

5.2 级联运算符:Dart 独有的链式写法

级联运算符..是我非常喜欢的 Dart 特色,它允许你对同一个对象连续调用多个成员,而不需要一步步接收返回值:

final buffer = StringBuffer() ..write('Hello ') ..write('Dart') ..write('!'); print(buffer.toString()); // Hello Dart!

它和普通的链式调用有一个本质区别:普通链式调用.method1().method2()靠的是每个方法返回自身对象;级联运算符不关心方法返回值,它永远返回的是原始对象,所以哪怕某个方法返回void,照样可以继续级联:

final list = <int>[] ..add(1) ..add(2) ..add(3);

add方法返回void,但这不影响..继续工作,因为它操作的是list本身。这在初始化配置对象、批量添加数据时非常好用,代码看起来像流水线一样一气呵成。注意,它和?.还能组合成?..这样的空安全级联,不过实际项目里用到的不多,Day04 阶段先知道有这么回事即可。

5.3 优先级速查与易错点

运算符优先级是“看着简单,错起来莫名其妙”的重灾区。我整理了一张从高到低的速查表,背熟它比临时翻文档靠谱:

优先级(从高到低)运算符
后缀++--[].?.!
前缀-!~++--
乘除*/~/%
加减+-
位移<<>>
按位与&
按位异或^
按位或|
关系和类型<><=>=asisis!
相等==!=
逻辑与&&
逻辑或||
空合并??
三目?:
级联..
赋值=+=-=等

记忆的时候有个小口诀:后缀 > 前缀 > 乘除 > 加减 > 位移 > 位运算三兄弟 > 关系 > 相等 > 逻辑 > 空合并 > 三目 > 赋值。

两个常见的易错点值得单独拉出来说。第一个是位移和加减在一起时谁优先:

print(1 + 2 << 2); // (1 + 2) << 2,结果是 12 print(1 << 2 + 1); // 1 << (2 + 1),结果是 8

因为加减的优先级高于位移,所以第一行是3 << 2 = 12。很多人凭直觉以为是先位移再加,就写错了。第二个是??和&&混用:

final bool result = a ?? false && b;

由于&&优先级高于??,它实际等价于a ?? (false && b),而不是(a ?? false) && b。遇到这种组合,直接加括号把意图写明白,不要赌自己和同事的优先级记忆。

6. 我今天踩过的坑与最终避坑清单

6.1 四个真实踩坑记录

第一个坑是整除踩的。我在写一个分页逻辑时,起初用了int index = itemIndex / pageSize;,结果编译报错。我当时的内心活动是“这不是很自然的整数除法吗”,但 Dart 不这么认为,它坚持返回double。改成~/之后就安静了。这个坑说大不大,但几乎每个新人都要经历一次。

第二个坑是取余的负号行为。我在做时间轮播时写了(currentIndex - step) % itemCount,心里预期 Java 的负余数,还打算在后面条件判断里补负数修正。结果一调试发现(-2) % 5直接是3,我准备好的“修正负号”代码反而把结果改坏了。Dart 的“非负取余”帮我省了一堆逻辑,但前提是我先知道了它的脾气。

第三个坑是空安全。我把一个String?类型当成非空直接用,编译期报错,我当时还觉得“运行时明明不会是空的啊”。后来明白了,空安全本身就是让编译器替你把“运行时不会空”这个保证提前确认掉的机制。现在我的做法是:要么??给默认值,要么!强转,要么提前用if收窄,三种方式都写清楚意图。

第四个坑是??和逻辑运算符混用导致的优先级混乱。那次我在一个配置类里写final value = a ?? b && c;,本来想表达“a 为空时默认给b && c”,结果被优先级一搅,运行结果怎么都不对,最后加了一对括号立刻清晰了。从那之后,我给自己定了条规矩:只要表达式里同时出现两种以上运算符,优先级不确定就加括号,两层括号也没关系,读代码的人会感谢你。

6.2 避坑速查表

场景正确姿势常见错误
整数除法用~/用/后强转类型
负数取余Dart 的%返回非负结果按 Java/C 的负余数思维写修正逻辑
字符串比较用==比较内容误以为要写equals()
空值兜底用??或??=直接解引用可空变量
强制非空确定后再用!没判空就!,运行时崩
优先级不明加括号全靠记忆力写复杂表达式
浮点相等避免==,用差值绝对值判断直接比较0.1 + 0.2和0.3
权限开关用位运算&、|用一堆布尔字段手动组合

6.3 运算符覆写:一个可以留到明天的进阶话题

Dart 允许你覆写一部分运算符,这在实际写自定义类型时很实用。举个最简单的例子:

class Money { final double value; const Money(this.value); Money operator +(Money other) => Money(value + other.value); } void main() { final salary = Money(1000) + Money(500); print(salary.value); // 1500.0 }

需要注意,Dart 只能覆写+、-、==、[]这类运算符,不能覆写&&、||、三元运算符这些。而且覆写==时还必须同时覆写hashCode,否则在Map、Set里会出现“内容相同但 hashCode 不同”的诡异问题。这个概念适合在后面的集合章节再展开,我这里先留个记号。

最后再分享一点个人体会:学运算符不要只看不敲,每个分类找两三个例子跑一遍,尤其是~/、非负%、空安全运算符这几个 Dart 和别的语言不一样的地方,亲手跑过踩过,印象才深。明天我打算把流程控制过一遍,到时候if、for、while和今天学的运算符会一起组合出更接近真实业务的代码。如果你也在自学 Dart,建议你也把今天笔记里的代码亲手敲一遍,再想想哪些运算符是你之前语言里没有的,大概率就是你将来写 Dart 时最容易出错的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询