1. 为什么“运算符、表达式和语句”是编程入门最容易被低估的一关很多人学编程前面变量和数据类型过得挺顺一到运算符、表达式和语句这块就开始犯迷糊。不是觉得太简单扫一眼就翻过去了就是被各种优先级、类型转换、短路求值搞得一头雾水。我带过不少刚入门的朋友发现一个规律凡是后面写代码总出低级bug的十有八九是这一关没打扎实。这一章的内容看起来像是数学课的延伸加减乘除、大于小于谁不会啊但编程里的运算符和数学里的运算符差别比想象中大得多。数学里1/2就是二分之一编程里整数除法1/2直接给你0数学里a b表示两边相等编程里这是把b的值塞进a这个盒子里。这些差异如果不搞清楚后面写循环、写条件判断、写函数全是坑。这篇文章我打算把运算符、表达式和语句这三块彻底拆开讲透。运算符是工具表达式是用工具组装出来的“半成品”语句是能独立执行、产生效果的完整指令。三者层层递进理解了它们的关系你才算真正摸到了编程的门道。适合刚学编程的新手也适合学了一段时间但总觉得基础不牢的朋友回头补课。我会用大量生活化的类比和可直接运行的代码示例把每个容易踩坑的地方都标出来。2. 运算符全解从算术到逻辑每个符号背后的门道2.1 算术运算符整数除法是新手第一坑算术运算符是最直观的一类加、减-、乘*、除/还有取余%。前三个基本不会出问题但除法这个符号在多数编程语言里藏着第一个大坑。在C、Java、PythonPython 2这类语言中两个整数相除结果还是整数小数部分直接被砍掉不是四舍五入是直接截断。比如7 / 2得到3-7 / 2得到-3向零截断。想要得到小数结果必须至少有一个操作数是浮点数写成7.0 / 2或者7 / 2.0。# Python 3 中整数除法行为已经改变但 // 仍然是整除 print(7 / 2) # 3.5 print(7 // 2) # 3 print(-7 // 2) # -4注意这里是向下取整不是向零截断Python 3 里/永远返回浮点数//才是整除。但 Python 的整除是向下取整-7 // 2得到-4这和C、Java的向零截断不一样。这个差异在跨语言写代码时特别容易搞混。取余运算符%也值得单独说。它的本质是a % b a - (a / b) * b但不同语言对负数的处理不同。Python 里-7 % 3得到2C 里得到-1。如果你在做周期性计算比如把角度归一到 0 到 359 度用 Python 的%就很省事负数也能自动归到正区间。实操心得写除法之前先问自己一句——我要的是精确值还是整除结果如果拿不准就显式地做类型转换别指望语言帮你猜。2.2 赋值运算符不是等于是“放进盒子里”赋值运算符是新手最容易和数学等号混淆的地方。数学里x 5是一个判断编程里这是一个动作把右边的值计算出来存到左边变量对应的内存空间里。由此衍生出一系列复合赋值运算符、-、*、/、%。a 3等价于a a 3。这类写法不只是省几个字符在底层实现上某些语言对复合赋值有优化而且能避免左边表达式重复求值带来的副作用。let arr [1, 2, 3]; let i 0; arr[i] 10; // 等价于 arr[i] arr[i] 10但 i 只自增一次 console.log(arr); // [11, 2, 3] console.log(i); // 1如果写成arr[i] arr[i] 10i 会自增两次结果完全不同。复合赋值在这里保证了左边只求值一次这是它比展开写法更安全的地方。还有一个经典陷阱链式赋值a b c 0。这在多数语言里是从右往左执行c 先赋 0然后 b 赋 c 的值最后 a 赋 b 的值。但如果中间涉及对象引用就要小心了a b []会让 a 和 b 指向同一个列表改一个另一个也跟着变。2.3 比较运算符返回的不只是真假比较运算符包括、!、、、、看起来简单但和的区别在JavaScript等语言中能写一本书。在 JavaScript 里会做类型转换再比较要求类型和值都相同。1 1是true1 1是false。这个设计导致了很多诡异的bug所以现在主流规范都建议一律用。console.log(0 false); // true console.log(0 false); // false console.log(null undefined); // true console.log(null undefined); // false console.log(NaN NaN); // falseNaN 不等于任何值包括自己Python 里比较值is比较身份是不是同一个对象。[1,2] [1,2]是True但[1,2] is [1,2]是False因为它们是两个不同的列表对象。小整数和短字符串在 Python 里有缓存机制a 256; b 256; a is b可能是True但换成257就变成False了。这种细节不需要死记但要知道有这回事别在身份比较上栽跟头。浮点数的比较是另一个大坑。0.1 0.2 0.3在几乎所有语言里都是false因为二进制浮点数无法精确表示 0.1 和 0.2。正确做法是比较差值是否小于一个极小值abs((0.1 0.2) - 0.3) 1e-9 # True2.4 逻辑运算符短路求值是特性不是bug逻辑运算符与、||或、!非核心特性是短路求值。a b里如果 a 为假b 根本不会被求值a || b里如果 a 为真b 也不会被求值。这个特性经常被用来做条件执行和默认值设置// 短路求值做默认值 let name inputName || 匿名用户; // 短路求值做条件执行 isLoggedIn showDashboard(); // 空值合并运算符只在 null 或 undefined 时取默认值 let count userCount ?? 0;??和||的区别在于||会把0、、false都当成假值而??只在null和undefined时才取默认值。如果用户数量真的是 0用||会错误地变成默认值用??才对。Python 里对应的写法是and、or、not短路行为一样。a or b在 a 为真时返回 a否则返回 b所以name input_name or 匿名用户也是常见写法。注意短路求值虽然方便但别把有副作用的操作放在右边指望它一定执行。doSomething() logIt()里如果doSomething()返回假logIt()永远不会跑这种隐式依赖会让代码很难调试。2.5 位运算符直接操作二进制的高效工具位运算符包括按位与、|按位或、^按位异或、~按位取反、左移、右移。日常业务代码用得少但在性能敏感场景、底层开发、算法题里非常常见。左移a n等价于a * 2^n右移a n等价于a / 2^n整数除法。位运算比乘除法快得多编译器有时会自动优化但显式写出来更可控。异或^有个有趣的性质a ^ a 0a ^ 0 a。利用这个性质可以在不使用临时变量的情况下交换两个数a 5 b 3 a a ^ b # a 6 b a ^ b # b 5 a a ^ b # a 3 print(a, b) # 3 5实际开发中更常用的是用位运算做权限标志位。比如用 1、2、4、8 分别表示读、写、删、改权限一个整数就能存所有权限组合判断权限用添加权限用|移除权限用 ~。READ 1 # 001 WRITE 2 # 010 DELETE 4 # 100 perm READ | WRITE # 011拥有读和写权限 has_read perm READ # 1真 has_delete perm DELETE # 0假 perm perm ~WRITE # 移除写权限2.6 运算符优先级不确定就加括号运算符优先级决定了表达式中谁先算谁后算。完整的优先级表很长没人会去背但有几个常见组合必须心里有数优先级运算符结合方向1最高()[].从左到右2!~--一元正负从右到左3*/%从左到右4-从左到右5从左到右6从左到右7!从左到右8从左到右9^从左到右10|从左到右11从左到右12||从左到右13?:三元从右到左14最低及复合赋值从右到左最容易出问题的是位运算和比较运算的优先级。a b c在C里会被解析成a (b c)因为优先级高于。想表达“a和b的按位与结果等于c”必须写成(a b) c。我的建议很简单除了最基础的加减乘除其他一律加括号。括号不花钱但省下的调试时间很值钱。3. 表达式从“半成品”到“可求值”的完整理解3.1 表达式的本质一切能算出值的东西表达式就是任何能求出一个值的代码片段。3 5是表达式求值为 8x 10是表达式求值为布尔值hello.toUpperCase()是表达式求值为HELLO甚至单独一个变量名x也是表达式求值为 x 当前的值。理解表达式的一个好方法是把它想象成一个“有返回值的机器”。你给它输入操作数它按规则计算吐出一个结果。函数调用是最典型的表达式因为函数执行完总会返回一个值没有显式返回的语言会返回undefined或None。表达式可以嵌套(a b) * (c - d)里a b和c - d是子表达式各自求值后再做乘法。这种嵌套结构就是表达式树编译器解析代码时就是在构建这棵树。3.2 类型转换隐式转换的坑比想象中多表达式中不同类型混在一起时语言会自动做类型转换这叫隐式转换。隐式转换的规则因语言而异是bug的高发区。JavaScript 的隐式转换规则尤其“灵活”console.log(5 3); // 53数字转字符串后拼接 console.log(5 - 3); // 2字符串转数字后相减 console.log(5 * 2); // 10两个字符串都转数字 console.log(true 1); // 2true 转成 1 console.log([] []); // 空数组转空字符串 console.log([] {}); // [object Object] console.log({} []); // 0 或 [object Object]取决于上下文号在 JavaScript 里既是加法又是字符串拼接当一边是字符串时另一边会被转成字符串。而-、*、/只做数学运算会把操作数都转成数字。这种不一致性是很多诡异bug的根源。Python 的隐式转换相对保守5 3直接报TypeError不会偷偷帮你转。这看起来不够“智能”但实际上更安全错误在早期就暴露了而不是等到运行时产生莫名其妙的结果。实操心得永远不要依赖隐式类型转换。需要字符串就显式str()需要数字就显式int()或float()。代码是写给人看的隐式转换省的那几个字符换来的是同事读代码时的困惑。3.3 短路求值在表达式中的实战应用短路求值让逻辑表达式不只是返回真假还能返回操作数本身。a || b在 a 为真时返回 a否则返回 ba b在 a 为假时返回 a否则返回 b。这个特性让逻辑运算符变成了简洁的条件选择工具。// 设置默认值 let config userConfig || defaultConfig; // 安全访问嵌套属性 let city user user.address user.address.city; // 条件渲染React 中常见 isLoading Spinner /;Python 里对应的写法是a or b和a and b行为一致。name input(姓名) or 匿名在用户直接回车时得到“匿名”。但要注意短路求值返回的是操作数本身不是布尔值。0 || 默认返回默认 || 默认也返回默认因为空字符串是假值。如果0是合法值就不能用||做默认值得用??JavaScript或显式判断。3.4 三元表达式简洁与可读性的平衡三元表达式condition ? valueIfTrue : valueIfFalse是 if-else 的表达式版本因为它能返回值所以可以直接用在赋值、函数参数、返回值等位置。# Python 的三元表达式 status 成年 if age 18 else 未成年 # 嵌套三元可读性开始下降 grade A if score 90 else B if score 80 else C if score 70 else D三元表达式适合简单的二选一场景。一旦嵌套超过两层可读性急剧下降这时候应该拆成 if-else 语句或者用字典映射。我见过有人把五层嵌套的三元表达式写在一行里维护的时候简直想骂人。3.5 表达式的副作用什么时候求值很重要大多数表达式是纯求值不改变程序状态。但有些表达式有副作用函数调用可能修改全局变量、赋值表达式会改变变量、自增自减会修改操作数。当有副作用的表达式出现在复杂表达式中时求值顺序就变得至关重要。f() g()里f 和 g 谁先执行多数语言没有明确规定编译器可以自由安排。如果 f 和 g 都修改同一个全局变量结果就不可预测了。int i 0; int arr[3] {10, 20, 30}; int x arr[i] arr[i]; // 未定义行为不同编译器结果不同这种代码在任何语言里都应该避免。把有副作用的操作拆成独立的语句执行顺序就明确了代码也更好读。4. 语句程序的基本执行单元4.1 语句和表达式的区别有没有“动作”表达式求值语句执行动作。x 1是表达式它算出一个值但什么也没改变x x 1是语句它改变了 x 的值。在大多数语言里表达式后面加分号就变成了表达式语句表示“计算这个值然后丢弃”。这个区分在 Python 里更明显。Python 的赋值是语句不能出现在表达式位置。所以if (x 5):在 Python 里直接语法错误而在 C 里这是合法的虽然通常是个bug应该是。Python 的设计者故意这么设计就是为了避免赋值和比较混淆。# Python 里这是语法错误 # if x 5: # print(five) # 正确写法 x 5 if x 5: print(five)4.2 复合语句用缩进或大括号组织代码块复合语句是把多条语句组合成一个逻辑块包括条件语句、循环语句、函数定义、类定义等。不同语言用不同方式标记代码块C、Java、JavaScript 用大括号{}Python 用缩进。Python 的缩进强制要求让代码风格高度统一但也意味着复制粘贴代码时特别容易出问题。从网页复制的代码经常混用空格和TabPython 3 直接报TabError。我的习惯是统一用4个空格编辑器设置成“Tab键插入空格”从根源上避免这个问题。# 正确的缩进 if score 60: print(及格) if score 90: print(优秀) else: print(不及格)4.3 条件语句if-elif-else 的执行逻辑条件语句根据条件决定执行哪段代码。if检查条件为真就执行对应块elifPython或else if其他语言在前面的条件都不满足时继续检查else兜底。一个常见的误解是认为if-elif-else会检查所有条件。实际上只要有一个条件为真后面的就全部跳过。所以条件的顺序很重要范围小的条件应该放在前面。# 错误顺序 60 会拦截所有大于等于60的分数 if score 60: print(及格) elif score 90: print(优秀) # 永远不会执行 # 正确顺序从严格到宽松 if score 90: print(优秀) elif score 60: print(及格) else: print(不及格)4.4 循环语句for 和 while 的选择for循环适合已知迭代次数或需要遍历序列的场景while循环适合条件满足就继续的场景。Python 的for是“for-each”风格直接遍历元素不需要索引。# 遍历列表 for item in items: print(item) # 需要索引时用 enumerate for index, item in enumerate(items): print(index, item) # while 循环 count 0 while count 10: print(count) count 1break跳出整个循环continue跳过本次迭代进入下一次。这两个关键字在嵌套循环里只作用于最内层想跳出多层循环需要额外标志变量或语言提供的标签语法。注意while循环里一定要有能改变条件的语句否则就是死循环。写while之前先想清楚循环什么时候结束把退出条件写在最显眼的地方。4.5 语句块的作用域变量在哪里活着语句块不只是组织代码还决定了变量的作用域。在 C、Java 里大括号内声明的变量在外面访问不到在 JavaScript 里var声明的变量会提升到函数作用域let和const才是块级作用域。if (true) { var a 1; let b 2; const c 3; } console.log(a); // 1var 提升到函数作用域 // console.log(b); // ReferenceError // console.log(c); // ReferenceErrorPython 没有块级作用域if块里定义的变量在外面也能访问。但函数作用域是独立的函数内赋值会创建局部变量除非用global或nonlocal声明。if True: x 10 print(x) # 10Python 没有块级作用域 def func(): y 20 # print(y) # NameError函数内的变量外面访问不到理解作用域对避免变量名冲突和意外修改至关重要。我的原则是变量尽量在离使用位置最近的地方声明作用域越小越好减少相互干扰的可能。5. 常见问题与排查技巧实录5.1 整数除法结果不对现象5 / 2得到2而不是2.5。原因两个整数相除结果被截断为整数。解决至少一个操作数转成浮点数5.0 / 2或float(5) / 2。Python 3 里/已经返回浮点数但其他语言和 Python 2 需要注意。5.2 浮点数比较不相等现象0.1 0.2 0.3返回false。原因二进制浮点数无法精确表示十进制小数。解决比较差值是否小于容差abs(a - b) 1e-9。涉及金额计算时用整数分或专门的十进制库。5.3 逻辑运算符返回的不是布尔值现象let x a || b里 x 可能是数字、字符串或对象不一定是true或false。原因短路求值返回操作数本身。解决需要布尔值时显式转换!!(a || b)或Boolean(a || b)。需要默认值时利用这个特性但要注意0、等假值的影响。5.4 运算符优先级导致结果错误现象a b c的结果和预期不符。原因优先级高于实际执行的是a (b c)。解决加括号(a b) c。养成习惯不确定就加括号。5.5 自增自减在复杂表达式中的未定义行为现象i i在不同编译器上结果不同。原因求值顺序未定义且涉及多次修改同一变量。解决把自增自减拆成独立语句不要在同一表达式里多次修改同一变量。5.6 循环变量在循环结束后仍可访问现象Python 和 JavaScript 的var在循环结束后循环变量仍然存在。原因Python 没有块级作用域var提升到函数作用域。解决用let代替varJavaScript或在循环后手动清理变量Python。更好的做法是把循环逻辑封装成函数利用函数作用域隔离。5.7 条件判断中误用赋值现象if (x 5)在 C 里永远为真。原因赋值表达式的值是赋的值5 为真。解决用比较。有些编译器会警告把警告当错误处理。Python 直接语法错误不会出现这个问题。问题原因解决整数除法截断两整数相除转浮点数或用浮点除法浮点数不相等二进制精度容差比较或十进制库逻辑运算返回非布尔短路求值显式转布尔或利用特性优先级错误运算符优先级加括号自增未定义行为求值顺序不定拆成独立语句循环变量泄漏作用域规则用块级作用域或封装函数赋值误用为比较语法允许用开启编译器警告6. 我踩过的坑和给你的实操建议刚学编程那会儿我觉得运算符优先级这种东西查表就行不用记。结果有一次写a b 2我以为先算加法再左移实际是a (b 2)因为移位优先级低于加法。这个bug找了两个小时最后用括号一包就好了。从那以后我的原则是除了a b * c这种一眼能看出来的其他全加括号。浮点数比较的坑我也踩过。做电商项目时算优惠价19.9 * 3得到59.699999999999996和59.7比较不相等导致优惠判断失效。后来所有金额计算都改成用整数分1990 * 3 5970分显示时再除以 100再也没出过问题。短路求值我一开始觉得是“偷懒写法”后来发现它是真的实用。user user.name user.name.first这种安全访问链比写三层 if 嵌套清爽多了。但我也见过有人滥用短路求值把有副作用的函数调用放在右边结果函数没执行导致数据没保存。所以我的建议是短路求值用来取值和判断别用来执行有副作用的操作。最后分享一个调试技巧当你怀疑某个表达式的结果不对时把它拆成多个中间变量每一步打印出来。比如result a b * c - d / e出问题了就写成temp1 b * c temp2 d / e result a temp1 - temp2 print(temp1, temp2, result)这样一眼就能看出是哪一步和预期不符。虽然多写了几行但调试时间省了不止十倍。代码是写给人看的清晰永远比简短重要。