资讯中心

Dart运算符全面解析:空安全、整除取余与实战避坑指南

📅 2026/10/3 16:09:19
Dart运算符全面解析:空安全、整除取余与实战避坑指南
今天进入 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先取值 5j变 6然后j先把j变成 7再取值 7所以5 7 12。写这样的代码编译没问题但维护起来非常头疼代码评审时大概率会被要求改成多行。我的建议是自增自减尽量独立成行不要塞进复杂的复合表达式里可读性比“看起来聪明”重要得多。3. 比较、类型判断和逻辑运算写出不会崩的条件3.1 比较运算符与 String 的等值比较比较运算符这一组看起来很平淡无非是、!、、、、。真正容易出错的是两部分字符串的等值比较和浮点数比较。先说字符串。在 Dart 里String a hello; String b hello; print(a b); // trueDart 的 String 比较的是内容这是很多 Java 转 Dart 的人需要“重学”的地方。Java 的比较两个 String 的引用地址内容相等要用equals()Dart 直接让语义变成了“内容相等”。从使用体验来说Dart 的设计更符合直觉也减少了一种常见的 bug。浮点数比较则是另一个坑。大家都知道0.1 0.2在 IEEE 754 下并不精确等于0.3Dart 也不例外print(0.1 0.2 0.3); // false结果是 0.30000000000000004如果你的业务是金额计算千万不要用去判断浮点数相等建议用“差值绝对值小于某个极小值”的思路或者干脆把金额转成整数分再运算。我在做支付相关逻辑时规定所有金额一律用整数分存储彻底避开浮点精度问题。3.2is、is!、as类型三兄弟的正确用法Dart 是强类型语言但很多场景下你拿到的数据是动态的比如从 JSON 里解析出来的Mapvalue的类型可能是int、String、List甚至null。这个时候类型判断运算符就派上用场了is用来判断“是某种类型”类似 Java 的instanceofis!是它的反义“不是某种类型”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 都会被当成falseDart 里这样直接编译报错你必须显式写成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会变成原来的值加 3b也拿到这个新值。逻辑上没问题但可读性极差我建议不要这样写。还有一个日常高频陷阱左边必须是变量不能是常量。比如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时不会抛异常而是直接返回nullString? 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 的位运算符有按位与、|按位或、^按位异或、~按位取反、左移、右移。相比 JavaDart没有无符号右移。这个差异在移植代码时要特别注意如果遇到需要无符号位移的场景得用其他方式绕。最经典的应用是权限标记。假设一个帖子系统里有三种权限读、写、执行用二进制位来表示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 时最容易出错的地方。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案