1. 操作符的本质:程序员每天都在用,却很少真正深想的底层逻辑
你有没有过这种瞬间——代码写了好几年,CRUD业务流利得不行,但突然被问一句"++i和i++到底有什么区别,从指令层面讲"?或者说,你在代码评审时看到同事写了个!!variable,第一反应是"这写的啥",第二反应是"居然还能这样"?
我做过几年一线开发,带过不少新人,也做过不少代码评审。观察下来有个很普遍的规律:大多数人对操作符的认知停留在"会用"的层面,真正理解操作符背后语义的人少之又少。这跟背不背函数没关系,而是大多数教程把操作符当成了"符号查询表"——遇到不认识的就翻一翻,真到了需要理解表达式求值顺序、副作用、类型强转规则的时候,往往就露馅了。
所以这篇内容我想认真聊聊操作符,不只是罗列"这个符号是干嘛的",而是从操作符的底层语义出发,把一元操作符、SQL里的UNION、Verilog硬件描述语言里那些非常规操作符挨个拆开揉碎。因为这些看似毫无关联的领域,实际上共享着同一套操作符认知框架——只要把逻辑捋顺了,无论你在写JavaScript、写SQL还是写Verilog,你都能一眼看穿表达式的真实行为。
先说个最基础的点:操作符到底是什么?用一句最简单的话说,操作符是"对操作数执行某种运算或逻辑判断的指令标记"。你可以把它理解为语言层面的最小语义单元。函数是封装好的逻辑块,而操作符是编译器或解释器直接识别的内建运算指令——比如+号在CPU层面可能对应ADD指令,&&对应分支跳转,:=在Verilog里对应非阻塞赋值。
但这里有个关键认知:操作符不是孤立存在的符号,而是同时携带三样东西——优先级(Precedence)、结合性(Associativity)和副作用(Side Effect)。前两个决定表达式怎么分组、怎么求值,最后一个决定这行代码执行完,除了返回值之外还会不会改变别的东西。
我用一个生活化的例子帮你理解这三要素:想象你站在收银台前算账。货架上拿了三瓶水、两包薯片、一盒口香糖,收银员手里的扫码枪每扫一个商品就往总金额上加钱。这个过程里,"扫码"就是操作符,"商品价格"是操作数,"扫完往总额上加"就是副作用——因为每一次扫码,收银机里的累计金额都会变。如果这时候有个"满100减20"的活动,"满100减20"这个逻辑就必须在合计完所有商品之后执行,这就涉及到优先级——你不能先减20再合计,那账就乱了。
表达式求值和你算账的逻辑一模一样:操作符按照优先级决定谁先算,按结合性决定同优先级下从左往右还是从右往左,按副作用决定这行代码执行完除了返回值还会影响什么。这个框架,贯穿我后文要讲的所有内容。
2. 一元操作符拆解:单操作数运算里藏着的那些反直觉细节
一元操作符(Unary Operator),顾名思义,只接收一个操作数。这类操作符在各语言里分布极其广泛,但恰恰因为形式简单,很多人反而忽略了它们背后的一些特殊语义。我把最典型、也最容易踩坑的一元操作符逐一拆开,这部分的经验既有来自日常开发调试的血泪,也有来自跟编译器行为死磕后得到的认知。
2.1 自增/自减(++/--):真面目是"读改写两步操作"
在所有一元操作符里,++和--是误解重灾区。你问一个初学者,他会告诉你"i++就是i = i + 1"。这话没错,但只对了一半——i++在绝大多数语言里的语义是"先用后加",++i是"先加后用"。
我之前带团队的时候做过一个笔试测试,让候选人写出下面这段代码的输出:
int i = 5; int a = i++; int b = ++i; printf("a=%d, b=%d, i=%d", a, b, i);正确答案是a=5, b=7, i=7。但总有相当比例的人答错,原因就在于他们没意识到i++这个表达式拆开来看其实是三部曲:先读取i的当前值(5),把5作为表达式的结果赋给a,最后才把i加1变6。等执行++i的时候,i已经是6,先加变成7,再把7赋给b。
这个细节在C/C++里还有更深的坑:如果你在一个表达式里对同一个变量多次做自增自减,比如i = i++ + ++i,这就是典型的未定义行为(Undefined Behavior),不同编译器出来的结果可能完全不一样。因为自增自减带了读改写三步操作,而C语言标准没有规定这些副作用在表达式内部的生效顺序。我早期写C++的串口通信库时就被这个坑过——同样的代码,GCC编译完输出正常,换到MSVC上结果就变了。排查了一整天才意识到是未定义行为,不是逻辑问题。
所以我的建议很简单粗暴:永远不要在同一行代码里对同一个变量执行两次以上带副作用的操作。读写分离、分步执行,牺牲一点点简洁,换来的是跨平台确定性和可维护性。
2.2 逻辑非(!)与位取反(~):一个管真假,一个管比特
很多新手会把!和~搞混,这俩在C系语言里长得像、行为却完全不同。
!是逻辑非,操作数被当成布尔值处理,结果是true或false(在C里是0或1)。它的核心语义是"取反真假"。而~是按位取反,它把操作数的每一个比特位都翻转:0变1,1变0。这两个符号的区别,用一句话总结就是:!是道德审判,只问你"这事儿成不成";~是分子级改造,把每个零件都换掉。
举个例子说明:在32位系统里,~0的结果是0xFFFFFFFF,也就是所有位都是1,对应整数 -1(补码表示)。而!0的结果是1,因为0被认为是假,取反后为真。
这个区别在写嵌入式代码或协议解析时尤其危险。我见过有人想写"当寄存器值为0时就怎样",结果写成if (~reg == 0)——这一写就废了。因为~reg在reg == 0时会得到0xFFFFFFFF,这个值永远不会等于0,条件永远不成立。正确写法是if (reg == 0)或if (!reg)。
2.3 正号(+)在JavaScript里的隐藏身份:一元转数字
在绝大多数经典语言里,一元正号+就是个语义透明的占位符——你写+5和写5没区别。但到了JavaScript里,一元正号有了一个特殊技能:强制把操作数转成数字。
这其实是JavaScript语言设计里一个非常精妙又非常容易被忽视的设计。+"123"的结果是数字123,+true的结果是1,+null的结果是0。反过来,一元负号-除了取相反数之外,同样会触发类型转换——-"123"得到 -123。
我自己的经验里,这个特性在解析表单输入时特别好用。比如你要从输入框拿一个字符串并参与数值运算,与其写parseInt(document.getElementById('num').value, 10)这么一长串,不如写const n = +document.getElementById('num').value。加号前提空字符串返回0而不是NaN,这个特性在处理默认值时能省不少防御代码。
但注意一个陷阱:如果字符串里混有非数字字符,+"12ab"返回NaN,而parseInt("12ab")返回12。这两种解析策略的选择,取决于你想要"严格还是宽松"的转换行为。
2.4 取反操作的符号魔法:从负号(-)到typeof与void
凡是接受单个操作数的操作符,广义上都属于一元操作符。除了前面讲的算数和逻辑类,还有几个容易被忽略的:
typeof:JS中负责探测变量类型的一元操作符。typeof null返回"object",这是个流传了二十多年的历史Bug,但不影响使用——因为规范已经把它定义为既定行为。void:JS里执行表达式但不返回结果,常用于<a href="javascript:void(0)">阻止链接默认跳转。本质上就是"算完扔掉"。delete:负责删除对象属性。它的返回值是个布尔值,表示删除是否成功,而且跟直觉相反:删除不存在的属性反而返回true。
我为什么要把这些放在一起讲?因为所有一元操作符都共享一个核心共性:它们的求值都严格依赖操作数本身的类型系统语义。理解这一点,你就不会犯"在C语言里用typeof""在JS里用按位取反做逻辑判断"这类跨语义错乱。
3. SQL里的UNION操作符:数据集合并的语法细节与性能模型
前文聊的都是一元操作符,这两个词被点名叫热搜,但我在实际写SQL时最常被问到的,反而是UNION这个二元(甚至多元)操作符。UNION在SQL里做的事,一句话说就是:把两个或多个SELECT查询的结果集纵向拼接成一个结果集。
但实际用起来,UNION远没有这句话看起来那么简单。
3.1 UNION与UNION ALL:排重逻辑带来数量级差异
UNION和UNION ALL最核心的区别不在语法,而在是否去重。UNION默认对合并后的结果集执行去重(distinct),相同行的数据只保留一条;UNION ALL则是简单的堆叠拼接,有多少条就返回多少条。
这个差异在数据量大的时候会变成性能杀手。原因很明确:去重意味着数据库必须对合并后的结果做排序或哈希操作,这通常需要创建临时表并执行一次DISTINCT扫描。如果你的单表数据在百万级以上,两个SELECT各返回50万行,UNION需要对这100万行做完整排序去重,而UNION ALL只是把结果拼接返回,时间差可能是几十倍。
举个我实际处理过的例子。有一次给一个订单系统做数据报表,需要把线上订单和线下订单合并统计。线上订单表有60万行,线下订单表有12万行。同事最初写的是UNION,接口响应平均耗时1.8秒。我改成UNION ALL后,耗时降到了0.3秒。为什么敢直接改?因为业务上线上订单ID和线下订单ID用不同前缀区分(比如O开头和P开头),本质不可能重复,UNION的排重是纯粹的无用功。
所以判断用哪个操作符的标准很简单:业务上两个结果集是否可能存在完全相同的行?如果不可能,无脑用UNION ALL;如果可能但重复行影响统计结果,才用UNION。
3.2 UNION的硬性规则:列数、列序、列类型的三重约束
UNION要能正确执行,必须满足一组硬性约束,违反了直接报错:
- 两个查询返回的列数必须完全一致——这好理解,拼接数据集就像拼接表格,你总不能一列对两列。
- 列的数据类型必须兼容——MySQL会做隐式类型转换,但转换规则跟SELECT直接查询时不完全一致,容易出现意外。比如一个返回字符串、一个返回整数,MySQL会按规则转成一种公共类型,但这可能导致索引失效。
- 列的顺序按位置对齐,而不是按列名对齐——这是个经典陷阱。第一条SELECT里第一列是
id、第二列是name,第二条SELECT里第一列是name、第二列是id,UNION照样能执行成功,但结果是两列数据错位拼接。我见过不止一次因为这个导致报表数据看错行的案例。
我有一个给自己团队立下的规矩:任何UNION语句必须用列别名显式指定字段名,并且在每个SELECT里用相同的别名。这样即使位置错了,至少代码评审阶段肉眼能看出来。比如:
SELECT order_id AS id, amount AS val, '线上' AS source_type FROM online_orders WHERE created_at >= '2024-01-01' UNION ALL SELECT order_id AS id, amount AS val, '线下' AS source_type FROM offline_orders WHERE created_at >= '2024-01-01'显式别名配合注释,基本能避免大多数列错位问题。
3.3 ORDER BY与LIMIT的挂载位置:很多人把排序条件放错地方
UNION配合排序和分页是一个高频踩坑点。核心规律是:ORDER BY和LIMIT只能出现在最后一个SELECT的末尾,作用范围是合并后的最终结果集。
什么意思?如果你试图让每一条SELECT内部先排序再合并,比如"先取线上订单金额最高的10条,再取线下订单金额最高的10条":
SELECT * FROM (SELECT order_id, amount FROM online_orders ORDER BY amount DESC LIMIT 10) UNION ALL SELECT * FROM (SELECT order_id, amount FROM offline_orders ORDER BY amount DESC LIMIT 10)这时候必须把每个SELECT包成子查询才能实现"内部先排序分页"。而如果你直接写:
SELECT order_id, amount FROM online_orders ORDER BY amount DESC LIMIT 10 UNION ALL SELECT order_id, amount FROM offline_orders ORDER BY amount DESC LIMIT 10MySQL大概率直接给你报语法错误,因为在UNION语义里,ORDER BY和LIMIT不允许出现在非末端的SELECT上。
如果想让最终合并后的结果排序,则应该:
SELECT order_id, amount FROM online_orders UNION ALL SELECT order_id, amount FROM offline_orders ORDER BY amount DESC LIMIT 20注意这里ORDER BY amount引用的amount是结果集里的列名。有些经验不足的开发者会把第二个SELECT里的列别名拿来做最终排序,如果别名不一致就会报"字段不存在"。
3.4 UNION的底层执行流程:别被"合并"两个字骗了
从执行计划的视角看,UNION在大多数数据库里被拆成以下几步:
- 分别执行两个SELECT,各得到一个中间结果集。
- 如果是
UNION DISTINCT(即默认的UNION),数据库会额外对所有行做一次去重,通常通过排序或哈希实现。 - 如果是
UNION ALL,直接走"追加(Append)"节点,把第二个结果集追加到第一个后面。
MySQL的执行计划里,UNION会用到一个叫Using temporary的标记,表示需要临时表来承载中间结果。凡是看到这个标记,都要长个心眼——它在数据量大时会造成磁盘临时表、性能断崖式下跌。场景允许的话,能拆成两个查询在应用层合并,就别让数据库做UNION。
4. Verilog操作符:从软件思维切换到硬件思维的关键分水岭
聊完SQL,我们直接跨到另一个极端场景——硬件描述语言Verilog。你可能觉得奇怪:一个讲操作符的博客,为什么翻到一半突然讲Verilog?因为热搜词里明确点到了verilog 操作符,而且我认为这恰好是最能检验操作符理解深度的领域。
一个写了多年软件代码的人,第一次看Verilog时通常会有一种"这语言看着眼熟,写起来处处别扭"的感觉。原因就在于:Verilog的操作符表面上跟C语言长得很像,但背后的语义模型完全不同——软件语言的操作符描述的是"怎么做计算",硬件语言的操作符描述的是"怎么接电路"。
4.1 位宽是硬件操作符的第一约束:你写的运算符决定硬件规模
在C语言里,a + b这个表达式,CPU自动帮你处理溢出不溢出。但在Verilog里,a + b对应的是综合工具生成的一个加法器电路。而加法器的位宽,直接由操作数的最大位宽决定。
举例:
wire [3:0] a; wire [3:0] b; wire [3:0] sum; assign sum = a + b;这里sum只有4位,而a + b到底能得到多少位?如果a = 4'b1111(15),b = 4'b0001(1),那么数学上的16对应的二进制是5位10000。但sum只有4位,所以高位的进位直接被截断,sum得到的是4'b0000。
这就是所谓位宽截断。软件里你顶多丢一次溢出,硬件里这会造成数据静默错误。要解决这个问题,你需要手动扩展位宽:
wire [4:0] sum; assign sum = {1'b0, a} + {1'b0, b}; // 或者用 $unsigned 等方式扩展用{1'b0, a}这种拼接操作符把操作数扩展成5位再做加法,才能完整保留进位。这个坑我在做FIR滤波器定点化的时候踩过,仿真完全正常,一上板子数据就不对,最后用波形一条一条比对才发现是高几位被截掉了。
4.2 三态逻辑与未知值:X和Z是硬件世界的特产操作数
Verilog的操作数取值不只是0和1,还有两个特殊值:x(未知态)和z(高阻态)。这个设计在仿真验证阶段极其重要,但也给操作符带来了软件世界里完全没有的语义:任何操作符遇到x或z操作数,结果都不再是确定的0或1。
比如:
wire a = 1'bx; wire b = 1'b0; assign c = a & b; // 结果是 x,因为未知值按位与上0,综合工具也不能保证最终电路行为严格来说,1'bx & 1'b0按逻辑是0,但在Verilog的仿真语义里,x表示"无法确定的电平",有的工具会优化掉,有的不会。我的建议是:千万别在RTL代码里依赖x的传播行为来做逻辑判断,X/Z 只应该出现在测试激励(Testbench)里模拟外部输入或者高阻态总线场景,比如双向IO口:
inout [7:0] data_bus; assign data_bus = enable ? tx_data : 8'bz; // 不使能时输出高阻z4.3 拼接操作符{}:硬件描述语言里的"数据布线神器"
{}拼接操作符是Verilog里非常好用也很有特色的一个符号。它做的事情是:把多个信号按字节顺序拼成一条更宽的总线。典型用法有三类:
- 零扩展和位宽调整(前文提到的
{1'b0, a}) - 组合成数据帧,比如串口协议里的起始位 + 数据位 + 停止位拼成一个帧
- 实现循环移位寄存器逻辑
reg [7:0] shift_reg; wire serial_in; always @(posedge clk) begin shift_reg <= {shift_reg[6:0], serial_in}; // 左移一位,从右侧移入新数据 end这里{shift_reg[6:0], serial_in}把原寄存器的低7位和新的输入拼成新的8位向量,实现一周期一次的移位。
在拼接操作符里有一个细节值得一提:不允许使用不定位宽的未声明常量。比如{a, 1}在某些工具里可能报错或产生不可预期的位宽扩展,正确写法是{a, 1'b1},明确指定位宽1位、值为1。这个约定其实是想让你时刻记住:硬件世界里没有"默认int",每个数都必须说清楚自己几位。
4.4 阻塞赋值=与非阻塞赋值<=:两种操作符对应了硬件时序的两类电路
这是Verilog最让软件工程师头秃的一个点:赋值操作符有俩,一个=(阻塞赋值),一个<=(非阻塞赋值)。光看符号你还以为只是写法差异,实际上它们对应了两类完全不同的硬件电路行为。
阻塞赋值=:按顺序立即执行,相当于软件里的普通赋值。写出来的代码对应组合逻辑电路——执行完上一条,下一条立刻基于新值继续算。如果在同一个always块里用阻塞赋值,综合工具会为你生成纯组合逻辑。
非阻塞赋值<=:右操作数在赋值语句执行时被读取,但是实际的寄存器更新发生在当前仿真时间步结束时。换句话说,所有非阻塞赋值在同一个always块内是"并行更新"的,它们不会互相干扰。这对应的是时序逻辑电路(触发器)。
下面的例子是经典的移位寄存器写法对比:
// 非阻塞赋值 —— 两个触发器都在时钟上升沿同时采样,结果是正确的两位移位 always @(posedge clk) begin reg_a <= reg_b; reg_b <= new_data; end // 阻塞赋值 —— 时序反而不对,reg_a拿到的是reg_b更新后的新值 always @(posedge clk) begin reg_a = reg_b; reg_b = new_data; end想真正理解这个差异,你得在心里建立一个"赋值是发生在时间快照结束时"的心智模型。我建议所有从软件转硬件的人,接触Verilog的第一周集中练这个:写几十个由=和<=拼接的小模块,在仿真波形里观察触发沿前后的值变化,比看十篇博客都有用。
5. 跨语言操作符的性格差异:搞懂一套,用对全家桶
你可能发现我前面用C、JavaScript、SQL、Verilog各自的例子讲操作符。实际上"操作符"这个词的威力,恰恰体现在跨语言对比中——每个语言设计者在设计操作符时,都对"预期使用者的思维方式"做了一次假设。这些假设,就是跨语言踩坑的根源。
5.1 两分钟看懂主流语言的“操作符性格”
| 语言 | 核心性格 | 代表性操作符 | 最容易踩的坑 |
|---|---|---|---|
| C/C++ | 贴近机器,精打细算,副作用自由 | *指针解引用、++/--、?: | 未定义行为、隐式整型提升 |
| JavaScript | 灵活包容,隐式转换大师 | +可加可拼、===/!==、?? | +到底是加法还是拼接 |
| Python | 简洁严谨,表达优先 | //整除、**幂运算、:=海象 | 没有++,用+=代替 |
| SQL | 面向集合,声明式思维 | UNION/INTERSECT/IN | 三值逻辑 NULL 比较总返回 UNKNOWN |
| Verilog | 硬件真实,位宽与时钟约束 | {}拼接、<=非阻塞、=== | X和Z传播、位宽截断 |
有些语言连操作符的数量都是刻意设计的:Python没有++和--,因为设计者认为它们违反可读性原则;C语言提供了++且允许写在一行表达式里,因为设计者倾向于信任工程师的水平。你带着一种语言的思维惯性去写另一种语言时,产品级的Bug往往就诞生于"我以为这个语法跟以前一样"的瞬间。
5.2 隐式类型转换是操作符最大的隐形变量
当操作数的类型和操作符期望的类型不一致时,语言会调用各自的隐式转换规则。这个规则在不同语言间的差异是巨大而且不直观的。经典对比:
JavaScript里:
1 + '2' // "12",数字被转成字符串做拼接 '5' - 1 // 4,字符串被转成数字做减法 [] + {} // "[object Object]",空数组被转成字符串再拼接Python里:
1 + '2' # TypeError: unsupported operand type(s) '5' - 1 # TypeErrorSQL里:
SELECT '5' + 1 -- MySQL返回6,因为字符串被转成数字为什么行为差异这么大?因为JavaScript的设计哲学是"尽量不报错,尽量帮用户猜意图";Python的设计哲学是"类型不匹配就明确报错,不让你猜";SQL里 NULL 又引入了三值逻辑(TRUE、FALSE、UNKNOWN)。
我自己的实操经验:在写要长期维护的代码时,永远不要依赖隐式转换的结果。如果你发现某一行表达式依赖了"刚好转成了我想要的类型"这一巧合,那就是技术债。要么用显式转换函数,要么拆开分步骤写清楚。
5.3 浮点比较与相等判断:操作符在IEEE 754面前的无力感
操作符直接可比对两个值,但遇到浮点数比较时,几乎所有语言都会露出软肋——因为IEEE 754标准的浮点数本质上无法精确表示大多数十进制小数。经典案例:
0.1 + 0.2 === 0.3 // false! 实际是 0.30000000000000004这不是语言Bug,是浮点存储的固有限制。操作符===严格地比较了内存里的位模式,它忠实地告诉了你"0.1 + 0.2的结果不等于0.3",只可惜这不是你想要的答案。
正确的做法是永远不要直接比较浮点相等,而是用"差值绝对值小于某个EPSILON"的方式:
function nearlyEqual(a, b, epsilon = 1e-10) { return Math.abs(a - b) < epsilon; } nearlyEqual(0.1 + 0.2, 0.3); // true跟操作符相处得越久,越会明白一个道理:操作符是一台精密的工具,它永远按照语言规范执行,从不会"照顾"你的数学直觉。学会操作符,本质上就是学会跟这种"精确的冷酷"共存。
6. 多年实操踩出来的操作符坑位与自查清单
前文讲了很多操作符的原理,但真正的经验,往往要用踩坑来换。下面这些坑,有我自己踩过的,也有帮同事排查代码时看到的,挑几个最典型的分享出来,希望你能绕过去。
6.1 优先级陷阱实录:&和==之间的爱恨情仇
我的一个亲身经历:早年写C语言的回调注册逻辑,想判断一个标志位是否为真:
if (flags & MASK == 0) { // do something }我当时的意图是(flags & MASK) == 0——按位与的结果再和0比较。但C语言的优先级表里,==的优先级高于&,所以这行代码实际被解析成flags & (MASK == 0)。MASK == 0是个布尔表达式,结果要么0要么1,之后再跟flags按位与——整个逻辑完全跑偏了。
这个Bug在代码评审时没人发现,因为它不会报错,编译不会警告,运行也很少触发问题,只有当MASK == 0为真时才会走进错误的分支。而我那次恰好踩中了。从那以后我给自己定了一条死规矩:混合使用位运算和逻辑运算时,优先级不确定就加括号,永远不要省。
同样类型的还有:JavaScript的??空值合并运算符与||逻辑或,还有Python的and/or与比较运算符,每门语言都有自己的一本优先级小算盘。最稳的办法是我在团队里推的"双括号法则"——只要表达式中出现两种不同类型的操作符,就为每一段加上括号。代码稍微长了一点,但彻底消除了优先级歧义。
6.2 类型强制转换结合操作符的经典连环坑
JavaScript里最著名的一道面试题:
[] + [] // "" ,两个空数组都转成""然后拼接 [] + {} // "[object Object]" {} + [] // 0?!第三行为什么是0?因为前导{}被解析成了代码块而不是对象字面量,整个表达式变成了+ []——一元正号把空数组转成数字,空数组先转成空字符串"",空字符串再转成数字0。
每次讲到这个例子,我都觉得这比任何长篇大论都能说明问题:操作符的求值结果,不仅取决于操作符和操作数类型,还取决于解析器怎么切分你的代码。这种"幽灵代码块"问题在真实项目里出现时会非常隐蔽。我记得有一次我们在Node.js后端排查一个签名计算Bug,加号拼接字符串时莫名其妙丢了数据,最后查到头是一个配置文件被当成代码块解析了。从那以后,对象字面量一律加括号包起来。
6.3 操作符自查清单:我每次代码评审都会对照的问题
多年积累之后,我慢慢在代码评审环节固定了一套操作符自查清单,每次看到表达式都逐一确认。这套清单也分享给你:
- 这个表达式中是否存在隐式类型转换?如果存在,是故意为之还是无意触发?
- 同一变量是否在同一表达式中被多次写入或修改?如果是,标红复查。
- 操作符的优先级是否符合我的直觉?如果混合了位运算、比较运算、逻辑运算,统统加括号。
- 浮点数是否参与了相等比较?凡是
==或===比较浮点数的,一律改成差值容差比较。 - 是否有需要短路求值的地方?
&&和||会不会因为求值顺序产生副作用? - SQL里是否出现列类型和排序字段跨数据集合并?
UNION的去重开销是否必要? - Verilog的
=和<=是否在同一always块里混用?如果混用了,为什么?
这套清单说穿了,就是"把操作符当成有生命的东西去对待"——理解它的优先级规则,理解它的隐式转换策略,理解它产生的副作用。任何一行表达式,你都能清楚地回答"这行代码执行完,除了返回值之外还改变了什么",你就真正成了操作符的主人。
操作符不是一门"死记硬背"的知识,它是一套内功心法。写代码写到最后,你越来越会发现:真正能用得行云流水的开发者,不是记住了多少个函数API,而是对语言底层的那几十个操作符了如指掌。希望这篇东西,能帮你把这块内功补上那么一点。