ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数学逻辑运算符编程实战:优先级、短路求值与德摩根定律

数学逻辑运算符编程实战:优先级、短路求值与德摩根定律 写了好几年代码之后我对“基础”这两个字的态度变了很多。刚入行那会儿总觉得越高级的东西越能体现水平后来被一个线上权限bug教育了一顿才发现自己连最基础的逻辑运算符都没完全搞明白——那个bug的最后根因是一个由四个条件拼起来的权限判断表达式因为优先级被理解错了导致某类特殊情况永远走错分支。排查到凌晨的经历到现在都记得。所以当我把这套“数学基础与编程实战”系列教程放在一起规划时毫不犹豫把“基本数学逻辑运算符”放到了第一章。它不是什么高深知识点却是后面所有条件判断、集合运算、查询语句、状态机设计都绕不开的地基。这一章的内容适合两类人刚入门想打牢基础的学习者以及写了几年代码但遇到复杂条件判断依然发怵的开发者。这一章要讲清楚四件事运算规则、优先级、短路求值和表达式化简。1. 为什么一个“看起来很简单”的话题值得放在第一章1.1 从一次线上权限判断故障说起先说那个让我印象极其深刻的线上故障。当时一个内容管理后台编辑权限的规则是这样定义的管理员或编辑角色可以编辑内容访客不行同时只要用户被封禁任何角色都不能编辑。有个同事把判断写成了if (!banned isAdmin || isEditor) { // 允许编辑 }看起来好像没毛病不是被封禁的人并且是管理员或者是编辑就放行。但真正跑起来之后被封禁的编辑用户竟然能正常进入编辑页面。原因很简单在绝大多数编程语言里的优先级高于||所以这个表达式实际被解释成了if ((!banned isAdmin) || isEditor) { // 允许编辑 }也就是“没被封禁 且 是管理员或者 是编辑”。被封禁的编辑满足后半句isEditor true整个表达式为真直接被放行了。这个问题藏了很久才被发现因为测试用例里刚好漏了“被封禁的编辑”这个组合。这件事给我一个特别深的体会基础不牢出问题的时候连排查方向都没有。你甚至会怀疑是权限模型设计有问题、是缓存数据没刷新、是网关拦截逻辑有bug唯独想不到是自己写的三个运算符坏了事。1.2 逻辑运算符比想象中多一层数学符号、编程符号与自然语言“逻辑运算符”这个词在不同场景里有三种表达体系新人经常被绕晕。第一种是数学逻辑里的符号否定用 ¬合取且用 ∧析取或用 ∨。这是数理逻辑、布尔代数和数字电路设计里的标准写法。标题里说的“数学逻辑运算符”指的就是这套体系它定义了运算本身的性质。第二种是编程语言里的符号非是!且是或是||。不同语言还有自己的变种比如 Python 里写and、or、notSQL 里写AND、OR、NOTPascal 里写and、or、not。语义一致但拼写不同。第三种是我们日常说话用的“非、且、或”。自然语言最大的问题是不严谨。比如“或”在中文里经常有两种意思有时表示“二选一”比如“你要咖啡或茶”有时表示“可以都要”比如“有学生证或老年证都可以打折”。数学里的“或”是包容性的只要有一个条件为真整体就为真两个都为真也允许。搞清楚三者的关系你再看英文文档里那句 “logical operators evaluate boolean expressions” 就不会觉得抽象了。它就是告诉你这些运算符负责对真/假做组合运算。1.3 第一章要解决的三件核心事这一章不会把逻辑运算符相关的所有边角料都塞进来我只想把最影响实战的三件事讲透。第一件三个基本运算的精确定义和真值表。你得能闭着眼说出true false是什么、!true是什么这是所有判断的地基。第二件优先级与短路求值。这是写表达式最容易翻车的地方也是很多面试官喜欢追问的隐藏机制。搞懂短路不只是为了做题更是为了写出健壮的代码——比如先判断对象不为空再去访问它的属性。第三件用德摩根定律化简式子。一个!(a b)可以被改写成!a || !b同样的逻辑换一种写法可读性和维护成本完全不同。这一章会用一个实际案例完整演示化简过程。这三件事全部打通后面读任何语言的源码、写任何复杂业务条件都会顺手很多。2. 三个基本运算的定义一张真值表讲清楚全部规则2.1 NOT非单目运算的“取反”逻辑非运算只有一个操作数它的规则一句话就能说清输入真输出假输入假输出真。A¬A真假假真在编程里最常见的用法是对一个布尔值取反is_online False if not is_online: print(用户当前离线)这里有一个很容易忽略的细节逻辑否定不等于语义相反。比如“今天不下雨”是对“今天下雨”的否定语义上成立但“他不快乐”并不是“他快乐”的否定因为中间还可能有“平静”“麻木”这类中间状态。写代码时也是一样!is_active表达的是“active 这个布尔条件不为真”而不是“用户一定处于某种相反状态”。搞清楚这一点能避免很多需求理解上的偏差。另外有些语言支持连续取反比如!!x。在 JavaScript 和 C 系语言里这是一种把任意值转成布尔值的惯用技巧先转成布尔再取反一次得到的就是Boolean(x)的效果。不过用之前要确认团队规范是否允许有些团队更倾向写显式的比较表达式。2.2 AND与与OR或两目运算的真假判断与运算的规则是两个操作数都为真结果才为真只要有一个为假结果就为假。ABA ∧ B真真真真假假假真假假假假或运算的规则是两个操作数都为假结果才为假只要有一个为真结果就为真。ABA ∨ B真真真真假真假真真假假假记忆方法也很朴实与运算是一票否决或运算是一票通过。这种差异在需求描述里经常被误解。比如“新用户或者VIP用户可以领取礼包”按数学逻辑里的“或”来理解新用户能领VIP用户能领既是新用户又是VIP的人当然也能领。但如果产品经理心里想的是“二选一两个身份不能叠加”那就要用排他或XOR了。数学里的∨不排他这一点在做需求分析时最好提前确认别等到上线了才发现理解不一致。2.3 真假值在不同语言里的“等价物”写代码时我们面对的往往不是纯粹的true和false而是各种会被“当作真”或“当作假”的值。比如在 Python 里0、空字符串、空列表、None都会在布尔上下文中被当成假值非零数字、非空字符串、非空容器、普通对象都会被当成真值。而且 Python 的and、or返回的不是布尔值而是参与运算的操作数之一。这是初学者最容易踩的坑a 0 or 默认值 # 结果是 默认值 b 非空 and 123 # 结果是 123JavaScript 更“灵活”0、、null、undefined、NaN都是 falsy其余值都是 truthy。用||设置默认值时经常有“明明给了合法值却被默认值替换”的诡异情况。SQL 又不一样数据库里的逻辑判断是三值逻辑TRUE、FALSE、UNKNOWN。NULL和任何值做比较、做与或运算结果往往不是直觉能推出的。比如NULL OR TRUE的结果是TRUE但NULL AND TRUE的结果是UNKNOWN。写 SQL 条件时如果没留意NULL的传播很容易查出意料之外的数据。所以记住同一个运算符在不同语言里的“真假等价物”并不相同。写代码前先确认你用的语言对 truthy/falsy 的定义是什么别靠在其他语言里的习惯类推。2.4 三个基本运算之外XOR等扩展基本逻辑运算符是¬、∧、∨三个但实际场景里你还会遇到它们的组合变体最常见的就是异或XOR。异或的定义是两个操作数不同则为真相同则为假。ABA XOR B真真假真假真假真真假假假异或在编程里可以直接用^Java、C、JavaScript表示也可以用基本运算符组合出来(A !B) || (!A B)。这个式子本身就是一个很好的练习素材——它用到了这节课里全部三种基本运算符。还有与非NAND和或非NOR它们分别是“与”和“或”的取反结果。数字电路里NAND 门和 NOR 门号称可以组合出任何逻辑电路算是硬件设计的基础。理解它们对排查一些底层库的位运算代码很有帮助日常业务开发不一定天天用但认识它们能让你阅读源码时更从容。3. 优先级、结合性与括号先算哪一步决定了表达式的命运3.1 数学符号体系和编程符号体系的优先级排列逻辑运算符的优先级本质上回答一个问题一个表达式里有¬、∧、∨混在一起时先算谁数学逻辑里的优先级是固定的否定运算优先级最高合取次之析取最低。所以¬A ∧ B 解释为 (¬A) ∧ B A ∨ B ∧ C 解释为 A ∨ (B ∧ C)编程语言里也是一样的逻辑!的优先级高于优先于||。于是!a b || c会被解释成((!a) b) || c。大部分主流语言C、C、Java、C#、JavaScript、Go 等都遵守这套规则。但千万不要因此觉得“所有语言都一样”。以 Ruby 为例和||的优先级与传统 C 类似但 Ruby 还有一个更低优先级的关键字版本and和or它们的优先级低到能排在赋值运算后面。同样的逻辑用关键字写和用符号写解释结果完全不同。PHP 也类似and、or的优先级比赋值还要低。这带来一个非常实用的建议如果你不确定某个表达式在不熟悉的语言里怎么解释不要靠记忆推测去翻官方文档的运算符优先级表或者直接加括号。3.2 一个反直觉的优先级实战案例再拿一个经典例子说明优先级带来的反差。假设一个用户画像系统定义“有效目标用户”为是活跃用户并且是会员 或 有过付费记录。最直观的翻译是if (isActive isVip || hasPaid) { // 满足条件的用户 }因为优先于||这个表达式实际等价于if ((isActive isVip) || hasPaid) { // 满足条件的用户 }也就是“活跃且会员”或“有付费记录”。和需求一比就能发现差异按照原需求一个不活跃但付过费的用户不应该被标记为有效目标用户按照代码的写法不活跃但付过费的用户反而被包含了。如果你把括号写上去问题当场消失if (isActive (isVip || hasPaid)) { // 满足条件的用户 }这个例子不是要否定优先级规则而是想说明人的阅读习惯和机器的解释规则不一定一致。你在脑海里会下意识地把A B || C读成“A 并且 B 或者 C”然后自然理解为“A 并且B 或者 C”。但机器看到的是(A B) || C。这种“脑内语法”和“真实语法”不一致的问题只能靠显式括号来对齐。3.3 什么时候可以信任默认优先级什么时候无条件加括号我的个人习惯是分三档来处理。第一档操作数很少、含义明确的表达式比如!isDeleted isPublished不加括号完全没问题任何人都能一眼看懂。第二档三个及以上条件混着!、、||无条件加括号。即使括号是冗余的也要加。括号不是给机器看的机器全都能算对括号是给下一个维护者看的包括三个月后的你自己。第三档团队规范有明确约定的按规范来。很多团队的代码规范里明确要求“逻辑表达式必须用括号分隔子表达式”这能极大减少 review 时的争论成本。代码规范里多一行这种要求比在代码评审里来回解释十遍优先级有效得多。从性能上来说多加几个括号不会让你的程序变慢。编译器会把表达式解析成语法树括号只影响树的形态不产生额外指令。不要为了省两个字符去挑战可读性。4. 短路求值逻辑运算符隐藏在“算得快”背后的行为4.1 短路是什么从左到右、够即停短路求值short-circuit evaluation是逻辑运算符最被低估的机制。它的规则是计算a b时如果a是假那么无论b是什么结果都是假于是b根本不会被求值。计算a || b时如果a是真那么无论b是什么结果都是真于是b不会被求值。用代码演示一下boolean flag false; boolean result flag (someMethod() 0); // 因为 flag 已经是 falsesomeMethod() 根本不会执行这个机制为什么存在从语言设计角度看逻辑运算符的目的是判断真假既然结果已经确定继续计算后续表达式就是浪费。编译器层面确实这么做的后续表达式对应的指令会被跳过。这是语言规范明确保证的行为不是优化器心血来潮的产物。4.2 短路带来的两大实战收益第一个收益是防空指针、防除零错误。这是短路求值最经典的应用。if (obj ! null obj.getValue() 100) { // 安全访问 obj 的属性和方法 }如果obj是nullobj ! null为假整个表达式直接短路obj.getValue()不会执行也就不会抛出NullPointerException。同样的道理判断除法前先判断分母if (denominator ! 0 numerator / denominator 5) { // 只有当分母不为 0 时才执行除法 }如果不短路numerator / denominator在denominator 0时会直接抛异常程序当场崩溃。这类写法在任何语言里都是必备生存技能。第二个收益是避免无意义的计算。if (list ! null list.size() 0 list.get(0).isActive()) { // 只有前两个条件都满足才去取第一个元素并判断 }这种链式写法把“安全检查”和“业务判断”串在一起每个条件都建立在前一个条件成立的基础上。一旦前面的条件不满足后续昂贵的计算比如网络请求、正则匹配、集合遍历全部被跳过效率自然更高。4.3 短路求值也有反模式副作用、误判与坑短路求值用顺手了很容易掉进反模式。最典型的坑是在逻辑表达式里依赖“必须被执行”的副作用。if (isAllowed sendEmail()) { // 当 isAllowed 为 false 时sendEmail() 不会执行 }如果sendEmail()是你期望无论如何都必须发送邮件的操作这个写法就是bug——isAllowed一旦为假邮件就静默不发了。正确的做法是把发信逻辑单独拎出来if (isAllowed) { sendEmail(); }另一个坑来自 Python 的and、or返回值。x a or b在不同语言里的行为并不一致在 Python 里a or b返回的是a如果a为真值或b本身不一定是布尔值。在 JavaScript 里a || b同样返回操作数而不是布尔值所以常被用来做默认值。在 Java、C# 里||要求操作数是布尔类型结果也是布尔类型不存在这种问题。如果你从一个语言跳到另一个语言没有及时切换思维很容易写出“看起来对但实际类型都错了”的代码。遇到这种问题第一反应不应该是抱怨语言“坑多”而是要确认当前语言对逻辑运算符返回值的精确定义。还有一点值得提SQL 里的逻辑判断通常不保证短路因为查询优化器会重排执行计划。你觉得可以先判断字段A再判断字段B数据库可能为了性能先过滤字段B。所以在 SQL 里写WHERE 字段A OR 字段B时不要指望它有和编程语言一样的短路行为更不要把“哪边先算”当成依赖。5. 德摩根定律把复杂否定化简的黄金公式5.1 两个公式与它们的记忆方式德摩根定律是逻辑运算符世界里最常用的等价变换规则。它包含两个方向¬(A ∧ B) ¬A ∨ ¬B ¬(A ∨ B) ¬A ∧ ¬B用编程语言写就是!(a b) 等价于 !a || !b !(a || b) 等价于 !a !b记忆方法可以概括成一句话把括号外面的非号“分配”进去与变或、或变与每一项各自加非。也就是“打开括号时运算符翻转操作数取反”。我当年记这两个公式靠的是反复默念“翻进去变号逐个非”顺口又管用。5.2 用生活场景理解德摩根定律光记住公式没有感觉真正理解得靠生活化的例子。“不是今天下雨 并且 我带伞了”——这句话等价于“今天没下雨 或者 我没带伞”。想一下如果“今天下雨且我带了伞”不成立那确实只有两种可能要么没下雨要么我带伞了这件事是假的两种情形至少站一个。这就是第一个公式。再看“不是这周加班 或者 周末有约”——等价于“这周不加班 并且 周末没有约”。如果“加班或有约”这个整体不成立那就说明加班不成立同时有约也不成立。这不就是我们常说的“两者都没有”吗这就是第二个公式。一旦从生活语言里找到对应德摩根定律就不再是抽象公式而是一种直觉。写代码时凡是看到!后面跟着一个大括号包住的复合条件就应该条件反射地想一下把!分配进去会不会让逻辑更清晰。5.3 化简权限判断一个完整的改写实战德摩根定律的实战价值在于把“带着否定号的复杂条件”改写成每个子条件都正向表达的版本。带否定号的表达式最容易把人绕晕。举个具体场景。一个抽奖活动规则是不允许“已经开通会员 并且 会员尚未过期”的用户参与同时也不允许黑名单用户参与。需求等价于不参与条件 (isMember !isExpired) || isBlacklist但我们通常想写的代码是“什么条件下允许参与”。允许参与条件就是上面式子的否定允许参与 !( (isMember !isExpired) || isBlacklist )这个式子能直接写进代码吗能但可读性很差尤其是那个嵌套的大括号。我们用德摩根定律逐层展开。第一步对整体的或运算取反 !(isMember !isExpired) !isBlacklist第二步对前面的与运算取反 (!isMember || isExpired) !isBlacklist第三步写成代码if ((!isMember || isExpired) !isBlacklist) { // 允许参与抽奖 }这个式子读起来非常顺不是会员或者会员已过期并且不在黑名单里。每个子条件都是正向的表达没有“否定整块”的嵌套逻辑一目了然。这个改写过程是本章最重要的练习之一。很多代码review里吵起来的点其实不是业务需求多复杂而是负号嵌套把所有人都绕晕了。用德摩根定律展开一次能省掉无数解释成本。5.4 如何验证化简结果正确真值表法化简完之后怎么确认两边真的一致最直接的方法就是列真值表。以!(a b)与!a || !b为例把a、b所有真假组合列出来逐列计算两边的结果aba ∧ b¬(a ∧ b)¬a¬b¬a ∨ ¬b两边是否一致真真真假假假假一致真假假真假真真一致假真假真真假真一致假假假真真真真一致四行全部一致公式成立。这个方法虽然朴素却是最可靠的。你不需要背公式只需要会列组合、会算基础运算就能验证任意一个逻辑表达式是否等价。实际做需求时条件一旦超过两个变量真值表的价值就更明显了——它可以帮你穷举所有边界组合避免只盯着自己脑子里那几个常见输入。6. 组合拳实战用逻辑运算符解决“闰年判断”问题6.1 问题拆解从需求到数学表达式把前面的知识串起来最好的练习是做一道经典题判断闰年。闰年规则格里高利历定义得很清晰能被4整除且不能被100整除的年份是闰年能被400整除的年份也是闰年其他情况都不是闰年。这个规则天然就是一个逻辑表达式。设三个条件变量P (y % 4 0) Q (y % 100 0) R (y % 400 0)那么闰年的数学表达式就是(P ∧ ¬Q) ∨ R翻译成自然语言就是能被4整除且不能被100整除或者能被400整除。这个表达式用到了与、或、非三种基本运算还隐含了一个容易被忽略的逻辑关系能被400整除的年份必然能被100整除也必然能被4整除。所以R为真时P和Q也是真的。理解这个层级关系后面列出真值表验证时就不会被“不合法”的组合干扰。6.2 从表达式到代码优先级与短路的综合运用按照数学表达式直接翻译可以得到一个非常清晰的代码版本def is_leap(year): return (year % 4 0 and year % 100 ! 0) or (year % 400 0)注意我在外面加了一层括号把(year % 4 0 and year % 100 ! 0)当作一个整体再和后面的year % 400 0做或运算。这个括号是多余的因为and本来就优先于or但加上括号能让人一眼看出“第一类是普通闰年、第二类是世纪闰年”可读性远远大于不加括号的版本。如果去掉括号写成return year % 4 0 and year % 100 ! 0 or year % 400 0也是正确的但我强烈不建议。理由和第3节里说的一样机器能算对但人容易看错。代码是写给人维护的优先级这件事上多一点括号少一点猜疑。换到 Java 或 JavaScript 里长得很像function isLeap(year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); }这里其实也用上了短路求值前半段year % 4 0 year % 100 ! 0如果year不能被4整除后面取余计算直接跳过整体为假再去看能不能被400整除。每一类年份走的路都不同最短路径和最长路径相差两三次取余运算数据量大的时候还是能省下不少时间的。6.3 验证方法真值表枚举与边界值测试写完成之后拿真实年份来测是必须的。最有代表性的四个年份年份P(y%40)Q(y%1000)R(y%4000)表达式结果说明2023假假假假普通平年2024真假假真普通闰年1900真真假假世纪平年能被100整除但不能被400整除2000真真真真世纪闰年能被400整除这个验证表有点意思是理论上三个变量有8种真假组合但放在闰年场景里合法的组合只有4种。因为R为真时P和Q必然为真Q为真时P必然为真Q为假时R必然为假。换句话说P假Q真、R真Q假这类组合在整数年份里根本不存在。做边界值测试时专门挑能被4整除的世纪年份比如 1900、2100它们最容易暴露“忘了判断不能被100整除”这种经典失误。我见过不少新手写的闰年函数只写了year % 4 0结果1900年直接判断成闰年一查发现错得离谱。6.4 延展同一个逻辑思想可以迁移到哪些场景闰年判断是一个“教科书级”的小场景但它背后的建模方式可以迁移到很多地方。比如表单校验一个表单允许提交的条件是“所有必填项不为空且勾选了同意协议 或 用户是内部白名单”。这跟(P ∧ ¬Q) ∨ R是同一个结构只是因为业务变量做了一些名字替换。再比如优惠券核销可用条件是“券未过期且未被使用或者该券是平台无条件赠送券”。本质上也是“一个普遍条件加一个特例条件”的组合。这种“普遍条件 特例条件”的逻辑结构在业务代码里反复出现。学会把这类需求拆解成(A ∧ B) ∨ C再一步步转成代码比硬背一百个业务规则强得多。数学逻辑运算符提供的正是这个抽象能力。7. 高频误区与自查清单这一章最容易踩的坑7.1 高频误区盘点这一章内容不算多但我在实际代码评审里见过的错误来来回回就是那几个。第一个误区混淆逻辑运算符和位运算符。是逻辑与返回布尔结果是位与会按二进制位做运算返回数值。在 Java、C#、JavaScript 里都有这种区别。true true也能算出true但语义完全不同尤其在混合使用条件判断和位掩码的代码里一个符号之差就可能计算出完全错误的结果。第二个误区在 Python 或 JavaScript 里把and、or当成必然返回布尔值的运算。我已经数不清见过多少次if x or y这种写法在 Python 里x or y可能返回y本身而不是布尔。如果y恰好是一个非空列表那整个条件就是真如果y是一个数字0条件就是假。默认值逻辑一旦依赖这种特性很容易在边界值上翻车。第三个误区把中文表达直接机械翻译成逻辑表达式。“不是会员 并且 没过期”被翻译成!isMember !isExpired这是很常见的错误。正确做法是先识别出“不是”后面包的是整块逻辑再用德摩根定律展开。少一步“展开”就直接把否定号分配进去式子就变了味。第四个误区认为所有语言短路行为一致。主流语言确实都支持短路但 SQL 这种声明式语言并不承诺执行顺序个别老语言或者特殊配置甚至可能不短路。写代码前确认当前环境的行为比背一个“放之四海而皆准”的结论更重要。7.2 自查清单写完条件表达式过一遍这5条结合这些年的经验我给自己定了一个5条自查清单每次写完稍复杂的条件表达式都会过一遍。第一条检查括号。每个复合子条件是否都用括号圈住了是否出现了三个以上条件的裸奔表达式如果有加括号。第二条检查短路依赖。我是否在逻辑表达式里依赖了“右边的代码必须执行”如果是把副作用操作移出去单独成行。第三条检查语言真假规则。当前语言对0、空字符串、null、undefined是怎么处理的我写的条件里有没有可能被隐式转换的值第四条检查取反方式。碰到!后面跟复合条件时有没有用德摩根定律展开过展开后是不是比原来更容易读如果展开后反而更绕保留原写法并加注释。第五条检查边界值。真分支和假分支都测过了吗尤其要测那些“看起来能通过但设计上不应该通过”的组合——比如被封禁的编辑、1900年、非会员且已过期用户的组合。这五条全部过一遍大多数逻辑运算符相关的低级错误都能在提交代码之前拦下来。7.3 排查逻辑问题时的一套实操思路最后分享一个排错时的实操思路。当你怀疑一个复杂的条件表达式有问题不要盯着屏幕干瞪眼拿着纸笔把真值表列出来。具体做法分四步。第一步把表达式里每个独立条件标成字母比如A、B、C。第二步写出所有合法组合。注意“合法”两个字不是每个理论组合都符合业务约束要先根据业务规则排除掉不可能出现的组合。第三步逐行计算整个表达式的值找到和你预期不符的行。第四步在那一行上下断点把每一个子条件的实际值都打出来看到底是哪一步和预期不一致。这套方法看起来很笨但它是定位问题时最不容易遗漏分支的。很多时候我们靠直觉猜猜来猜去忘了某个组合排查半天没有结论。列真值表相当于把搜索空间显式地摊开问题几乎是透明的。别嫌麻烦排错五分钟内解决真值表列一张就够了不列真值表憋半小时可能还在原地绕。到现在为止我依然保持着“写完任何带逻辑判断的代码先自己在脑内过一遍真值表”的习惯。逻辑运算符是那种你越熟练越觉得简单的工具但前提是基本功打得足够牢。这套系列后续章节里集合、函数、条件推理都会建立在今天这些运算符的基础上每一步走稳后面写复杂逻辑的时候你就能把注意力放在业务本身而不是纠缠在!、、||的战壕里。
返回列表