先问你一个场景:某天你在 code review 里看到同事写了 if user_level & 2:,而在此之前他刚刚写过 if is_admin and is_active:。你第一反应可能是"这俩不都是 and 嘛,有什么差别"。然后你默默把 & 改成 and,跑了一下测试,发现原来能通过的用例挂了,甚至某个分支再也不进去了。你没有改错,真正的问题在于:在 Python 里,& 和 and 从来就不是同一个东西。
这个题目我盘过很多次,每次给新人讲"逻辑运算"的时候,都能看到那种"原来如此"的表情。今天就把这两个运算符从头到尾掰开揉碎讲清楚,包括它们的底层机制、返回值差异、短路行为、优先级陷阱,以及在实际项目里最常见的翻车现场。适合正在学 Python 的初学者,也适合写了几年但偶尔还是会混用的老手,看完你至少能少踩三个坑。
1. 两个"与"的底层逻辑:位运算和逻辑运算从一开始就分道扬镳
1.1 逻辑 and 的真值判断:Python 如何决定一个对象是真是假
and 是逻辑运算符,它工作的前提是"真值判断"。Python 里每个对象都可以被问一句:你是 True 还是 False?这个判断靠的是对象的 __bool__() 方法,如果没定义,就退回 __len__(),如果长度是 0 就是 False,否则是 True。再不行,默认就是 True。所以空列表是 False,空字符串是 False,0 是 False,None 是 False,其余绝大多数对象都是 True。
and 做的事情是:从左到右依次判断操作数的真值。如果左边是假,直接返回左边的值,不再看右边。如果左边是真,继续看右边,最终返回决定整个表达式结果的那个对象。也就是说,a and b 的结果不是简单的 True 或 False,而是某个原始对象。
我见过很多人把 and 理解成"两个条件都成立就返回 True",这个理解在条件判断里问题不大,但一旦拿到表达式返回值上,就会踩坑。
python复制result = 1 and 2
print(result) # 2,不是 True
result2 = 0 and 2
print(result2) # 0,不是 False
是不是和直觉不一样?这就是 Python 逻辑运算符的一个重要特性:它返回的是操作数本身,而不是布尔值。后面我会专门展开讲这个。
1.2 位 & 的二进制对位:从二进制位串的角度理解
& 是位运算符,全称按位与。它的工作方式完全不关心对象是真是假,它只关心对象在内存里的二进制位。把两个操作数转换成整数(Python 的整数是任意精度的,但逻辑一样),然后逐位比对:两个位都是 1,结果位才是 1,否则是 0。
python复制# 5 的二进制是 101
# 3 的二进制是 011
# 按位与:101 & 011 = 001,也就是 1
print(5 & 3) # 1
所以 & 本质上是数值计算,不是逻辑判断。哪怕你操作的是两个布尔值,Python 也会先把 True 当成 1、False 当成 0,再做位运算。
python复制print(True & False) # 0
print(True & True) # 1
注意看,True & True 返回的是整数 1,而不是布尔 True。这就是为什么有些代码里 bool_var1 & bool_var2 能工作,但结果类型可能和你预期不一致。
1.3 一个关键事实:bool 是 int 的子类,这让混乱更容易发生
如果 & 只接受整数,那还好说。但 Python 里 bool 是 int 的子类,True 本质上就是值为 1 的整数,False 就是 0。这意味着 True & 2 这种写法完全合法,因为 True 会被当作 1 参与位运算。
python复制print(True & 2) # 0,因为 01 & 10 = 00
print(True and 2) # 2,因为 True 为真,返回 2
同一个表达式,只是把 & 换成 and,结果从 0 变成 2。这在逻辑判断里可能就是"该分支进不去"和"该分支进去了但拿到错误数据"的区别。这个继承关系让很多静态分析工具都很难直接报错,因为语法上它确实合法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 返回值差异:为什么 1 and 2 是 2,而 1 & 2 是 0
2.1 逻辑运算返回的是"决定结果的那个对象"
展开讲讲 and 的返回值规则。表达式 a and b 的执行逻辑是:
- 先算
a的真值 - 如果
a为假,返回a,不再评估b - 如果
a为真,评估b,返回b的值(不管b是真还是假)
注意最后一步,b 的返回值就是表达式的返回值,不做任何转换。所以 1 and 2 返回 2,1 and 0 返回 0,[] and 1 返回 []。
这个机制的经典用法就是默认值惯用法。比如 name = input_name or "unknown",如果 input_name 是空字符串(假),返回右边的 "unknown",否则返回输入本身。它和 if/else 等价,但更紧凑。
但很多人不知道,and 也有类似用法:x = user and user.name。如果 user 是 None,返回 None(不访问 .name 属性);如果 user 不是 None,返回 user.name。这其实是一个安全访问的惯用法,比写三行 if 要短。
问题在于,这种惯用法依赖"左侧对象决定返回值"的规则。一旦使用不当,比如 x = level and level + 1,当 level 是 0 的时候,x 会变成 0 而不是 1,后续逻辑很可能会把 0 当成"未赋值"处理,然后 bug 就来了。
2.2 位运算返回的是"按位计算后的数值"
& 的返回值规则很单纯:把两个操作数转成整数,逐位与,返回那个整数。它不会关心操作数本身的真值语义,也不会返回操作数之一。
python复制print(1 & 2) # 0
print(2 & 3) # 2
print(5 & 4) # 4
所以 1 & 2 等于 0,是因为二进制 01 与 10 按位对齐后没有任何一位同时为 1。而 1 and 2 等于 2,是因为 1 为真,直接返回右边的 2。这两个结果差了十万八千里。
这里补充一个实际案例。我有一次处理权限系统,看到一段代码:
python复制if user_role & ADMIN_ROLE:
grant_access()
user_role 是整数,ADMIN_ROLE 是常量整数,这里用 & 判断"角色的位掩码中是否包含管理员位",这是完全正确的位掩码用法,因为它要检测的是二进制位的重合,而不是两个布尔条件的逻辑与。但如果你把这段代码改成 if user_role and ADMIN_ROLE:,逻辑就变成了"user_role 非零且 ADMIN_ROLE 非零",这通常会导致所有非零角色都能进入管理员分支,严重越权。这就是 & 和 and 在真实业务里最血腥的差别。
2.3 对比表格:常见输入下 and 与 & 的输出
给你一张表,把这些组合一次看明白:
| 表达式 | a and b |
a & b |
|---|---|---|
1 and 2 / 1 & 2 |
2 | 0 |
2 and 3 / 2 & 3 |
3 | 2 |
0 and 2 / 0 & 2 |
0 | 0 |
True and False / True & False |
False | 0 |
True and 2 / True & 2 |
2 | 0 |
"" and "x" / "" & "x" |
"" | 报错 TypeError |
[1] and [2] / [1] & [2] |
[2] | 报错 TypeError |
{1,2} and {2,3} / {1,2} & {2,3} |
看到没,有两类很典型的问题。第一类是整型和布尔型混用,结果都是 0 或者 2 这种看似正常但语义完全错误的数值。第二类是字符串、列表直接用 & 直接抛 TypeError: unsupported operand type(s) for &: 'str' and 'str',这种报了错还好,至少你知道有问题。最怕的是第一种,不报错,但结果悄悄错了。
2.4 为什么说"返回原对象"是 Python 逻辑运算的隐藏特性
很多从 C 或者 Java 转过来的人会特别不适应这一点。在 C 语言里,&& 的结果一定是 0 或 1;在 Python 里,and 的结果却是某个操作数。这意味着,你可以拿 and 的结果继续参与逻辑判断,也可以把它当原始值用。
这个特性在某些场景下很好用,比如链式判断:
python复制config = get_config() and get_config().get("timeout")
如果 get_config() 返回 None,表达式返回 None,不会执行后面的 .get()。如果返回了配置对象,表达式返回配置对象的 timeout 值。但问题也在这里:如果你拿到 config 之后再做一次 if config is None 判断,没问题;如果你拿 config 去做 config + 1 之类的运算,就可能得到 None + 1 的 TypeError。因为返回值可能是任意类型,不是稳定的布尔值或者特定数值。
我在代码评审里见过一堆这样的 bug,最典型的是:
python复制result = score and score * 2
本来是想"如果 score 存在就翻倍",但当 score 等于 0 的时候,0 and score * 2 返回 0,看起来好像也"对",可实际上你把"score 为 0 并且结果应该是 0"和"score 为 0 所以返回原值"这两个语义搅在一起了。如果后续代码想区分"没有分数"和"分数为 0",这个写法就直接把信息吞了。
3. 短路行为的天壤之别:and 会"见好就收",& 永远埋头算完
3.1 短路到底是什么
短路是逻辑运算符最实用的特性。a and b 在确定 a 为假之后,就不会再去碰 b。因为不管 b 是真还是假,整个表达式已经确定是假了,没必要多算。同理,a or b 在 a 为真之后也不会去碰 b。
这个特性有很多实际价值。最典型的就是安全访问:
python复制if user and user.is_active:
send_message(user)
当 user 是 None 时,user.is_active 根本不会执行,你不会收到 AttributeError。这就是前端"可选链"语法在 Python 里的手动实现。
3.2 函数副作用案例:日志、计数、API 调用
& 没有短路,它必须把两边都算出来,然后做位运算。这个差异在操作普通变量时没什么感觉,但一旦涉及到函数调用,后果立刻呈现。
看一个例子:
python复制def get_flag():
print("计算右侧")
return 1
print(0 and get_flag())
# 输出:
# 0
# 注意没有"计算右侧",因为短路了
print(0 & get_flag())
# 输出:
# 计算右侧
# 0
# 因为 & 必须把右侧算完再按位与
这个差异在工程上有多严重?我见过一个真实案例:代码里写 if is_valid and send_email():,目标是"校验通过后发邮件,返回值决定是否继续",因为 and 的短路,校验失败时 send_email 压根不执行,这正是预期的"不通过就不发邮件"。但如果有人图省事把 and 改成 &,比如 if is_valid & send_email():,那无论 is_valid 是不是 False,send_email 都会执行,等于撤掉了最后一道防线。邮件该发的、不该发的,全发出去了。
3.3 性能差异:大量数据下的实测
短路对性能的影响在单次判断里微乎其微,但在循环和大量计算的场景下会放大。用一个简单的计时例子:
python复制import time
def heavy_calc():
time.sleep(0.01)
return 1
start = time.time()
for _ in range(100):
result = 0 and heavy_calc()
print("and 短路耗时:", time.time() - start)
start = time.time()
for _ in range(100):
result = 0 & heavy_calc()
print("& 不短路耗时:", time.time() - start)
我这台机器上,and 版本的循环耗时 0.0001 秒上下,& 版本耗时 1 秒左右,差别接近一万倍。因为 & 每次都老老实实把 heavy_calc 跑完,而 and 看到左边是 0 直接就返回了。
当然,如果你的右侧只是一个简单的变量读取,这个性能差异可以忽略。但在做数据过滤、递归计算、外部服务调用这些场景里,短路带来的收益是实打实的。
3.4 如果混用会导致什么
混用最典型的问题就是"我以为它短路了,其实没有"。比如这段代码:
python复制def check_permission(user):
if not user:
return False
return user.permission & REQUIRED_PERMISSION
这里 & 放在 if 条件里,首先要明确 check_permission 的返回值大概率是整数,比如 0 或某个权限值。你在 if check_permission(user): 的地方用布尔判断,0 是假,非零是真,所以位掩码通常能工作。但一旦权限值恰好是 2,而你写的条件是 if check_permission(user) == True:,就会失败,因为 2 == True 是 False。
短路的混用更容易出现在防御式编程里。比如:
python复制def process(data):
if data is not None & "key" in data:
return data["key"]
这段代码会直接抛 TypeError,因为 & 的优先级高于 in,它先尝试计算 None & "key",这两个类型根本不支持 &。如果这里用的是 and,data is not None 为 False,直接短路,根本不会去执行 "key" in data。这也是把两者混写最直接的报错现场。
4. 当 & 被库重新定义后:set、numpy、pandas 里各有一本账
4.1 set 的 &:唯一看起来像"逻辑与"的位运算
Python 的 set 类型重载了 &,含义是集合交集。这个设计其实非常自然:两个集合取交集,逻辑上很像"两个条件同时成立"。所以很多人第一次接触 & 不是在整数位运算上,而是在集合操作上。
python复制a = {1, 2, 3}
b = {2, 3, 4}
print(a & b) # {2, 3}
但这里有个大坑:a and b 并不会做交集,而是返回 b 本身。
python复制print(a and b) # {2, 3, 4},不是交集!
因为 a 是非空集合(真值 True),and 直接返回右边的 b,和集合运算没有半点关系。我见过新手写 result = a and b 想取交集,结果把整个 b 拿过去了,后面的代码遍历 result 时多出一堆元素,排查半天才发现是 and 和 & 的差异在捣鬼。
如果你的代码里要表达集合交集,要么用 a & b,要么用 a.intersection(b)。前者简洁,后者语义更明确,适合写进公共代码。
4.2 numpy 数组:必须用 & 而不能用 and,否则抛异常
numpy 是 & 重载最典型的例子。当你对两个 numpy 布尔数组使用 &,得到的是逐元素(element-wise)逻辑与:
python复制import numpy as np
a = np.array([True, False, True])
b = np.array([True, True, False])
print(a & b)
# [ True False False ]
每个位置上的元素独立做"与"运算,结果是一个新的布尔数组。
但 and 就没这么幸运了。np.array([True, False]) and np.array([True, False]) 会直接报错:
text复制ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
原因很简单:and 需要判断左边数组的整体真值,但一个数组里有多个布尔值,Python 不知道应该按"所有元素为真"还是"任一元素为真"来算,所以干脆拒绝。
这也是很多从 Python 原生列表转 numpy 的人最容易撞的墙。你在原生列表上习惯了 if a and b:,到了 numpy 环境还想这么写,立刻抛异常。正确写法是 (a & b).all() 或者 np.logical_and(a, b)。
4.3 pandas 布尔索引:条件筛选里的经典报错
pandas 的 DataFrame 和 Series 在条件筛选时同样重载了 &。最常见的场景是:
python复制df = pd.DataFrame({
"age": [20, 35, 28, 40],
"city": ["北京", "上海", "北京", "广州"]
})
result = df[(df["age"] > 30) & (df["city"] == "北京")]
这里必须用 &,不能用 and。因为 df["age"] > 30 返回一个布尔 Series,df["city"] == "北京" 也返回一个布尔 Series,& 会逐行进行逻辑与,得到一个新的布尔 Series,用于过滤行。
如果写成 and:
python复制result = df[(df["age"] > 30) and (df["city"] == "北京")]
会报和 numpy 一样的错误:The truth value of a DataFrame is ambiguous。原因完全一样,and 要求判断整个对象的真值,而一个包含多行多列的 DataFrame 无法给出单一真值。
这里我还要强调一个很容易被忽略的细节:& 在 pandas 里的优先级高于比较运算符?不是,恰恰相反,比较运算符的优先级高于 &?实际上,在 Python 中,& 的优先级高于 == 和 >?让我给你准确结论:& 的优先级高于比较运算符,所以 df["age"] > 30 & df["city"] == "北京" 会被解析成 df["age"] > (30 & df["city"]) == "北京",然后爆炸。这就是为什么每个 pandas 教程都会拼命提醒你:每个条件都要用括号包起来。不是因为他们啰嗦,是因为不包真的会出事。
4.4 读代码时的判断准则:看到 & 先看操作数类型
混用出问题的时候,最常见的困惑是"这段代码到底想干什么"。我的经验是:看到 & 先别急着眼花缭乱,停下来看操作数的类型。
- 如果操作数是整数、布尔值,它大概率是位级操作或位掩码判断。
- 如果操作数是 set,它是集合交集。
- 如果操作数是 numpy 数组或 pandas Series、DataFrame,它是逐元素逻辑与。
- 如果操作数是自定义类的实例,那就得看这个类有没有实现
__and__方法,语义完全取决于开发者。
反过来,看到 and,语义就稳定得多:它永远是布尔逻辑真值连接,返回的是操作数对象,同时具备短路特性。
这也是我在实际排障时的习惯:先定位两个运算符两侧的变量类型,再判断这段代码原本的意图。如果是条件判断但用了 &,且两侧不是数组/集合,那大概率需要改成 and。如果是位掩码场景但被改成了 and,那问题就更严重了,属于逻辑漏洞。
5. 优先级是隐形的坑:& 和 and 在表达式里解析顺序完全不同
5.1 运算符优先级表里位置
这是另一个让人"大吃一惊"的点:& 的优先级比比较运算符高,而 and 的优先级比比较运算符低。两个运算符在表达式解析顺序上差了整整一个梯队。
Python 官方文档里,从高到低大概是:
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 高 | ** |
幂 |
*, /, //, % |
乘除取余 | |
+, - |
加减 | |
<<, >> |
移位 | |
& |
按位与 | |
^ |
按位异或 | |
| |
按位或 | |
==, !=, <, >, <=, >=, is, in |
比较 | |
not x |
逻辑非 | |
and |
逻辑与 | |
| 低 | or |
逻辑或 |
注意看,& 在算术运算和比较运算之间,and 在比较运算之下,紧贴着 or。这个排序决定了表达式 a & b == c 和 a and b == c 的解析方式完全不同。
5.2 1 & 2 == 2 与 1 and 2 == 2 的不同结果
先看一个具体例子:
python复制print(1 & 2 == 2) # 结果是什么?
print(1 and 2 == 2) # 结果是什么?
按照优先级的规则:
1 & 2 == 2 中,& 优先级高于 ==,所以先算 1 & 2,得到 0,再比较 0 == 2,结果是 False。
1 and 2 == 2 中,== 优先级高于 and,所以先算 2 == 2,得到 True,再算 1 and True,返回 True。
两个表达式看起来很像,但结果一个是 False,一个是 True。如果你在代码里看到了:
python复制if flag & mask == expected:
你以为的解析方式是 flag & (mask == expected) 或者 (flag & mask) == expected?实际它会先算 flag & mask,再和 expected 比较。如果这是你想表达的,那没问题;如果本意是"flag 为真,并且 mask == expected",那就完全错了,此时应该写 flag and mask == expected。
5.3 位运算与比较运算混用的实测错误
类似的还有:
python复制print(5 & 3 == 1) # 5 & 3 = 1,1 == 1,True
print(5 and 3 == 1) # 3 == 1 = False,5 and False,返回 False
因为 & 先算,所以 5 & 3 == 1 实际是 (5 & 3) == 1,也就是 1 == 1,True。而 and 是后算,5 and 3 == 1 实际上是 5 and (3 == 1),也就是 5 and False,返回 False。
这类表达式在代码里如果出现,几乎每个读代码的人都要停下来重新推导一遍。我在实际项目里看到过这种写法:
python复制if a & b == c and d:
...
这个表达式的解析顺序会让维护者抓狂。第一直觉是 (a & b) == (c and d)?不对。实际是 ((a & b) == c) and d。如果 d 是非空对象,最终返回值是 d,而不是布尔值;如果放进 if 条件里,又变成对 d 的真值判断。也许碰巧能跑,但逻辑已经绕了两层弯。
5.4 括号不是可选项,是必备品
经过这些案例,你应该明白一个道理:在涉及 & 和 and 混用的表达式中,括号不是可加可不加的装饰,而是保证语义正确的必需品。
我的习惯是:
- 位运算参与复杂表达式时,用括号明确分组。比如
(flags & MASK) == target。 - 逻辑运算和比较运算混用时,括号同样加上。比如
(a > 0) and (b > 0)。 - pandas 和 numpy 的条件筛选,所有条件用括号包住,再接
&或|。
写括号不是为了给解释器看,而是给明天早上改你代码的同事看的,也是给三个月后的自己看的。代码里清楚地写 (a > 0) and (b > 0),没人会误读;写 a > 0 and b > 0,也要花半秒钟确认优先级。半秒钟看似不多,但一个项目里这样的表达式多了,阅读成本就上去了。
6. 给日常写代码的几条实操建议
6.1 从意图区分:你是想做"真值判断"还是"位级处理"
这是最根本的判断标准。写代码之前先问自己:我要表达的是什么?
如果我要表达的是"这两个条件同时成立",比如"用户已登录且账号未禁用","分数大于 60 且小于 90","列表非空且有元素符合预期"——这些都是真值判断,用 and。
如果我要表达的是"对二进制位做操作",比如"读取某个标志位"、"合并权限掩码"、"检查两个集合的公共元素"——这些才是 & 的用武之地。
有一个小技巧:如果你发现自己在一个 if 条件里用了 &,但两侧操作数是布尔变量或者比较表达式,比如 if is_admin & is_active:,那基本可以断定这里应该用 and。因为 is_admin 和 is_active 已经是布尔值,你不需要对它们的位做任何操作,只需要逻辑与。if is_admin & is_active: 虽然也能跑,因为 True & True 等于 1,if 把 1 当真,但它会让读代码的人停下来想一下"这里是不是故意做位运算",这种不必要的认知负荷能省则省。
6.2 防御性写法:显式转布尔、加括号
如果你在维护一段老代码,里面有很多 & 和 and 混用的情况,又不想大改逻辑,那么最小成本的防御性写法是:
python复制if bool(a) and bool(b):
...
显式把操作数转成布尔值,再交给 and。这样不管是整数、字符串、None 还是其他对象,都先经过统一的真值判断,返回结果一定是布尔值,不会出现 5 and "hello" 返回字符串的情况。
位掩码判断则建议写成:
python复制if (user_role & ADMIN_ROLE) == ADMIN_ROLE:
...
把位运算和比较运算用括号明确分隔,避免优先级带来的歧义。这里的 == ADMIN_ROLE 是为了确保所有需要的位都置 1,而不仅仅是"有任意一个位是 1",这在权限系统里是两种完全不同的语义。比如 ADMIN_ROLE = 3(二进制 11),user_role = 1(二进制 01),user_role & ADMIN_ROLE 结果是 1,不等于 3,说明用户只有部分权限,不是完整的管理员。如果你写 if user_role & ADMIN_ROLE:,那么用户只要有 1 权限就会进入管理员分支,这是严重的安全漏洞。
6.3 用静态检查工具提前发现可疑用法
我强烈建议你在项目里开启静态检查工具,比如 Ruff、Pylint、Flake8。虽然它们不能完全替代人工 review,但很多明显的位运算滥用能提前暴露。
Ruff 里有一类规则专门针对"条件表达式里的位运算"和"布尔操作里的类型问题"。比如 E713 是针对 not in 的,不是这个场景。但 Ruff 的 PLC1901(compare-to-empty-string)、PLC1801(len-as-condition)之类,能帮你揪出不少真值判断写得不规范的地方。更直接的是,很多类型检查工具比如 mypy,在启用了严格模式之后,能通过类型标注发现你不是在算位运算却用了 & 的迹象。
当然,工具只是辅助。我在团队里一般会直接在 code review 规范里写一条:逻辑条件判断不允许使用 & 和 |,除非操作对象是明确的 numpy 数组、pandas Series 或 set。这条规矩看着简单,实际执行起来能挡住 90% 的混用问题。
6.4 自定义类时注意 __bool__ 与 __and__ 的重载
最后一个比较进阶的坑,但遇到了就很隐蔽。当你在自定义类里重载了 __and__ 方法,但没注意 __bool__ 或 __len__ 的实现,那么同一个对象在 a & b 和 a and b 里的行为可能南辕北辙。
看一个简化的例子:
python复制class Permission:
def __init__(self, value):
self.value = value
def __and__(self, other):
return Permission(self.value & other.value)
def __bool__(self):
return self.value == 0
这个类的 __and__ 重载了位运算,返回一个新的 Permission 对象。但 __bool__ 的逻辑反直觉:value 等于 0 的时候才返回 True。那么 p1 and p2 会先判断 p1 的真值,如果 p1.value 不是 0,bool(p1) 返回 False,整个表达式直接返回 p1,根本不会看 p2。而 p1 & p2 会老老实实做位运算,得到一个新的 Permission 对象。
同一个类,两个运算符,表达的是完全不同的语义,这种行为对调用方来说极其困惑。所以在设计公开 API 时,如果重载了 __and__,最好把 __bool__ 的语义设计得符合直觉,否则调用方会把位运算结果当逻辑判断用,或者反过来。更稳妥的做法是,对于这种语义复杂的类,只暴露明确的方法名,比如 has_permission("admin")、intersection(set),而不是依赖运算符重载。
最后说一点个人心得。我教过很多人区分 & 和 and,最有效的方法不是背优先级表,而是每次写之前先在心里默念一遍:我是想让两个条件同时成立,还是想对二进制位做处理?如果一句话能说清楚,运算符自然就选对了。踩过几次坑之后你会发现,凡是 & 和 and 混写出问题的地方,出错的原因从来不是"记不住规则",而是"根本没意识到它们是两个世界的东西"。这篇文章把两个世界都展示了一遍,剩下的就是你在实际代码里多留个心眼了。
