为什么将x + = y之类的快捷方式视为良好做法?


305

我不知道这些实际上是什么,但我一直都在看到它们。Python的实现类似于:

x += 5作为的简写x = x + 5

但是,为什么这被认为是好的做法?我在阅读过的每本有关Python,C,R等的书籍或编程教程中都涉及到它。我知道这很方便,可以节省三个按键,包括空格。但他们似乎总是我绊倒,当我阅读代码,至少在我看来,使它可读性,而不是更多。

我是否遗漏了一些在所有地方都使用过的明显原因?


@EricLippert:C#是否以与上述最佳答案相同的方式处理此问题?它实际上是更有效的CLR明智地说x += 5不是x = x + 5?还是您所建议的只是语法糖?
blesh'2

24
@blesh:关于如何在源代码中表达加法的细节,可能会影响最终的可执行代码的效率,这种情况可能在1970年就已出现;当然不是现在。优化编译器是件好事,您所担心的比这里或那里要多一纳秒。+ =运算符是在“二十年前”开发的,这显然是错误的;已故的丹尼斯·里奇(Dennis Richie)从1969年到1973年在贝尔实验室开发了C语言。
埃里克·利珀特

1
参见blogs.msdn.com/b/ericlippert/archive/2011/03/29/…(另请参阅第二部分)
SLaks 2012年

5
大多数函数式程序员都会考虑这种不好的做法。
皮特·柯坎

Answers:


598

这不是速记。

+=符号在1970年代以C语言出现,并且-具有C的“智能汇编程序”思想,对应于明显不同的机器指令和地址模式:

像“ i=i+1”,"i+=1“”和“ ++i”之类的东西虽然在抽象级别上产生相同的效果,但在较低级别上对应于处理器的不同工作方式。

特别是这三个表达式,假设i变量驻留在存储在CPU寄存器中的内存地址中(以它的名字命名D-将其视为“指向int的指针”),并且处理器的ALU接受一个参数并在一个“累加器”(我们称其为A-认为它是一个整数)。

有了这些限制(从那时开始,在所有微处理器中都很常见),转换很可能是

;i = i+1;
MOV A,(D); //Move in A the content of the memory whose address is in D
ADD A, 1;  //The addition of an inlined constant
MOV (D) A; //Move the result back to i (this is the '=' of the expression)

;i+=1;
ADD (D),1; //Add an inlined constant to a memory address stored value

;++i;
INC (D); //Just "tick" a memory located counter

第一种方法是不最佳的,但是在使用变量而不是常量(ADD A, BADD A, (D+x))进行操作或转换更复杂的表达式时(它们全部归结为堆栈中的推入低优先级操作,称为高优先级,弹出并重复进行,直到消除所有论点为止)。

第二个更典型的是“状态机”:我们不再是“求值表达式”,而是“运算值”:我们仍然使用ALU,但要避免在允许替换参数的结果周围移动值。在需要更复杂的表达的情况下,无法使用此类指令:i = 3*i + i-2由于i需要多次,因此无法就地操作。

第三个(甚至更简单)甚至没有考虑“加法”的概念,而是将更“原始”(在计算意义上)的电路用于计数器。由于对寄存器进行改造以使其成为计数器所需的组合网络较小,因此指令被缩短,加载更快并立即执行,因此比全加器之一更快。

使用当代的编译器(现在称为C),启用编译器优化,可以根据方便交换对应关系,但是语义上仍然存在概念上的差异。

x += 5 手段

  • 查找由x标识的地点
  • 加5

但是x = x + 5意味着:

  • 评估x + 5
    • 查找由x标识的地点
    • 将x复制到累加器中
    • 将5加到累加器
  • 将结果存储在x中
    • 查找由x标识的地点
    • 将累加器复制到它

当然,优化可以

  • 如果“查找x”没有副作用,则可以一次执行两个“查找”(并且x成为存储在指针寄存器中的地址)
  • 如果将ADD应用于&x累加器,则可以删除两个副本

因此,使优化的代码与该代码重合x += 5

但是,只有在“查找x”没有副作用的情况下才能这样做,否则

*(x()) = *(x()) + 5;

*(x()) += 5;

在语义上是不同的,因为将产生两次或一次x()副作用(承认x()是一个在周围做奇怪事情并返回的函数int*)。

因此,x = x + y和之间的等价关系x += y是由于+==应用于直接I值的特殊情况。

要转向Python,它继承了C的语法,但是由于在解释语言中执行代码之前没有翻译/优化,因此事情不一定是如此紧密地联系在一起(因为减少了一个解析步骤)。但是,解释器可以针对三种类型的表达式引用不同的执行例程,这取决于表达式的形成方式和评估上下文,从而利用不同的机器代码。


对于谁喜欢更多细节...

每个CPU都有一个ALU(算术逻辑单元),就其本质而言,它是一个组合网络,其输入和输出根据指令的操作码“插入”到寄存器和/或存储器中。

二进制操作通常被实现为“累加器寄存器的修改器,其输入在”某处”,在某处可以是-在指令流本身内部(对于清单内容而言是典型的:ADD A 5)-在另一个注册表内部(对于用于表达式计算而言,通常是)临时变量:例如ADD AB)-在内存中,在寄存器给定的地址处(数据获取的典型值,例如:ADD A(H))-在这种情况下,H的作用类似于解引用指针。

有了这个伪代码,x += 5

ADD (X) 5

虽然x = x+5

MOVE A (X)
ADD A 5
MOVE (X) A

也就是说,x + 5给出了一个暂时分配的临时对象。x += 5直接在x上操作。

实际的实现取决于处理器的实际指令集:如果没有ADD (.) c操作码,则第一个代码将成为第二个:没有办法。

如果存在这样的操作码并启用了优化,则在消除了反向移动并调整了寄存器操作码之后,第二个表达式将成为第一个表达式。


90
+1是唯一的答案,它解释了它过去曾经映射到不同(且效率更高)的机器代码。
彼得Török

11
@KeithThompson是的,但是不能否认汇编对C语言(以及随后的所有C风格语言)的设计产生了巨大影响
MattDavey 2012年

51
Erm,“ + =”不映射到“ inc”(映射到“ add”),“ ++”映射到“ inc”。
布伦丹

11
我认为“ 20年前”是指“ 30年前”。和BTW,COBOL有由20年使用C拍:加5 X.
JoelFan

10
理论上很棒;事实错了。x86 ASM INC仅加1,因此它不会影响此处讨论的“添加和分配”运算符(尽管这对于“ ++”和“-”是一个很好的答案)。
Mark Brackett 2012年

281

根据您的想法,它实际上更容易理解,因为它更简单。举个例子:

x = x + 5 调用“取x,将x加5,然后将该新值分配回x”的心理处理。

x += 5 可以被认为是“ x增加5”

因此,这不仅仅是速记,它实际上更直接地描述了功能。当阅读大量的代码时,它更容易掌握。


32
+1,我完全同意。我从小就开始编程,那时我的思维容易延展,x = x + 5但仍然困扰着我。当我晚些时候上数学时,这让我更加困扰。使用x += 5明显更具描述性,并且作为表达更有意义。
多项式

44
reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1某些情况下,变量的名称也很长:...哦,不!!!错字

9
@Matt Fenwick:不必是长变量名;它可能是某种形式的表达。这些可能更难以验证,并且读者必须花费大量精力来确保它们相同。
David Thornley,2012年

32
X = X + 5是那种黑客攻击的时候你的目标是通过5增加x
JeffO

1
@Polynomial我想我小时候也遇到过x = x + 5的概念。如果我没记错的话,这个数字已经9岁了-它对我来说很有意义,现在对我来说仍然很有意义,并且更喜欢x + =5。第一个更为冗长,我可以更清楚地想象这个概念-我将x赋值为x的先前值(无论是什么)然后加5的想法。我猜想不同的大脑以不同的方式工作。
克里斯·哈里森

48

至少在Python中x += y并且x = x + y可以做完全不同的事情。

例如,如果我们这样做

a = []
b = a

然后a += [3]将导致a == b == [3],而a = a + [3]将导致a == [3]b == []。也就是说,在创建新对象并将变量绑定到它的+=同时,就地修改对象(可能会做到,您可以定义该__iadd__方法执行几乎所有您喜欢的事情)=

在使用NumPy进行数值运算时,这一点非常重要,因为您经常会最终获得对数组不同部分的多个引用,并且确保您不要无意间修改了数组中其他引用的部分,这一点很重要,或不必要地复制数组(这可能非常昂贵)。


4
+1代表__iadd__:在某些语言中,您可以创建对可变数据结构的不变引用,并在其中operator +=定义,例如scala:val sb = StringBuffer(); lb += "mutable structure"vs var s = ""; s += "mutable variable"。进一步修改了数据结构的内容,而后者又将变量指向新的结构。
飞羊

43

这叫做成语。编程习惯用语很有用,因为它们是编写特定编程结构的一致方式。

每当有人写的时候,x += y您就会知道它x会被递增y而不是一些更复杂的操作(作为一种最佳实践,通常,我不会混合使用更复杂的操作和这些语法速记)。当增加1时,这是最有意义的。


13
x ++和++ x与x + = 1和彼此略有不同。
加里·威洛比

1
@Gary:++xx+=1在C和Java(也许也为C#)中是等效的,但由于存在复杂的运算符语义,因此在C ++中不一定如此。关键是它们都评估x一次,将变量加一,并且得到的结果就是评估后变量的内容。
Donal Fellows

3
@Donal研究员:优先级不同,所以++x + 3与相同x += 1 + 3。用括号括起来x += 1,它是相同的。就其本身而言,它们是相同的。
David Thornley,2012年

1
@DavidThornley:当它们都具有未定义的行为时,很难说“它们是相同的” :)
轨道上的轻度竞赛

1
没有一个表达式++x + 3x += 1 + 3或者(x += 1) + 3具有未定义的行为(假定结果值“适合”)。
John Hascall

42

为了使@Pubby的观点更清楚一点,请考虑 someObj.foo.bar.func(x, y, z).baz += 5

没有+=操作员,有两种方法:

  1. someObj.foo.bar.func(x, y, z).baz = someObj.foo.bar.func(x, y, z).baz + 5。这不仅冗长而且冗长,而且速度较慢。因此,人们将不得不
  2. 使用一个临时变量:tmp := someObj.foo.bar.func(x, y, z); tmp.baz = tmp.bar + 5。可以,但是简单的事情会带来很多噪音。这实际上与运行时发生的情况非常接近,但是编写起来很繁琐,仅使用它就+=会将工作转移到编译器/解释器。

+=和其他此类运营商的优势是不可否认的,而习惯它们只是时间问题。


一旦深入到对象链的3个级别,就可以停止关心小的优化(如1)和嘈杂的代码(如2)了。相反,请多考虑一下您的设计。
Dorus

4
@Dorus:我选择的表达式只是“复杂表达式”的任意代表。随意用您的脑袋里的东西代替它,您不会吵架;)
back2dos 2012年

9
+1:这是进行此优化的主要原因- 不管左侧表达式有多复杂,它总是正确的。
S.Lott 2012年

9
将错字放在其中可以很好地说明可能发生的情况。
David Thornley,2012年

21

的确,它更短,更容易,并且它确实是受底层汇编语言启发的,但它的最佳实践是,它可以防止整个类的错误,并且可以更轻松地检查代码并确保它能做什么。

RidiculouslyComplexName += 1;

由于只涉及一个变量名,因此您可以确定该语句的作用。

RidiculouslyComplexName = RidiculosulyComplexName + 1;

始终怀疑双方是完全一样的。您看到错误了吗?当存在下标和限定词时,情况甚至更糟。


5
但是,在赋值语句可以隐式创建变量的语言中,情况变得更糟,尤其是在语言区分大小写的情况下。
supercat

14

尽管+ =表示法是惯用语并且更短,但这不是为什么它更易于阅读的原因。读取代码最重要的部分是将语法映射到含义,因此语法与程序员的思维过程越接近,可读性就越高(这也是样板代码不好的原因:这不是思想的一部分程序,但仍需使代码起作用)。在这种情况下,思路是“将变量x乘以5”,而不是“让x为x加5的值”。

在其他情况下,较短的符号不利于可读性,例如,当您使用三元运算符时,if更适合使用语句。


11

有关为什么这些运算符以“ C风格”语言开头的一些见解,摘录自34年前的K&R 1st Edition(1978):

除了简洁之外,赋值运算符还具有与人们的思维方式更好地相对应的优势。我们说“将i加2”或“将i加2”,而不是“将i加2,然后将结果放回i”。这样i += 2。另外,对于像

yyval[yypv[p3+p4] + yypv[p1+p2]] += 2

赋值运算符使代码更易于理解,因为读者不必费力地检查两个长表达式的确相同,或者想知道为什么它们不是这样。赋值运算符甚至可以帮助编译器生成更有效的代码。

我认为这段话很明显,Brian KernighanDennis Ritchie(K&R)相信复合赋值运算符有助于提高代码的可读性。

自K&R撰写此书以来已有很长时间了,此后,关于人们应该如何编写代码的许多“最佳实践”已经改变或发展。但是这个程序员问题是我第一次回想起有人对复合赋值的可读性提出抱怨,所以我想知道是否有很多程序员认为它们是一个问题?再说一次,当我键入此命令时,该问题有95个否决票,因此也许人们在阅读代码时确实会感到不快。


9

除了可读性之外,它们实际上还做不同的事情:+=不必两次评估其左操作数。

例如,expr = expr + 5expr两次评估(假设expr是不纯的)。


对于除最奇怪的编译器以外的所有编译器,都没有关系。大多数编译器都很聪明,足以为expr = expr + 5expr += 5
vsz

7
@vsz如果expr没有副作用。
Pubby 2012年

6
@vsz:在C中,如果expr有副作用,则expr = expr + 5 必须两次调用这些副作用。
基思·汤普森

@Pubby:如果有副作用,那么“便利性”和“可读性”就没有问题了,这是原始问题的重点。
vsz 2012年

2
最好小心那些“副作用”陈述。读取和写入volatile的副作用,x=x+5x+=5当具有相同的副作用xvolatile
MSalters

6

除了其他人很好地描述的优点以外,当您的名字很长时,它也更紧凑。

  MyVeryVeryVeryVeryVeryLongName += 1;

要么

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;

6

简明扼要。

它的键入要短得多。它涉及较少的运算符。它具有较小的表面积和较少的混淆机会。

它使用一个更具体的运算符。

这是一个人为的示例,我不确定实际的编译器是否实现了这一点。x + = y实际上使用一个参数和一个运算符并在适当位置修改x。x = x + y可以具有x = z的中间表示形式,其中z是x + y。后者使用两个运算符(加法和赋值)以及一个临时变量。单个运算符非常清楚地表明,值方不能是y以外的任何值,并且不需要解释。从理论上讲,可能会有一些花哨的CPU,其正等号运算符的运行速度比串联的正号运算符和赋值运算符要快。


2
从理论上讲,理论上讲。ADD许多CPU上的指令都有一些变体,可以使用其他寄存器,内存或常量作为第二个加数直接在寄存器或内存上进行操作。并非所有组合都可用(例如,将内存添加到内存),但是有足够的用处。无论如何,有一个体面的优化的编译器会知道生成相同的代码x = x + y,因为它会为x += y
Blrfl 2012年

因此“人为”。
马克·坎拉斯2012年

5

这是一个很好的成语。是否更快取决于语言。在C语言中,它更快,因为它可以转换为在右侧增加变量的指令。包括Python,Ruby,C,C ++和Java在内的现代语言均支持op =语法。它体积小巧,您很快就会习惯。由于您会在其他人的代码(OPC)中看到很多内容,因此您不妨习惯并使用它。这是其他几种语言中发生的事情。

在Python中,键入x += 5仍会导致创建整数对象1(尽管它可能是从池中绘制的),并且会孤立包含5的整数对象。

在Java中,它会导致默认转换。尝试输入

int x = 4;
x = x + 5.2  // This causes a compiler error
x += 5.2     // This is not an error; an implicit cast is done.

现代命令式语言。在(纯)功能性语言x+=5中,意义不如x=x+5;例如在Haskell后者也不会引起x由5递增-代替它即被一个无限递归循环。谁想要这样的简写?
大约

对于现代编译器而言,它并不一定要快得多:即使在C
语言中

的速度与+=语言无关,而与处理器有关。例如,X86是两个地址的体系结构,它仅+=本地支持。之所以a = b + c;必须这样编译一条语句,是a = b; a += c;因为没有指令可以将加法的结果放在除求和项之一之外的其他任何地方。相比之下,Power体系结构是三地址体系结构,没有用于的特殊命令+=。在此体系结构上,语句a += b;a = a + b;始终编​​译为相同的代码。
cmaster

4

+=当您将变量用作累加器(即运行总计)时,诸如此类的运算符非常有用:

x += 2;
x += 5;
x -= 3;

比以下内容容易阅读:

x = x + 2;
x = x + 5;
x = x - 3;

在第一种情况下,从概念上讲,您正在修改中的值x。在第二种情况下,您要计算一个新值并将其分配给x每次。尽管您可能永远也不会写出那么简单的代码,但是想法还是一样的……重点是您对现有值所做的工作,而不是创建一些新的值。


对我而言,这恰恰相反-第一种形式就像屏幕上的污垢,第二种则干净且可读。
Mikhail V

1
@MikhailV为不同的人提供了不同的笔触,但是,如果您不熟悉编程,则可能会在一段时间后更改视图。
卡雷布(Caleb)2016年

我对编程不是那么陌生,我只是认为您很长时间以来就已经习惯了+ =表示法,因此您可以阅读它。最初它并没有使它具有任何美观的语法,所以客观地,用空格包围的+运算符比+ =和几乎没有区别的朋友更加清晰。并不是说您的答案是错误的,而是您不应过分依赖自己的习惯,使消费变得“容易阅读”。
Mikhail V

我的感觉是,更多的是在累积和存储新值之间的概念差异,而不是使表达式更紧凑。大多数命令式编程语言都具有类似的运算符,因此我似乎并不是唯一一个认为它有用的人。但是就像我说的,不同的人有不同的中风。如果您不喜欢它,请不要使用它。如果您想继续讨论,也许“ 软件工程聊天”会更合适。
Caleb

1

考虑一下

(some_object[index])->some_other_object[more] += 5

你真的要写D0

(some_object[index])->some_other_object[more] = (some_object[index])->some_other_object[more] + 5

1

其他答案针对更常见的情况,但是还有另一个原因:在某些编程语言中,它可能会超载。例如Scala


小Scala课程:

var j = 5 #Creates a variable
j += 4    #Compiles

val i = 5 #Creates a constant
i += 4    #Doesn’t compile

如果一个类仅定义+运算符,x+=y则确实是的快捷方式x=x+y

+=但是,如果类重载,则它们不是:

var a = ""
a += "This works. a now points to a new String."

val b = ""
b += "This doesn’t compile, as b cannot be reassigned."

val c = StringBuffer() #implements +=
c += "This works, as StringBuffer implements “+=(c: String)”."

另外,运算符++=是两个单独的运算符(不仅是:+ a,++ a,a ++,a + ba + = b也是不同的运算符);在可能出现运算符重载的语言中,这可能会产生有趣的情况。如上所述,如果+要使运算符重载以执行加法运算,请记住+=也必须重载。


“如果一个类仅定义+运算符,则x + = y确实是x = x + y的快捷方式。” 但是可以肯定的是,它反过来也起作用吗?在C ++中,通常先定义+=然后定义a+b为“制作副本ab使用粘贴+=,然后返回a”。
leftaboutabout

1

说一次,只说一次:在中x = x + 1,我说两次“ x”。

但是永远不要写“ a = b + = 1”,否则我们将不得不杀死10只小猫,27只小鼠,一只狗和一只仓鼠。


永远不要更改变量的值,因为它可以更容易地证明代码正确—请参见函数式编程。但是,如果这样做,最好不要只说一次。


“从不更改变量的值” 功能范式
彼得·莫滕森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.