Dart运算符看起来是那种“查一眼就会”的章节但真正写起Flutter来很多人的第一个运行时崩溃就发生在?.、??和!这三兄弟身上。今天这篇是学习Day04的整理我会把Dart运算符从头到尾过一遍不只列语法还会把几个特别容易踩的坑——比如整除与取余的差异、空安全运算符和类型转换、连续比较的写法限制、级联运算符的优先级——一并讲透。这套内容适合三类人刚从Java转Dart的、正在学Flutter的、以及刚接触编程不久想扎牢基础的新手。运算符是表达式的基石后续写集合操作、Lambda、Stream处理时天天都会跟它们打交道。与其在实战里被报错反复教育不如今天就用一篇文章把这块地基夯实。1. 认识Dart运算符的全貌1.1 Dart运算符家族图谱Dart的运算符整体上继承了C语言系的风格跟Java、C#长得非常像。一个从Java转过来的开发者几乎可以零成本上手前几类运算符。但Dart也有自己的一套“私货”比如空安全运算符、级联运算符这些是其他语言里很少见甚至没有的才是今天学习的重点。按功能划分Dart运算符大致可以分为下面几类分类运算符说明算术运算符 - * / ~/ %~/是整除%是取余自增自减 --前缀、后缀两种形式赋值运算符 - * / ~/ %等复合赋值比较运算符 !结果是bool逻辑运算符类型运算符is is! as类型判断与转换空安全运算符?. ?? ?? !避免空值崩溃位运算符^ ~ 级联运算符..连续调用同一个对象条件运算符?:三目运算注意看表格里的~/这是Dart和Java/C#最大的一个区别。Java里两个整数相除如果写成/结果直接截断取整比如5 / 2结果是2。但在Dart里5 / 2的结果是2.5类型是double。想要整数除法必须用~/写成5 ~/ 2结果才是2。这个细节在写分页、计算索引、处理金额时非常重要十个人里有九个第一次都会写错。1.2 为什么运算符值得单独花一天学Dart的运算符表面上只是语法糖但学深一层它直接影响代码的安全性和可读性。举一个最简单的场景从接口拿回一个可能为null的用户对象然后取它的昵称。用Java写需要一层层判空代码膨胀得厉害用Dart空安全运算符一行就能搞定而且写错编译器直接报错把运行时的崩溃问题提前到了编译期。这就是Dart设计上最聪明的地方把容易出错的场景用运算符语法固化下来让“正确写法”比“错误写法”更短、更自然。所以Day04这堂课核心不是记符号而是建立一种条件反射看到可能为空的变量手指自动就想去敲?.和??看到类型不确定的对象第一反应是用is判断而不是盲猜强转。1.3 和其他语言的对比参照学Dart运算符时拿Java做参照系效率最高。 - * /、、、、这些符号的行为和Java几乎一致可以直接迁移。和JavaScript比Dart最大的不同是没有隐式类型转换。JavaScript里if (0)、if ()可以正常判断真假Dart里if (0)直接编译报错只有bool值能进条件判断。从JS转过来的朋友要特别注意这一点Dart对类型的执拗是刻在骨子里的。2. 从算术到赋值先把“算得准”解决掉2.1 算术运算符五种基础计算一次讲清Dart的算术运算符一共六个、-、*、/、~/、%。前三个没什么好说的跟数学一致。后三个才是容易出现理解偏差的地方。void main() { int a 17; int b 5; print(a / b); // 输出3.4返回类型是double print(a ~/ b); // 输出3返回类型是int整除 print(a % b); // 输出2取余数 }用生活化的话解释/是精确计算分蛋糕时说“17块饼干分给5个人每人3.4块”~/是“每人都拿整数份剩下的不要了”答案是3%是“分完之后手里还剩几块”答案是2。实际开发中~/最常用于分页计算、数组索引换算%最常用于判断奇偶、循环定时、哈希散列。还有一个容易被忽略的小坑取余结果的正负号跟被除数保持一致。-17 % 5的结果是-2不是2。如果业务逻辑里需要“永远非负的余数”比如做循环下标记得手动补一个偏移量或者用(x % n n) % n这种写法。当int和double混合运算时结果自动提升为double。比如print(5 2.5)输出7.5不用手动转换。但反过来把一个double赋值给int是不行的必须显式调用.toInt()。2.2 自增自减与复合赋值和--的用法跟Java完全一样前缀是先加后用后缀是先用后加。初学者最容易懵的是它在表达式中的位置带来的差异void main() { int x 5; int y x; // y 5x变成6后缀先取值再自增 int z x; // x先变成7z 7前缀先自增再取值 }实际开发中这种歧义代码能少写就少写。我见过一堆因为list[i]和list[i]混用导致的诡异Bug。除非是临时变量否则更推荐拆成两行一行自增一行取用可读性完胜。复合赋值运算符合并了“计算”和“赋值”两步。a 3等价于a a 3a ~/ 2等价于a a ~/ 2。Dart里所有算术运算符、位运算符都对应有复合版本完整的复合赋值列表是 - * / ~/ % ^ |2.3 字符串拼接与运算符实战字符串用拼接这个特性Dart同样支持但用法上有讲究。短字符串拼接没问题一旦拼接量大了每次都会生成新的字符串对象循环几千次性能就会明显劣化。更好的做法是用字符串插值String name 张三; int age 18; // 推荐做法字符串插值 String intro 我叫$name今年$age岁; // 不推荐多个加号拼接 String intro2 我叫 name 今年 age.toString() 岁;在Dart里字符串插值用$变量名或${表达式}${}里可以放任意Dart表达式比如${a b}、${user.name}。这个方法比拼接更简洁在Flutter里写日志、拼接接口参数时效率也更高。3. 比较与逻辑写让程序会“思考”的条件3.1 比较运算符的正确打开方式比较运算符 !的返回结果是一个bool值用来驱动if、while、三目运算符等控制流。这里有个Dart独有的细节字符串可以直接用比较内容。这是和Java最大的一个差异Java里比较字符串内容必须用.equals()比较的是引用地址。Dart没有这个烦恼hello hello直接返回true。对自定义对象来说默认比较的是“是不是同一个对象”等同于identical(a, b)。日常开发中如果你想让两个属性完全相同的用户对象视为“相等”就需要重写和hashCode。这个属于进阶知识但理解这个原理能帮你排查很多“明明内容一样为什么是false”的疑惑。还有另一个让新手摸不着头脑的限制Dart不支持连续比较。数学上的min x max在Dart里直接写会报错——你会看到x max先被求值成bool然后拿min bool继续比较类型对不上。正确写法是int score 85; if (score 60 score 90) { print(成绩中等); }3.2 逻辑运算符的短路效应且、||或、!非是逻辑运算符三家。它们最有价值的特点是短路求值左侧为false时右侧根本不会执行||左侧为true时右侧同样不会执行。这个特性可以用来简化判空逻辑// 只有user不为null时才去读取profile if (user ! null user.profile ! null) { print(user.profile.name); }这里还有一道高频考题return a ?? b和a ! null ? a : b是等价的但??更短。逻辑上??也可以理解为“短路取左左空取右”。它与||的区别在于||只认bool值??认的是null。这个区别从JS转过来的同学容易混记住一句话想判断是否存在用??想判断真假用||。Dart里对条件判断卡的非常死if、、||、!只接受bool类型。一个从Python或JavaScript转过来的人最容易犯的错误是写if (count)或if (name)Dart会直接拒绝编译必须先写成if (count 0)或if (name.isNotEmpty)。虽然麻烦但这其实是好事逼着你把语义写清楚少了一堆隐式转换带来的坑。3.3 三目运算符与条件表达式三目运算符条件 ? 表达式1 : 表达式2是if-else的紧凑形态在Flutter里非常常用。典型场景是根据状态切换UI颜色、文案bool isLoading true; String status isLoading ? 加载中... : 加载完成;注意这里有个优先级陷阱三目运算符的优先级非常低只比赋值运算符高一点。如果表达式里有、等运算符要小心结合顺序。最稳妥的办法是多套一层括号比如String result (a 0 b 0) ? 正数 : 含负数;4. Dart特色运算符这才是Day04的精华4.1 空安全三兄弟?.、??、??Dart从2.12开始强制开启空安全。这意味着String name这样的普通类型默认“绝不为null”编译器会阻止你往里塞null。如果确实可能为空必须显式声明成String? name。有了这个前提才诞生了下面三个极其趁手的运算符。?.是安全导航运算符。它只在左边对象不为null时才访问右边的属性或方法为null时整个表达式直接返回null。class User { String? nickname; } void main() { User? user; print(user?.nickname); // 输出null而不是抛空指针异常 }??是空值合并运算符。左边为null时取右边的默认值String displayName user?.nickname ?? 匿名用户;这三者组合起来是处理嵌套空值的神器。比如接口返回了一个可能为空的公司信息你要取公司地址用传统写法需要三层判空用Dart一行搞定// 传统写法 String address ; if (user ! null user.company ! null) { address user.company.address ?? ; } // Dart风格写法 String address user?.company?.address ?? ;??是空值赋值运算符。只有左边为null时才执行赋值否则保持原值。以及它的近亲!(非空断言运算符)用于你明确知道某个可空变量在此时不为null但又不想再写一次判空的情况。用!相当于告诉编译器“别管了我有把握”。但注意如果运行时你判断错了还是会抛TypeError。所以我的个人习惯是能用??给默认值解决的绝不用!硬解包。4.2is、is!、as类型运算符is和is!用来做类型判断返回bool值。在有继承关系或接口多态的场景下非常实用void handle(dynamic value) { if (value is int) { print(整数$value); } else if (value is String) { print(字符串$value); } }最棒的是Dart的类型提升机制一旦value is int判断通过在这个代码块内部Dart会自动把value当成int类型来用直接调用int的方法不用手动强转。这是很多人在代码里看到的“为什么is之后还能用int方法”的原因。as是显式类型转换。它跟Java的强制类型装换类似转型失败时运行时会抛TypeError。dynamic obj hello; String str obj as String; // 成功 int num obj as int; // 运行时报错类型转换失败我的建议是在不确定类型时优先用is判断后让编译器自动提升而不是直接as赌一把。as更适合你从逻辑上能确认类型的场景比如从Map取出的值明明是List只是编译器不知道。4.3 级联运算符..写配置的极简方式级联运算符是Dart独有的一大杀器。它让你在同一个对象上连续调用多个方法或设置多个属性不用反复写变量名。最典型的应用场景是在Flutter里创建UI配置// 传统写法 var sb StringBuffer(); sb.write(Hello); sb.write( ); sb.write(Dart); // 级联写法 var sb2 StringBuffer() ..write(Hello) ..write( ) ..write(Dart);..不产生新对象它左边始终是原对象右边的每次操作都作用在原对象上。写UI的时候这种链式调用省了大量重复代码。注意级联运算符的优先级非常特殊在复杂表达式中很容易出歧义。比如var x a..b..c;会被解析成var x (a..b..c);实际赋值给x的是a。如果后面还跟着赋值表达式最好加上括号比如var x (a..b)..c;。4.4 位运算符底层的二进制开关位运算符直接操作二进制的每一位听起来高端实际用起来却很“接地气”。Dart支持的位运算符包括按位与、|按位或、^按位异或、~按位取反、左移、右移。最实用的场景之一是用整数的二进制位做权限标记。假设一个用户有读、写、执行三种权限分别对应二进制的第0、1、2位int READ 1 0; // 0001 int WRITE 1 1; // 0010 int EXEC 1 2; // 0100 int userPermission READ | WRITE; // 0011读写权限 bool canWrite (userPermission WRITE) ! 0; // true另一个场景是颜色通道提取。ARGB颜色值是一个32位整数用位运算可以分别取出Alpha、Red、Green、Blue通道值int color 0xFF336699; int alpha (color 24) 0xFF; int red (color 16) 0xFF; int green (color 8) 0xFF; int blue color 0xFF;位运算在图像处理、网络协议解析、底层工具库中出场率很高日常业务代码里用得不算多。但掌握它是理解计算机底层数据表示的重要一步我建议至少把、|、、练熟。5. 运算符优先级与实战避坑指南5.1 一张表速记优先级运算符优先级决定了表达式按什么顺序求值。记住全部层级不现实但至少要把下面这张表印在脑子里。按优先级从高到低排列优先级运算符最高()、[]、.、?.、!非空断言一元-、!、~、、--、await乘除*、/、~/、%加减、-移位、位与位异或^位或关系和类型、、、、is、is!、as等值、!逻辑与逻辑或空合并??三目?:赋值、、??等有个记忆口诀括号最高单目次之乘除加减移位再位运算然后比较和类型判断接着等值和逻辑最后赋值兜底。5.2 三个真实踩过的优先级陷阱第一个是赋值和比较混用。很多人觉得if (a 1)会判true在Dart里这直接编译错误因为赋值表达式不返回bool。但在一些复杂场景里和混写就容易出错。比如检查某一项权限是否同时具备两项// 这样写是错的因为 优先级高于 if (permission READ READ) { ... } // 正确写法先运算位与再和READ比较 if ((permission READ) READ) { ... }第二个是逻辑与逻辑或混用优先级高于||。所以a || b c实际是a || (b c)。想要“a或b成立并且c成立”必须写成(a || b) c。这种代码最好直接加括号没人会因为括号说你菜。第三个是取反运算符和比较。!属于一元运算符优先级高于。所以!a b是(!a) b而不是!(a b)。如果意图是“a不等于b”直接用a ! b更清晰。5.3 典型问题快速排查表症状可能原因处方a / b结果不对拿到了小数int / int返回double换成a ~/ bif (0)编译报错Dart不接受非bool条件写成if (count ! 0)min x max编译报错不支持连续比较写成min x x max空对象调用方法崩溃忘了判空用?.或提前判空强转后运行时报错as类型不匹配改用is判断后再用字符串循环拼接很慢生成大量临时对象用StringBuffer或StringBuffer()..write()5.4 几个能抄作业的实战小片段最后放几个评论区经常有人问的场景代码。第一个是安全登录判断综合了空安全、逻辑运算和类型判断class LoginResult { final bool success; final String? message; LoginResult(this.success, this.message); } LoginResult parseResult(MapString, dynamic? response) { if (response null || response[code] ! 200) { return LoginResult(false, response?[msg] ?? 网络异常); } return LoginResult(true, response[msg] as String?); }第二个是分组取整和取余的经典场景做列表分页int pageSize 10; int total 83; int pageCount (total pageSize - 1) ~/ pageSize; // 向上取整技巧 int lastPageSize total % pageSize 0 ? pageSize : total % pageSize;第三个是Flutter里用级联配置控件属性的常见写法虽然现在很多人喜欢用..copyWith但级联依然是顺手趁手的工具// 假设有个可配置样式的RichText控制 var text RichText( text: const TextSpan(text: Hello), ) ..textAlign TextAlign.center ..maxLines 2;关于运算符的练习建议Day04的运算符内容到这里就全部过完一遍了。说实话我当年学这一课的时候看一遍觉得全会了一写Flutter Demo就各种报错。现在回头看最大的经验是运算符不需要背符号需要背的是场景。与其逐条背??、?.、??的定义不如多写几个处理接口空值的函数多写几个列表分页的工具方法。每遇到一种报错就回来看看这张优先级表多踩几次坑比看十遍文档都管用。下一篇Day05大概率要讲控制流到时候if、for、while和今天学的比较、逻辑运算符会高频联动把这章基础打牢后面会顺畅很多。