Почему ярлыки типа x + = y считаются хорошей практикой?

305

Я понятия не имею, как они на самом деле называются, но я вижу их все время. Реализация Python выглядит примерно так:

x += 5в качестве сокращенной записи для x = x + 5.

Но почему это считается хорошей практикой? Я сталкивался с этим почти в каждой книге или учебнике по программированию, которые читал для Python, C, R и так далее, и так далее. Я понимаю, что это удобно, сохраняя три нажатия клавиш, включая пробелы. Но они, кажется, всегда сбивают меня с толку, когда я читаю код, и, по крайней мере, на мой взгляд, делают его менее читабельным, а не более.

Я пропускаю какую-то ясную и очевидную причину, по которой они используются повсеместно?

Fomite
источник
@EricLippert: C # обрабатывает это так же, как описанный верхний ответ? Является ли это на самом деле более эффективным CLR-накрест сказать , x += 5чем x = x + 5? Или это действительно просто синтаксический сахар, как вы предлагаете?
блеф
24
@blesh: эти мелкие детали того, как кто-то выражает дополнение в исходном коде, влияют на эффективность получающегося исполняемого кода, возможно, имело место в 1970 году; это, конечно, не сейчас. Оптимизация компиляторов - это хорошо, и у вас больше забот, чем наносекунды здесь или там. Идея о том, что оператор + = был разработан «двадцать лет назад», очевидно ложна; покойный Деннис Ричи разработал C с 1969 по 1973 год в Bell Labs.
Эрик Липперт
1
См. Blogs.msdn.com/b/ericlippert/archive/2011/03/29/… (см. Также часть вторая)
SLaks
5
Большинство функциональных программистов сочтут это плохой практикой.
Пит Киркхам

Ответы:

598

Это не стенография.

+=Символ появился на языке C в 1970 - х годах, а также - с идеей C «умного ассемблера» соответствует четко другому режиму команд машины и адресации:

Такие вещи, как " i=i+1", "i+=1"и" ++i", хотя и на абстрактном уровне производят одинаковый эффект, на низком уровне соответствуют другому способу работы процессора.

В частности, эти три выражения, предполагая, что iпеременная находится в адресе памяти, хранящемся в регистре процессора (назовем его 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)) или при переводе более сложных выражений (все они сводятся к операции push с низким приоритетом в стеке, вызывают высокий приоритет, pop и повторять, пока все аргументы не будут устранены).

Второй тип более типичен для «конечного автомата»: мы больше не «вычисляем выражение», а «работаем со значением»: мы все еще используем ALU, но избегаем перемещения значений, поскольку в результате можно заменить параметр. Этот вид инструкций не может использоваться, когда требуется более сложное выражение: i = 3*i + i-2его нельзя использовать на месте, поскольку iтребуется больше раз.

Третий, даже более простой, даже не рассматривает идею «сложения», но использует более «примитивную» (в вычислительном смысле) схему для счетчика. Инструкция закорочена, загружается быстрее и выполняется немедленно, поскольку комбинаторная сеть, необходимая для модификации регистра, чтобы сделать его счетчиком, меньше и, следовательно, быстрее, чем у полного сумматора.

С современными компиляторами (см. C, к настоящему времени), позволяющими оптимизировать компилятор, соответствие может быть изменено в зависимости от удобства, но в семантике все еще есть концептуальное различие.

x += 5 означает

  • Найдите место, обозначенное х
  • Добавьте 5 к этому

Но x = x + 5значит:

  • Оценить х + 5
    • Найдите место, обозначенное х
    • Скопируйте х в аккумулятор
    • Добавьте 5 к аккумулятору
  • Сохранить результат в х
    • Найдите место, обозначенное х
    • Скопируйте аккумулятор к нему

Конечно, оптимизация может

  • если «поиск x» не имеет побочных эффектов, два «поиска» можно выполнить один раз (и x станет адресом, хранящимся в регистре указателей)
  • две копии могут быть опущены, если &xвместо аккумулятора применяется аккумулятор

таким образом, оптимизированный код должен совпадать с x += 5одним.

Но это можно сделать только в том случае, если «поиск х» не имеет побочных эффектов, в противном случае

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

а также

*(x()) += 5;

семантически различаются, поскольку x()побочные эффекты (признание x()- это функция, выполняющая странные вещи и возвращающая int*), будут создаваться дважды или один раз.

Следовательно, эквивалентность между x = x + yи x += yобусловлена ​​частным случаем, когда +=и =применяются к прямому l-значению.

Чтобы перейти на Python, он унаследовал синтаксис от C, но так как нет перевода / оптимизации ДО выполнения на интерпретируемых языках, вещи не обязательно так тесно связаны (так как есть еще один шаг разбора). Однако интерпретатор может ссылаться на разные подпрограммы выполнения для трех типов выражений, используя преимущества различного машинного кода в зависимости от того, как сформировано выражение и от контекста оценки.


Для тех, кто любит более подробно ...

Каждый ЦП имеет АЛУ (арифметико-логический блок), который по своей сути является комбинаторной сетью, чьи входы и выходы «подключены» к регистрам и / или памяти в зависимости от кода операции инструкции.

Двоичные операции обычно реализуются как «модификатор регистра накопителя с вводом, взятым« где-то », где где-то может быть - внутри самого потока команд (типично для константы манифеста: 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работает непосредственно на х.

Фактическая реализация зависит от реального набора команд процессора: если нет ADD (.) cкода операции, первый код становится вторым: нет пути.

Если есть такой код операции, и оптимизация включена, второе выражение, после устранения обратных движений и корректировки регистра кода операции, становится первым.

Эмилио Гаравалья
источник
90
+1 за единственный ответ, объясняющий, что он использовался для сопоставления с другим (и более эффективным) машинным кодом в былые времена.
Петер Тёрёк
11
@KeithThompson Это правда, но нельзя отрицать, что сборка оказала огромное влияние на дизайн языка C (и впоследствии всех языков стиля C)
MattDavey
51
Erm, "+ =" не сопоставляется с "inc" (это сопоставляется с "add"), "++" сопоставляется с "inc".
Брендан
11
Под «20 лет назад» я думаю, что вы имеете в виду «30 лет назад». И, кстати, COBOL побил C еще на 20 лет: ADD 5 TO X.
JoelFan
10
Великий в теории; неправильно в фактах. В x86 ASM INC добавляется только 1, поэтому он не влияет на обсуждаемый здесь оператор «добавить и назначить» (хотя это было бы отличным ответом для «++» и «-»).
Марк Брэкетт
281

В зависимости от того, как вы думаете об этом, на самом деле это легче понять, потому что это более просто. Взять, к примеру:

x = x + 5 вызывает умственную обработку «возьмите х, добавьте к нему пять, а затем присвойте это новое значение обратно х»

x += 5 можно рассматривать как «увеличение х на 5»

Таким образом, это не просто стенография, это фактически описывает функциональность более прямо. Когда вы читаете куски кода, это гораздо легче понять.

Эрик Кинг
источник
32
+1, я полностью согласен. Я начал заниматься программированием в детстве, когда мой разум был легко податлив и x = x + 5все еще беспокоил меня. Когда я попал в математику в более позднем возрасте, это беспокоило меня еще больше. Использование x += 5значительно более наглядно и имеет гораздо больше смысла в качестве выражения.
Полином
44
Также есть случай, когда переменная имеет длинное имя: reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1... о, нет !!! опечатка
9
@Matt Fenwick: это не должно быть длинным именем переменной; это может быть какое-то выражение. Это, вероятно, будет еще сложнее проверить, и читатель должен уделить много внимания, чтобы убедиться, что они одинаковы.
Дэвид Торнли
32
X = X + 5 - это что-то вроде хака, когда ваша цель
увеличить
1
@Polynomial Я думаю, что я столкнулся с концепцией х = х + 5 в детстве. 9 лет, если я правильно помню - и это имело смысл для меня, и оно все еще имеет смысл для меня сейчас, и я очень предпочитаю его х + = 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 , так как вы часто сталкиваетесь с несколькими ссылками на разные части массива, и важно убедиться, что вы случайно не изменили часть массива, на которую есть другие ссылки, или без необходимости копировать массивы (которые могут быть очень дорогими).

James
источник
4
+1 для __iadd__: есть языки , где вы можете создать неизменяемую ссылку на изменяемую структуру данные, в которой operator +=определяются, например , лестница: val sb = StringBuffer(); lb += "mutable structure"против var s = ""; s += "mutable variable". дальнейшее изменение содержимого структуры данных, в то время как последняя указывает переменную на новую.
летающая овца
43

Это называется идиома . Идиомы программирования полезны, потому что они представляют собой последовательный способ написания конкретной конструкции программирования.

Всякий раз, когда кто-то пишет, x += yвы знаете, что xэто увеличение, yа не какая-то более сложная операция (как лучший метод, как правило, я бы не смешивал более сложные операции и эти сокращения синтаксиса). Это имеет смысл при увеличении на 1.

Джо
источник
13
x ++ и ++ x немного отличаются от x + = 1 и друг от друга.
Гэри Уиллоуби
1
@Gary: ++xи x+=1эквивалентны в C и Java (возможно, также в C #), но не обязательно в C ++ из-за сложной семантики операторов. Ключевым моментом является то, что они оба оценивают xодин раз, увеличивают переменную на единицу и имеют результат, который является содержимым переменной после оценки.
Donal Fellows
3
@Donal Fellows: приоритет отличается, поэтому ++x + 3не то же самое, что x += 1 + 3. Сделайте скобки x += 1и они идентичны. Как заявления сами по себе, они идентичны.
Дэвид Торнли
1
@DavidThornley: Трудно сказать «они идентичны», когда у них обоих неопределенное поведение :)
Гонки Легкости на орбите
1
Ни одно из выражений ++x + 3, x += 1 + 3либо (x += 1) + 3имеет неопределенное поведение (при условии, что полученное значение «подходит»).
Джон Хэсколл
42

Чтобы прояснить точку зрения Пабби, рассмотрим 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. Это нормально, но это много шума для простой вещи. Это на самом деле очень близко к тому, что происходит во время выполнения, но писать это утомительно, и простое использование +=переключит работу на компилятор / интерпретатор.

Преимущество +=и других подобных операторов неоспоримо, а привыкнуть к ним - только вопрос времени.

back2dos
источник
Когда вы пройдете 3 уровня в глубине цепочки объектов, вы можете перестать заботиться о небольших оптимизациях, таких как 1, и шумном коде, таком как 2. Вместо этого еще раз подумайте над своим дизайном.
Дорус
4
@Dorus: выбранное мной выражение является просто произвольным представителем "сложного выражения". Не стесняйтесь заменить это чем-то в своей голове, о чем вы не будете придираться;)
back2dos
9
+1: это основная причина этой оптимизации - она всегда верна, независимо от сложности левого выражения.
S.Lott
9
Помещение опечатки - хорошая иллюстрация того, что может произойти.
Дэвид Торнли
21

Это правда, что он короче и проще, и правда, что он, вероятно, был вдохновлен базовым языком ассемблера, но причина, по которой он лучше всего подходит, заключается в том, что он предотвращает целый класс ошибок и облегчает просмотр кода и уверенность что оно делает.

С участием

RidiculouslyComplexName += 1;

Поскольку задействовано только одно имя переменной, вы уверены, что делает оператор.

С участием RidiculouslyComplexName = RidiculosulyComplexName + 1;

Всегда есть сомнения, что обе стороны абсолютно одинаковы. Вы видели ошибку? Еще хуже, когда присутствуют подписчики и квалификаторы.

Джейми Кокс
источник
5
Это еще хуже в языках, где операторы присваивания могут неявно создавать переменные, особенно если языки чувствительны к регистру.
суперкат
14

Хотя обозначение + = идиоматично и короче, это не причины, по которым его легче читать. Наиболее важной частью чтения кода является сопоставление синтаксиса со значением, и поэтому, чем ближе синтаксис соответствует мыслительным процессам программиста, тем более читабельным он будет (это также причина, по которой шаблонный код плох: он не является частью мысли процесс, но все же необходимо сделать код функции). В этом случае мысль «увеличить переменную х на 5», а не «пусть х будет значением х плюс 5».

Есть и другие случаи, когда более короткая запись плоха для читабельности, например, когда вы используете троичный оператор, где ifоператор был бы более уместным.

tdammers
источник
11

Чтобы понять, почему эти операторы используют языки «стиля С», вот выдержка из K & R 1st Edition (1978), 34 года назад:

Помимо краткости, операторы присваивания имеют то преимущество, что они лучше соответствуют тому, как люди думают. Мы говорим «добавить 2 к i» или «увеличить i на 2», а не «взять i, добавить 2, а затем вернуть результат обратно в i». Таким образом i += 2. Кроме того, для сложного выражения, как

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

Оператор присваивания облегчает понимание кода, так как читателю не нужно тщательно проверять, что два длинных выражения действительно совпадают, или удивляться, почему это не так. А оператор присваивания может даже помочь компилятору создавать более эффективный код.

Я думаю, из этого отрывка ясно, что Брайан Керниган и Деннис Ритчи (K & R) полагали, что составные операторы присваивания помогли с читабельностью кода.

С тех пор, как K & R написал это, прошло много времени, и с тех пор многие «лучшие практики» о том, как люди должны писать код, изменились или эволюционировали. Но этот вопрос programmers.stackexchange - это первый случай, когда я вспоминаю, как кто-то высказывал жалобу на удобочитаемость составных назначений, поэтому мне интересно, считают ли многие программисты их проблемой? Опять же, когда я набираю это, у вопроса 95 голосов, поэтому, возможно, люди находят их раздражающими при чтении кода.

Michael Burr
источник
9

Помимо читабельности, они на самом деле делают разные вещи: +=не нужно оценивать его левый операнд дважды.

Например, expr = expr + 5будет оцениваться exprдважды (при условии, что exprэто нечисто).

Pubby
источник
Для всех, кроме самых странных компиляторов, это не имеет значения. Большинство компиляторов достаточно умны, чтобы генерировать один и тот же двоичный файл для expr = expr + 5иexpr += 5
vsz
7
@vsz Нет, если exprесть побочные эффекты.
Пабби
6
@vsz: В C, если exprесть побочные эффекты, тогда expr = expr + 5 нужно вызывать эти побочные эффекты дважды.
Кит Томпсон
@Pubby: если у него есть побочные эффекты, нет никаких проблем с «удобством» и «удобочитаемостью», что и было темой исходного вопроса.
вс
2
Лучше быть осторожным с этими заявлениями "побочных эффектов". Чтение и запись volatileявляются побочными эффектами x=x+5и x+=5имеют те же побочные эффекты, когда xестьvolatile
MSalters
6

Помимо очевидных достоинств, которые другие люди описали очень хорошо, когда у вас очень длинные имена, он более компактен.

  MyVeryVeryVeryVeryVeryLongName += 1;

или же

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;
Андрей Рубштейн
источник
6

Это сжато.

Это намного короче, чтобы напечатать. В нем задействовано меньше операторов. Он имеет меньшую площадь поверхности и меньше возможностей для путаницы.

Он использует более конкретный оператор.

Это надуманный пример, и я не уверен, что фактические компиляторы реализуют это. x + = y фактически использует один аргумент и один оператор и модифицирует x на месте. x = x + y может иметь промежуточное представление x = z, где z - x + y. Последний использует два оператора, сложение и присваивание, и временную переменную. Один оператор делает супер ясным, что сторона значения не может быть ничем иным, кроме y, и не нуждается в интерпретации. И теоретически может быть какой-нибудь причудливый процессор с оператором плюс-равно, который работает быстрее, чем оператор плюс и оператор назначения последовательно.

Mark Canlas
источник
2
Теоретически шмеоретически. В ADDинструкции на многих процессорах есть варианты , которые работают непосредственно на регистрах или памяти с использованием других регистров, памяти или констант в качестве второго слагаемого. Не все комбинации доступны (например, добавить память в память), но их достаточно, чтобы быть полезными. В любом случае, любой компилятор с приличным оптимизатором будет знать, что нужно генерировать тот же код, 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.
ncmathsadist
источник
Современные императивные языки. В (чистых) функциональных языках x+=5так же мало смысла, как x=x+5; например, в Haskell последнее не приводит xк увеличению на 5 - вместо этого он создает бесконечный цикл рекурсии. Кто бы хотел для этого стенографию?
оставил около
С современными компиляторами совсем не обязательно быстрее: даже в C.
HörmannHH
Скорость +=не столько зависит от языка, сколько зависит от процессора . Например, X86 - это двухадресная архитектура, она поддерживается только +=изначально. Оператор like a = b + c;должен быть скомпилирован так, a = b; a += c;потому что просто нет инструкции, которая могла бы поместить результат сложения где-либо еще, кроме как на место одного из слагаемых. Архитектура Power, напротив, представляет собой трехадресную архитектуру, для которой нет специальных команд +=. На этой архитектуре операторы a += b;и a = a + b;всегда компилируются в один и тот же код.
cмейстер
4

Такие операторы, как +=очень полезны, когда вы используете переменную в качестве аккумулятора , то есть промежуточного итога:

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

Гораздо легче читать, чем:

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

В первом случае, концептуально, вы изменяете значение в x. Во втором случае вы вычисляете новое значение и присваиваете его xкаждому разу. И хотя вы, вероятно, никогда бы не написали такой простой код, идея остается прежней ... основное внимание уделяется тому, что вы делаете с существующим значением, а не создаете какое-то новое значение.

Калеб
источник
Для меня это как раз наоборот - первый вариант похож на грязь на экране, а второй - чистый и читаемый.
Михаил V
1
@MikhailV, разные удары для разных людей, но если вы новичок в программировании, вы можете через некоторое время изменить свое мнение.
Калеб
Я не настолько новичок в программировании, я просто думаю, что вы просто привыкли к обозначению + = в течение длительного времени, и поэтому вы можете прочитать его. Что изначально не делает его привлекательным синтаксисом, так что оператор +, окруженный пробелами, объективно чище, чем + = и друзья, которые едва различимы. Не то, чтобы ваш ответ был неправильным, но не следует слишком полагаться на свою привычку, делающую выводы о том, насколько «легко читать».
Михаил V
Мне кажется, что это скорее концептуальная разница между накоплением и хранением новых ценностей, чем создание более компактного выражения. Большинство императивных языков программирования имеют схожие операторы, поэтому я, похоже, не единственный, кто находит это полезным. Но, как я уже сказал, разные инсульты для разных людей. Не используйте его, если он вам не нравится. Если вы хотите продолжить обсуждение, возможно , будет более уместным использование Software Engineering Chat .
Калеб
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:

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как «сделать копию a, надеть bее +=, вернуть копию a».
оставил около
1

Скажите это один раз и только один раз: x = x + 1я говорю «х» дважды.

Но никогда не пишите «a = b + = 1», иначе нам придется убить 10 котят, 27 мышей, собаку и хомяка.


Вы никогда не должны изменять значение переменной, так как это упрощает проверку правильности кода - см. Функциональное программирование. Однако, если вы делаете, то лучше не говорить вещи только один раз.

ричард
источник
"никогда не меняйте значение переменной" Функциональная парадигма ?
Питер Мортенсен