x + = yのようなショートカットが良い習慣と見なされるのはなぜですか?


305

これらが実際に何と呼ばれているのか分かりませんが、私は常にそれらを見ています。Pythonの実装は次のようなものです。

x += 5の略記法としてx = x + 5

しかし、なぜこれが良い習慣と見なされるのでしょうか?Python、C、Rなどについて読んだほぼすべての書籍やプログラミングチュートリアルで、この問題に出くわしました。スペースを含む3つのキーストロークを節約できるので便利です。しかし、私がコードを読んでいるとき、彼らはいつも私をつまずかせるように見えます、そして少なくとも私の心には、それをそれ以上ではなく、読みにくくします。

これらが至る所で使用されている明確で明白な理由がありませんか?


@EricLippert:C#は、これを一番上の回答と同じ方法で処理しますか?実際に言うx += 5よりもCLRの方が効率的x = x + 5ですか?それとも、あなたが示唆するように、それは本当に単なる構文糖ですか?
-blesh

24
@blesh:ソースコードで追加を表現する方法の小さな詳細が、結果の実行可能コードの効率に影響を与えるのは、1970年の場合かもしれません。確かに今ではありません。コンパイラーの最適化は優れており、あちこちでナノ秒よりも大きな心配があります。+ =演算子が「20年前」に開発されたという考えは明らかに間違っています。故デニス・リッチーは1969年から1973年までベル研究所でCを開発しました。
エリックリッパー

1
blogs.msdn.com/b/ericlippert/archive/2011/03/29/…を参照してください(パート2も参照)
SLaks

5
ほとんどの機能的なプログラマは、この悪い習慣を考慮するでしょう。
ピートカーカム

回答:


598

速記ではありません。

+=シンボルは、1970年代にC言語で登場し、 - 「スマートアセンブラ」のCアイデアとは明らかに異なるマシン命令とadressingモードに対応しています。

i=i+1」、「」、"i+=1「」のようなもの++iは、抽象的なレベルでは同じ効果を生み出しますが、低レベルではプロセッサーの異なる動作方法に対応します。

特にこれら3つの式は、i変数がCPUレジスタに保存されているメモリアドレスにあると仮定し(名前を付けましょうD-「intへのポインタ」と考えてください)、プロセッサのALUはパラメータを受け取り、結果を「アキュムレータ」(Aと呼びましょう-intと考えてください)。

これらの制約により(その期間のすべてのマイクロプロセッサで非常に一般的)、翻訳はほとんどの場合

;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, BまたはADD A, (D+x))の代わりに変数を使用して操作する場合、またはより複雑な式を変換する場合(スタック内のプッシュ低優先度操作ですべてが沸騰し、高優先度を呼び出し、ポップする場合)すべての引数が削除されるまで繰り返します)。

2番目は「ステートマシン」のより典型的なものです。「式を評価する」のではなく、「値を操作する」のです。これらの種類の命令は、より複雑な表現が必要な場所では使用i = 3*i + i-2できませんi

3番目の(さらに単純な)手法では、「加算」という考えさえ考慮されていませんが、カウンターに対してより「プリミティブ」な(計算的な意味での)回路を使用しています。レジスタを後付けしてカウンタにするために必要な組み合わせネットワークは小さく、したがって全加算器のものよりも高速であるため、命令は短絡され、ロードが速く、すぐに実行されます。

コンパイラーの最適化を可能にする最新のコンパイラー(現在はCを参照)では、利便性に基づいて対応関係を入れ替えることができますが、セマンティクスには概念的な違いがあります。

x += 5 手段

  • xで識別される場所を見つける
  • 5を追加します

しかし、x = x + 5意味:

  • x + 5を評価する
    • xで識別される場所を見つける
    • xをアキュムレーターにコピーします
    • アキュムレーターに5を追加します
  • 結果をxに保存します
    • xで識別される場所を見つける
    • アキュムレーターをそれにコピーします

もちろん、最適化は

  • 「xの検索」に副作用がない場合、2つの「検索」を1回実行できます(xはポインタレジスタに格納されたアドレスになります)
  • ADDが&xアキュムレータの代わりに適用される場合、2つのコピーは省略できます。

したがって、最適化されたコードが一致するようにしx += 5ます。

ただし、これは「xを検出」に副作用がない場合にのみ可能です。

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

そして

*(x()) += 5;

以来、意味的に異なっているx()(了解が副作用x()の周りに奇妙なことをやって返す関数であるint*)を2回または一回生成されます。

間の等価性x = x + yとは、x += y原因特定の場合には、従ってある+==直接L値に適用されます。

Pythonに移行するために、Cから構文を継承しましたが、インタープリター言語での実行の前に翻訳/最適化がないため、物事は必ずしもそれほど密接に関連しているわけではありません(解析ステップが1つ少ないため)。ただし、インタープリターは3種類の式の異なる実行ルーチンを参照でき、式の形成方法と評価コンテキストに応じて異なるマシンコードを利用できます。


詳細が好きな人のために...

すべてのCPUにはALU(算術論理ユニット)があります。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 += 5xで直接動作します。

実際の実装は、プロセッサの実際の命令セットに依存します。ADD (.) cオペコードがない場合、最初のコードは2番目になります。

そのようなオペコードがあり、最適化が有効になっている場合、2番目の式は、逆方向の移動を排除してレジスタオペコードを調整した後、最初の式になります。


90
+1は、昔は別の(より効率的な)マシンコードにマップしていたと説明している唯一の答えです。
ペテルトレック

11
@KeithThompsonそれは本当ですが、アセンブリがC言語(およびその後すべてのCスタイル言語)の設計に大きな影響を与えたことを否定することはできません
MattDavey

51
Erm、「+ =」は「inc」にマッピングされません(「add」にマッピングされます)、「++」は「inc」にマッピングされます。
ブレンダン

11
「20年前」とは、「30年前」を意味すると思います。ところで、COBOLはCにさらに20年の
差を

10
理論的には素晴らしい。事実間違っています。x86 ASM INCは1のみを追加するため、ここで説明した「追加と割り当て」演算子には影響しません(ただし、これは「++」と「-」に対する優れた答えです)。
マークブラケット

281

それについてどう考えるかにもよりますが、実際はわかりやすいので理解しやすいです。例:

x = x + 5 「xを取得し、5を追加し、その新しい値をxに戻す」という精神処理を呼び出します。

x += 5 「xを5増やす」と考えることができます

したがって、それは単なる速記ではなく、実際に機能をより直接的に説明しています。多数のコードを読むと、把握しやすくなります。


32
+1、私は完全に同意します。私は子供の頃からプログラミングに夢中になりましたが、頭が簡単に順応性があり、x = x + 5それでも悩みました。後年数学を始めたとき、それはさらに私を悩ませました。を使用x += 5すると、はるかに記述的であり、式としてはるかに意味があります。
多項式

44
変数に長い名前がある場合もあります:reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1... oh noes !!! タイプミス

9
@Matt Fenwick:長い変数名である必要はありません。何らかの表現である可能性があります。それらは検証するのがさらに難しい可能性が高く、読者はそれらが同じであることを確認するために多くの注意を払わなければなりません。
デビッドソーンリー

32
X = X + 5は、xを5ずつ増やすことが目標の場合の一種のハックです。
JeffO

1
@Polynomial私は子供の頃にもx = x + 5の概念に出くわしたと思います。正しく覚えていれば9歳-それは私にとって完全に理にかなっており、今でも私にとって完全に理にかなっており、x + = 5よりもずっと好んでいます。 xをxの以前の値(それが何であれ)に割り当ててから5を追加するという考え。
クリスハリソン

48

少なくともPythonではx += yx = 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:++xおよびx+=1CとJava(おそらくC#)で同等ですが、C ++では必ずしもそうではありませんが、演算子のセマンティクスが複雑です。重要な点は、両方ともx一度評価され、変数が1ずつ増加し、評価後の変数の内容である結果が得られることです。
ドナルドフェローズ

3
@Donalフェロー:優先度が異なるので、++x + 3同じではありませんx += 1 + 3。を括弧で囲んでx += 1同じです。それ自体のステートメントとして、それらは同一です。
デビッドソーンリー

1
@DavidThornley:それは:)、どちらも未定義の動作を持っているとき、「彼らは同じだ」と言うのは難しい
軌道上での明度レース

1
++x + 3x += 1 + 3または(x += 1) + 3未定義の動作はありません(結果の値が「適合する」と仮定)。
ジョンハスコール

42

@Pubbyのポイントをもう少し明確にするために、 someObj.foo.bar.func(x, y, z).baz += 5

+=オペレーターがいない場合、次の2つの方法があります。

  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のようなノイズの多いコードを気にするのをやめることができます。代わりに、もう一度設計を考え直してください。
ドルス

4
@Dorus:私が選んだ表現は、「複雑な表現」のarbitrary意的な代表にすぎません。それをあなたの頭の中の何かに置き換えることをお気軽に、あなたは約約を選ぶことはありません;)
back2dos

9
+1:これがこの最適化の主な理由です。左側の式がどれほど複雑であっても、常に正しいです。
S.Lott

9
そこにタイプミスを入れると、何が起こり得るかを示しています。
デビッドソーンリー

21

短くて簡単であり、おそらく基礎となるアセンブリ言語に触発されたのは確かですが、ベストプラクティスである理由は、エラーのクラス全体を防止し、コードの確認と確認を容易にすることですそれが何をするか。

RidiculouslyComplexName += 1;

関係する変数名は1つだけであるため、ステートメントの動作は確実です。

RidiculouslyComplexName = RidiculosulyComplexName + 1;

両者がまったく同じであることは常に疑いの余地があります。バグを見ましたか?下付き文字と修飾子が存在すると、さらに悪化します。


5
特に大文字と小文字が区別される場合、代入ステートメントが暗黙的に変数を作成できる言語ではさらに悪化します。
supercat

14

+ =表記は慣用的で短くなっていますが、これらが読みやすい理由ではありません。コードを読むことの最も重要な部分は、構文を意味にマッピングすることであり、したがって、構文がプログラマーの思考プロセスに近いほど、読みやすくなります(これは定型コードが悪い理由でもあります:それは思考の一部ではありませんプロセスですが、コードを機能させるために必要です)。この場合、思考は「xに5を加えた値」ではなく、「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

割り当て演算子はコードを理解しやすくします。なぜなら、2つの長い式が実際に同じであることを読者が苦労してチェックする必要がないためです。また、代入演算子は、コンパイラがより効率的なコードを生成するのに役立つ場合もあります。

この文章から、ブライアンカーニハンデニスリッチー(K&R)は、複合代入演算子がコードの読みやすさを助けたと信じていることは明らかだと思います。

K&Rがそれを書いてから長い時間が経ち、人々がどのようにコードを書くべきかについての多くの「ベストプラクティス」は、それ以来変化または進化しています。しかし、このprogrammers.stackexchangeの質問は、複合割り当ての読みやすさについて苦情を言っている人を思い出すことができるのは初めてですので、多くのプログラマが問題を見つけているのでしょうか?繰り返しますが、私がこれを入力すると、質問には95の賛成票があります。


9

可読性に加えて、実際には異なること+=を行います。左のオペランドを2回評価する必要はありません。

たとえば、2回expr = expr + 5評価exprします(expr不純であると仮定)。


最も奇妙なコンパイラを除いて、それは問題ではありません。ほとんどのコンパイラは同じのためのバイナリを生成するのに十分なスマートされているexpr = expr + 5expr += 5
VSZ

7
@vsz副作用がない場合expr
パブ

6
@vsz:Cでは、expr副作用がある場合、それらの副作用を2回呼び出すexpr = expr + 5 必要があります。
キーストンプソン

@Pubby:副作用がある場合、元の質問のポイントであった「利便性」と「読みやすさ」に関する問題はありません。
-vsz

2
これらの「副作用」ステートメントに注意してください。読み込みと書き込みをするためにvolatile、副作用であり、x=x+5そしてx+=5ときに、同じ副作用がxあるvolatile
MSalters

6

他の人が非常によく説明した明らかなメリットに加えて、非常に長い名前を持っている場合は、よりコンパクトです。

  MyVeryVeryVeryVeryVeryLongName += 1;

または

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;

6

簡潔です。

入力する方がずっと短いです。オペレーターの数が少なくなります。表面積が小さく、混乱の機会が少ない。

より具体的な演算子を使用します。

これは不自然な例であり、実際のコンパイラがこれを実装しているかどうかはわかりません。x + = yは実際には1つの引数と1つの演算子を使用し、xをその場で変更します。x = x + yは、x = zの中間表現を持つことができます。ここで、zはx + yです。後者は、2つの演算子、加算と割り当て、および一時変数を使用します。単一の演算子により、値側がy以外になり得ず、解釈する必要がないことが非常に明確になります。理論的には、プラス演算子と代入演算子よりも高速で実行されるプラス等価演算子を備えた派手なCPUが存在する可能性があります。


2
理論的にはSchmeoretically。ADD多くのCPU の命令には、2番目の加数として他のレジスタ、メモリ、または定数を使用して、レジスタまたはメモリで直接動作するバリアントがあります。すべての組み合わせが利用できるわけではありません(たとえば、メモリにメモリを追加する)が、役に立つほど十分です。いずれにしても、まともなオプティマイザを持つ任意のコンパイラがために同じコードを生成するために知っているだろうx = x + y、それがために同じようにx += y
Blrfl

したがって、「工夫された」。
マークカンラス

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では後者は5ずつ増加することありませx -代わりに無限の再帰ループが発生します。誰がその速記を望みますか?
左辺約

これは、最近のコンパイラでは全く必ずしも速くはありません。でも、Cで
HörmannHH

の速度+=は、プロセッサに依存しているため、言語にそれほど依存していません。たとえば、X86は2アドレスアーキテクチャであり、+=ネイティブでのみサポートされます。のようなステートメントは、加数のいずれかの場所以外に加算の結果を置くことができる命令がないため、a = b + c;コンパイルする必要がありますa = b; a += c;。これとは対照的に、Powerアーキテクチャは、3つのアドレスアーキテクチャであり、特別なコマンドはありません+=。このアーキテクチャでは、文a += b;a = a + b;常に同じコードにコンパイルします。
cmaster

4

などの演算子+=は、変数をアキュムレータとして使用している場合、つまり積算合計として非常に便利です。

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

以下よりも読みやすいです:

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

最初のケースでは、概念的に、の値を変更していますx。2番目のケースでは、新しい値を計算し、x毎回それを割り当てています。おそらくそれほど単純なコードを書くことはないでしょうが、考え方は同じままです。新しい値を作成するのではなく、既存の値に対して行っていることに焦点を合わせます。


私にとっては正反対です-最初のバリエーションは画面上の汚れのようなもので、2番目はきれいで読みやすいです。
ミハイルV

1
@MikhailVの人によってストロークは異なりますが、プログラミングが初めての場合は、しばらくしてから表示を変更できます。
カレブ

私はプログラミングにそれほど慣れていないので、長い間+ =表記法に慣れているだけなので、読むことができると思います。最初は見栄えの良い構文になりません。そのため、スペースで囲まれた+演算子は、+ =およびすべてがほとんど区別できない友人よりも客観的にきれいです。あなたの答えが間違っているというわけではありませんが、消費を「読みやすい」ようにする消費習慣に頼りすぎてはいけません。
ミハイルV

私は、式をよりコンパクトにすることよりも、新しい値を蓄積することと保存することの概念的な違いのほうがより多くのことを感じています。ほとんどの命令型プログラミング言語には同様の演算子があります。そのため、私がそれを便利だと思うのは私だけではないようです。しかし、私が言ったように、人によってストロークは異なります。気に入らない場合は使用しないでください。議論を続ける場合は、おそらくソフトウェアエンジニアリングチャットの方が適切でしょう。
カレブ

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

他の回答はより一般的なケースを対象としていますが、別の理由があります。一部のプログラミング言語では、オーバーロードされる可能性があります。例えばスカラ


Small 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)”."

さらに、演算子+and +=は2つの別個の演算子です(これらだけでなく、+ a、++ a、a ++、a + ba + = bも異なる演算子です)。演算子のオーバーロードが利用可能な言語では、これは興味深い状況を作り出す可能性があります。前述のように+、追加を実行するために演算子をオーバーロードする場合+=、同様にオーバーロードする必要があることに注意してください。


「クラスが+演算子のみを定義している場合、x + = yは実際にはx = x + yのショートカットです。」しかし、確かにそれは他の方法でも動作しますか?C ++では、最初に定義して+=から、a+b「のコピーを作成しab使用し+=てコピーを返す」として定義することは非常に一般的aです。
左辺約

1

一度だけ言ってください:ではx = x + 1、「x」を2回言います。

しかし、「a = b + = 1」と書かないでください。そうしないと、10匹の子猫、27匹のネズミ、犬、ハムスターを殺す必要があります。


変数の値を変更しないでください。コードが正しいことを証明しやすくなります。関数型プログラミングを参照してください。しかし、あなたがそうするなら、それは一度だけ言うことはないほうが良いです。


「変数の値を変更しないでください」機能的パラダイム
ピーターモーテンセン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.