Чому ярлики типу x + = y вважаються хорошою практикою?


305

Я поняття не маю, як це насправді називається, але я їх весь час бачу. Реалізація Python - це щось на зразок:

x += 5як скорочення для x = x + 5.

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

Чи пропускаю я якусь чітку і очевидну причину, яку вони використовують у всьому світі?


@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)) або при перекладі складніших виразів (всі вони збиваються при натисканні операції з низьким пріоритетом у стеку, виклику високого пріоритету, поп і повторюйте, поки всі аргументи не будуть усунені).

Другий більш характерний для "машини машини": ми вже не "оцінюємо вираз", а "керуємо значенням": ми все ще використовуємо ALU, але уникаємо переміщення значень, щоб результат міг замінити параметр. Цей вид інструкцій не можна використовувати там, де потрібні складніші висловлювання: i = 3*i + i-2їх не можна використовувати на місці, оскільки iце потрібно більше разів.

Третя - навіть простіша - навіть не розглядає ідею "додавання", але використовує більш "примітивну" (в обчислювальному сенсі) схему для лічильника. Інструкція скорочується, завантажується швидше і виконується негайно, оскільки комбінаторна мережа, необхідна для переобладнання реєстру, щоб зробити його лічильником, менша, а значить, швидша, ніж повна суматор.

У сучасних компіляторах (див. С, на даний момент), що дозволяє оптимізувати компілятор, відповідність може бути замінена на основі зручності, але все ще існує концептуальна різниця в семантиці.

x += 5 засоби

  • Знайдіть місце, визначене x
  • Додайте до нього 5

Але x = x + 5означає:

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

Звичайно, оптимізація може

  • якщо "пошук x" не має побічних ефектів, два "знаходження" можна зробити один раз (і x стати адресою, що зберігається в реєстрі покажчиків)
  • дві копії можна видалити, якщо &xзамість цього до акумулятора застосовано ADD

таким чином, роблячи оптимізований код, щоб він збігався x += 5.

Але це можна зробити, лише якщо "пошук x" не має побічних ефектів, в іншому випадку

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

і

*(x()) += 5;

є семантично різними, оскільки x()побічні ефекти (визнання x()це функція, яка робить дивні речі навколо та повернення int*) будуть вироблятися двічі або один раз.

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

Щоб перейти на Python, він успадкував синтаксис від C, але оскільки немає перекладу / оптимізації перед виконанням інтерпретованих мов, речі не обов'язково так тісно пов'язані (оскільки є один крок менш синтаксичного аналізу). Однак інтерпретатор може посилатися на різні процедури виконання для трьох типів вираження, користуючись різними машинними кодами залежно від того, як формується вираз та від контексту оцінки.


Для тих, хто любить детальніше ...

Кожен процесор має ALU (арифметико-логічну одиницю), що є, по суті, комбінаторною мережею, входи та висновки якої "підключені" до регістрів та / або пам'яті залежно від опкоду інструкції.

Бінарні операції, як правило, реалізуються як "модифікатор реєстру акумулятора з входом, взятим" десь ", де десь може бути - всередині самого потоку інструкцій (типовий для контент-маніфесту: ADD A 5) - всередині іншого реєстру (типовий для обчислення вираження з temporaries: напр., 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 за єдину відповідь, що пояснює, що вона використовувала для відображення різних (і більш ефективних) машинних кодів за старих часів.
Péter Török

11
@KeithThompson Це правда, але не можна заперечувати, що збірка мала величезний вплив на дизайн мови C (а згодом і всіх мов стилю C)
MattDavey

51
Erm, "+ =" не відображається у "inc" (він відображає "add"), "++" - на "inc".
Брендан

11
Під "20 років тому", я думаю, ви маєте на увазі "30 років тому". І до речі, COBOL побив C ще 20 років, коли: ДОДАТИ 5 ДО X.
JoelFan

10
Чудово в теорії; неправильно у фактах. X86 ASM INC додає лише 1, тому він не впливає на обговорений тут оператор "додавання та призначення" (це було б чудовою відповіддю для "++" та "-", хоча).
Марк Брекетт

281

Залежно від того, як ви думаєте про це, насправді простіше зрозуміти, оскільки це простіше. Візьмемо, наприклад:

x = x + 5 викликає розумову обробку "взяти x, додати п'ять до неї, а потім призначити це нове значення назад до x"

x += 5 можна вважати "збільшення х на 5"

Отже, це не просто скорочення, це фактично набагато прямо описує функціональність. Читаючи коди, це набагато простіше зрозуміти.


32
+1, я повністю згоден. Я почав займатися програмуванням ще в дитинстві, коли мій розум був легко підданий, і x = x + 5все ще турбував мене. Коли я пізніше зайнявся математикою, це мене ще більше турбувало. Використання x += 5є значно більш описовим і має набагато більше сенсу як вираз.
Поліном

44
Існує також випадок, коли змінна має довгу назву: reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1... о, ні !!! a typo

9
@Matt Fenwick: це не повинно бути довгою назвою змінної; це може бути виразом якогось. Це, ймовірно, буде важче перевірити, і читачеві доведеться приділити багато уваги, щоб переконатися, що вони однакові.
Девід Торнлі

32
X = X + 5 - це такий собі злом, коли ваша мета - збільшити х на 5.
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 +=визначаються, наприклад , сходи: val sb = StringBuffer(); lb += "mutable structure"проти var s = ""; s += "mutable variable". далі змінюється вміст структури даних, тоді як остання робить змінну точку новою.
літаючі вівці

43

Його називають ідіомою . Програмування ідіом корисно, оскільки вони є послідовним способом написання певної конструкції програмування.

Кожен раз, коли хтось пише, x += yви знаєте, що xце збільшується, yа не якась більш складна операція (як найкраща практика, як правило, я б не змішував складніші операції та ці синтаксичні скорочення). Це має найбільш сенс при збільшенні на 1.


13
x ++ і ++ x трохи відрізняються від x + = 1 і один до одного.
Gary Willoughby

1
@Gary: ++xі x+=1еквівалентні C і Java (можливо, також C #), хоча це не обов'язково так у C ++ через складну семантику оператора. Ключовим є те, що вони обидва оцінюють xодин раз, збільшуючи змінну на одну, і отримують результат, який є змістом змінної після оцінки.
Стипендіати доналу

3
@ Дональд стипендіатів: пріоритет інший, тому ++x + 3не такий, як x += 1 + 3. Визначте, x += 1і це ідентично. Як твердження самі по собі, вони однакові.
Девід Торнлі

1
@DavidThornley: Важко сказати "вони однакові", коли вони обоє мають невизначене поведінку :)
Гонки легкості в орбіті

1
Ні в одному з виразів ++x + 3, x += 1 + 3або (x += 1) + 3мають невизначений поведінка (припускаючи , що отримане значення «припадки»).
Джон Хаскалл

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. Натомість продумайте свій дизайн ще раз.
Дорус

4
@Dorus: Вираз, який я вибрав, є просто довільним представником для "складного вираження". Не соромтеся замінити це чимось у голові, про що ви не
збираєтеся нітрохи

9
+1: Це головна причина цієї оптимізації - вона завжди правильна, незалежно від того, наскільки складним є вислів зліва.
S.Lott

9
Введення друку є гарною ілюстрацією того, що може статися.
Девід Торнлі

21

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

З

RidiculouslyComplexName += 1;

Оскільки є лише одне ім'я змінної, ви впевнені, що робить оператор.

З RidiculouslyComplexName = RidiculosulyComplexName + 1;

Завжди є сумніви, що дві сторони абсолютно однакові. Ви бачили помилку? Це стає ще гірше, коли присутні абоненти та кваліфікатори.


5
Але це стає ще гіршим у мовах, де заяви про призначення можуть неявно створювати змінні, особливо якщо мови залежать від регістру.
supercat

14

Хоча позначення + = ідіоматичне і коротше, це не причини, чому його легше читати. Найважливішою частиною коду читання є зіставлення синтаксису зі значенням, і чим ближче синтаксис відповідає мисленнєвим процесам програміста, тим він буде більш читабельним (це також причина поганого кодового коду: він не є частиною думки процес, але все ж необхідний, щоб зробити функцію коду). У цьому випадку думка є "приріст змінної x на 5", а не "нехай x є значенням x плюс 5".

Є й інші випадки, коли коротша позначення погана для читабельності, наприклад, коли ви використовуєте потрійний оператор, де ifзаява буде більш доречною.


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 написав це, і багато тих "найкращих практик" про те, як люди повинні писати код, змінилися або розвинулися з тих пір. Але це питання programer.stackexchange - це перший раз, коли я можу згадати, як хтось висловив скаргу на читаність складних призначень, тому мені цікаво, чи багато програмістів вважають їх проблемою? Потім знову, коли я набираю це запитання, це 95 оновлених результатів, тому, можливо, люди знаходять їх, коли вони читають код.


9

Крім читабельності, вони насправді роблять різні речі: +=не потрібно двічі оцінювати його лівий операнд.

Наприклад, двічі expr = expr + 5буде евалтувати expr(якщо нечисто expr).


Для всіх, крім найдивніших компіляторів, це не має значення. Більшість компіляторів досить розумні, щоб генерувати один і той же двійковий файл дляexpr = expr + 5expr += 5
вс

7
@vsz Не exprмає, якщо має побічні ефекти.
Паббі

6
@vsz: Якщо C exprмає побічні ефекти, тоді expr = expr + 5 потрібно викликати ці побічні ефекти двічі.
Кіт Томпсон,

@Pubby: якщо це має побічні ефекти, питання про "зручність" та "читабельність" не виникає, про що було сказано в первісному питанні.
vsz

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, і її не потрібно інтерпретувати. І теоретично міг би бути якийсь фантазійний процесор, який має оператор плюс-рівний, який працює швидше, ніж оператор плюс та оператор присвоєння послідовно.


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.

Сучасні імперативні мови. У (чистих) функціональних мовах x+=5має якнайменше значення x=x+5; наприклад, у Haskell останній не призводить xдо збільшення на 5 - замість цього він містить нескінченний цикл рекурсії. Хто б хотів цього скоротити?
Ліворуч близько

З сучасними компіляторами це зовсім не обов'язково швидше: навіть у C.
HörmannHH

Швидкість роботи +=не стільки залежна від мови, скільки залежна від процесора . Наприклад, 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кожному разу. І хоча ви, мабуть, ніколи не пишете код, який є таким простим, ідея залишається такою ж ... фокус робиться на тому, що ви робите, для вже існуючого значення, а не для створення нового значення.


Для мене це абсолютно навпаки - перший варіант - це як бруд на екрані, а другий - чистий і читабельний.
Михайло 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

Інші відповіді спрямовані на більш поширені випадки, але є й інша причина: у деяких мовах програмування вона може бути перевантажена; наприклад Скала .


Малий урок 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)”."

Крім того, оператори +і +=дві окремі оператори (і не тільки ці: + а, ++ , а, а ++, а + ба + = Ь різні оператори, а); мовами, де доступна перевантаження оператора, це може створити цікаві ситуації. Як описано вище - якщо ви будете перевантажувати +оператора, щоб виконати додавання, майте на увазі, що +=доведеться також перевантажуватись.


"Якщо клас визначає лише оператор +, x + = y дійсно є ярликом x = x + y." Але напевно це також працює навпаки? У C ++ дуже часто спочатку визначати, +=а потім визначати a+bяк "зробити копію a, надіслати bїї за допомогою +=, повернути копію a".
близько

1

Скажіть це один раз і лише один раз: в x = x + 1, я кажу "х" двічі.

Але ніколи не пишіть "a = b + = 1", або нам доведеться вбити 10 кошенят, 27 мишей, собаку і хом'яка.


Ніколи не слід змінювати значення змінної, оскільки це спрощує доведення коду правильним - див. Функціональне програмування. Однак якщо ви це зробите, то краще не говорити речі лише один раз.


"ніколи не змінювати значення змінної" Функціональна парадигма ?
Пітер Мортенсен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.