Por que atalhos como x + = y são considerados boas práticas?

305

Não faço ideia do que realmente são chamados, mas os vejo o tempo todo. A implementação do Python é algo como:

x += 5como uma notação abreviada para x = x + 5.

Mas por que isso é considerado uma boa prática? Eu o encontrei em quase todos os livros ou tutoriais de programação que li para Python, C, R e assim por diante. Entendo que é conveniente, economizando três pressionamentos de tecla, incluindo espaços. Mas eles sempre parecem me atrapalhar quando leio código e, pelo menos na minha opinião, tornam menos legível, não mais.

Estou perdendo algum motivo claro e óbvio que eles são usados ​​em todo o lugar?

Fomite
fonte
@ EricLippert: O C # lida com isso da mesma maneira que a resposta principal descrita? É realmente mais eficiente dizer do x += 5que o CLR x = x + 5? Ou é realmente apenas açúcar sintático, como você sugere?
Blesh
24
@blesh: Que pequenos detalhes de como alguém expressa uma adição no código-fonte têm impacto na eficiência do código executável resultante podem ter sido o caso em 1970; certamente não é agora. A otimização de compiladores é boa e você tem preocupações maiores do que um nanossegundo aqui ou ali. A idéia de que o operador + = foi desenvolvido "vinte anos atrás" é obviamente falsa; o falecido Dennis Richie desenvolveu C de 1969 a 1973 no Bell Labs.
Eric Lippert
1
Veja blogs.msdn.com/b/ericlippert/archive/2011/03/29/… (veja também a parte dois)
SLaks
5
A maioria dos programadores funcionais considerará essa má prática.
Pete Kirkham

Respostas:

598

Não é taquigrafia.

O +=símbolo apareceu na linguagem C na década de 1970 e - com a idéia C de "assembler inteligente" corresponde a um modo de instrução e endereço da máquina claramente diferente:

Coisas como " i=i+1", "i+=1"e" ++i", embora em um nível abstrato produzam o mesmo efeito, correspondem em baixo nível a uma maneira diferente de trabalhar do processador.

Em particular, essas três expressões, assumindo que a ivariável reside no endereço de memória armazenado em um registro da CPU (vamos chamá-la D- pense nela como um "ponteiro para int") e a ALU do processador pega um parâmetro e retorna um resultado em um "acumulador" (vamos chamá-lo de A - pense nisso como um int).

Com essas restrições (muito comuns em todos os microprocessadores daquele período), a tradução provavelmente será

;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

A primeira maneira de fazer isso é desagradável, mas é mais geral ao operar com variáveis ​​em vez de constante ( ADD A, Bou ADD A, (D+x)) ou ao traduzir expressões mais complexas (todas elas se resumem na operação push de baixa prioridade em uma pilha, chame de alta prioridade, pop). e repita até que todos os argumentos tenham sido eliminados).

A segunda é mais típica da "máquina de estados": não estamos mais "avaliando uma expressão", mas "operando um valor": ainda usamos a ALU, mas evitamos mover valores, pois o resultado pode substituir o parâmetro. Esse tipo de instrução não pode ser usado onde expressões mais complicadas são necessárias: i = 3*i + i-2não pode ser operado no local, pois ié necessário mais vezes.

O terceiro - mesmo mais simples - nem considera a idéia de "adição", mas usa um circuito mais "primitivo" (no sentido computacional) para um contador. A instrução é reduzida, carrega mais rápido e é executada imediatamente, pois a rede combinatória necessária para atualizar um registro para torná-lo um contador é menor e, portanto, mais rápida que a de um somador completo.

Com os compiladores contemporâneos (consulte C, a essa altura), habilitando a otimização do compilador, a correspondência pode ser trocada com base na conveniência, mas ainda há uma diferença conceitual na semântica.

x += 5 significa

  • Encontre o local identificado por x
  • Adicione 5 a ele

Mas x = x + 5significa:

  • Avalie x + 5
    • Encontre o local identificado por x
    • Copie x em um acumulador
    • Adicione 5 ao acumulador
  • Armazene o resultado em x
    • Encontre o local identificado por x
    • Copie o acumulador para ele

Obviamente, a otimização pode

  • se "encontrar x" não tiver efeitos colaterais, os dois "encontrar" poderão ser feitos uma vez (e x se tornar um endereço armazenado em um registro de ponteiro)
  • as duas cópias podem ser elididas se o ADD for aplicado &xno acumulador

fazendo com que o código otimizado coincida com o código x += 5.

Mas isso pode ser feito apenas se "encontrar x" não tiver efeitos colaterais, caso contrário

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

e

*(x()) += 5;

são semanticamente diferentes, pois os x()efeitos colaterais (admitir x()é uma função que faz coisas estranhas e retorna um int*) serão produzidos duas ou uma vez.

A equivalência entre x = x + ye x += yé, portanto, devida ao caso particular em que +=e =é aplicada a um valor l direto.

Para migrar para o Python, ele herdou a sintaxe de C, mas como não há tradução / otimização ANTES da execução em linguagens interpretadas, as coisas não são necessariamente tão intimamente relacionadas (já que há uma etapa a menos de análise). No entanto, um intérprete pode se referir a diferentes rotinas de execução para os três tipos de expressão, aproveitando diferentes códigos de máquina, dependendo de como a expressão é formada e do contexto da avaliação.


Para quem gosta de mais detalhes ...

Toda CPU possui uma ALU (unidade aritmética-lógica) que é, em sua essência, uma rede combinatória cujas entradas e saídas são "conectadas" aos registradores e / ou memória, dependendo do código de operação da instrução.

As operações binárias são tipicamente implementadas como "modificador de um registrador acumulador com uma entrada obtida" em algum lugar ", em que algum lugar pode estar - dentro do próprio fluxo de instruções (típico para o manifesto de manifestação: ADD A 5) - dentro de outro registro (típico para computação de expressão com temporários: por exemplo, ADD AB) - dentro da memória, em um endereço fornecido por um registro (típico de busca de dados, por exemplo: ADD A (H)) - H, nesse caso, funciona como um ponteiro de cancelamento de referência.

Com esse pseudocódigo, x += 5é

ADD (X) 5

enquanto x = x+5é

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

Ou seja, x + 5 fornece um temporário que é atribuído posteriormente. x += 5opera diretamente em x.

A implementação real depende do conjunto real de instruções do processador: se não houver ADD (.) ccódigo de operação, o primeiro código se tornará o segundo: de jeito nenhum.

Se houver tal código de operação e a otimização estiver ativada, a segunda expressão, após eliminar os movimentos reversos e ajustar o código de registro, será a primeira.

Emilio Garavaglia
fonte
90
+1 na única resposta que explica que costumava mapear para códigos de máquina diferentes (e mais eficientes) nos tempos antigos.
Péter Török
11
@KeithThompson Isso é verdade, mas não se pode negar que a montagem teve uma enorme influência sobre o projeto da linguagem C (e, posteriormente, todos os idiomas estilo C)
MattDavey
51
Erm, "+ =" não mapeia para "inc" (mapeia para "adicionar"), "++" mapeia para "inc".
Brendan
11
Por "20 anos atrás", acho que você quer dizer "30 anos atrás". E, BTW, COBOL teve C batida por mais 20 anos com: ADICIONE 5 A X. #
930 JoelFan
10
Ótimo em teoria; errado em fatos. O x86 ASM INC adiciona apenas 1, portanto, não afeta o operador "adicionar e atribuir" discutido aqui (essa seria uma ótima resposta para "++" e "-").
Mark Brackett
281

Dependendo de como você pensa sobre isso, é realmente mais fácil de entender, porque é mais direto. Considere por exemplo:

x = x + 5 chama o processamento mental de "pegue x, adicione cinco a ele e atribua esse novo valor de volta a x"

x += 5 pode ser pensado como "aumentar x em 5"

Portanto, não é apenas uma abreviação, na verdade descreve a funcionalidade de maneira muito mais direta. Ao ler um monte de código, é muito mais fácil entender.

Eric King
fonte
32
+1, concordo totalmente. Comecei a programar quando criança, quando minha mente era facilmente maleável e x = x + 5ainda me incomodava. Quando entrei em matemática mais tarde, isso me incomodou ainda mais. O uso x += 5é significativamente mais descritivo e faz muito mais sentido como expressão.
Polinomial
44
Há também o caso em que a variável tem um nome longo: reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1... oh não !!! Um erro de digitação
9
@ Matt Fenwick: Não precisa ser um nome de variável longo; poderia ser uma expressão de algum tipo. É provável que essas sejam ainda mais difíceis de verificar, e o leitor precisa prestar muita atenção para se certificar de que é o mesmo.
David Thornley
32
X = X + 5 é uma espécie de truque quando seu objetivo é incrementar x por 5.
Jeffo
1
@ Polinomial Acho que me deparei com o conceito de x = x + 5 quando criança também. 9 anos de idade, se bem me lembro - e fazia todo sentido para mim e ainda faz todo sentido para mim agora e prefere x + = 5. O primeiro é muito mais detalhado e posso imaginar o conceito com mais clareza - a ideia de atribuir x ao valor anterior de x (seja o que for) e depois adicionar 5. Acho que cérebros diferentes funcionam de maneiras diferentes.
Chris Harrison #
48

Pelo menos em Python , x += ye x = x + ypode fazer coisas completamente diferentes.

Por exemplo, se fizermos

a = []
b = a

então a += [3]resultará em a == b == [3], enquanto a = a + [3]resultará em a == [3]e b == []. Ou seja, +=modifica o objeto no local (bem, pode ser, você pode definir o __iadd__método para fazer praticamente o que quiser), enquanto =cria um novo objeto e vincula a variável a ele.

Isso é muito importante ao realizar trabalhos numéricos com o NumPy , pois você frequentemente termina com várias referências a diferentes partes de uma matriz e é importante garantir que você não modifique inadvertidamente parte de uma matriz à qual existem outras referências, ou copiar desnecessariamente matrizes (que podem ser muito caras).

James
fonte
4
+1 para __iadd__: existem línguas onde você pode criar uma referência imutável para uma estrutura de dados mutáveis, onde a operator +=é definido, por exemplo, scala: val sb = StringBuffer(); lb += "mutable structure"vs var s = ""; s += "mutable variable". o modifica ainda mais o conteúdo da estrutura de dados, enquanto o último faz com que a variável aponte para uma nova.
ovelha voadora
43

É chamado de idioma . Os idiomas de programação são úteis porque são uma maneira consistente de escrever uma construção de programação específica.

Sempre que alguém escreve, x += yvocê sabe que xestá sendo incrementado ye não por alguma operação mais complexa (como prática recomendada, normalmente eu não misturaria operações mais complicadas e essas abreviações de sintaxe). Isso faz mais sentido ao incrementar em 1.

Joe
fonte
13
x ++ e ++ x são ligeiramente diferentes de x + = 1 e entre si.
Gary Willoughby
1
@ Gary: ++xe x+=1são equivalentes em C e Java (talvez também em C #), embora não necessariamente em C ++, devido à semântica complexa do operador. A chave é que ambos avaliam xuma vez, incrementam a variável em um e têm um resultado que é o conteúdo da variável após a avaliação.
Donal Fellows
3
@Donal Fellows: A precedência é diferente, então ++x + 3não é a mesma coisa que x += 1 + 3. Entre parênteses oe x += 1é idêntico. Como declarações por si mesmas, são idênticas.
David Thornley
1
@DavidThornley: É difícil dizer "eles são idênticos" quando ambos têm um comportamento indefinido :)
Lightness Races in Orbit
1
Nenhuma das expressões ++x + 3, x += 1 + 3ou (x += 1) + 3ter indefinido comportamento (assumindo que o valor "encaixa" resultantes).
John Hascall
42

Para esclarecer um pouco o argumento de @ Pubby, considere someObj.foo.bar.func(x, y, z).baz += 5

Sem o +=operador, existem dois caminhos a seguir:

  1. someObj.foo.bar.func(x, y, z).baz = someObj.foo.bar.func(x, y, z).baz + 5. Isso não é apenas extremamente redundante e longo, mas também mais lento. Portanto, seria preciso
  2. Utilize uma variável temporária: tmp := someObj.foo.bar.func(x, y, z); tmp.baz = tmp.bar + 5. Tudo bem, mas é muito barulho para uma coisa simples. Isso é realmente muito parecido com o que acontece no tempo de execução, mas é entediante escrever e usar apenas +=mudará o trabalho para o compilador / intérprete.

A vantagem de +=outros operadores é inegável, enquanto se acostumar com eles é apenas uma questão de tempo.

back2dos
fonte
Quando você tiver três níveis de profundidade na cadeia de objetos, poderá parar de se importar com pequenas otimizações como 1 e códigos ruidosos como 2. Em vez disso, pense no seu design mais uma vez.
Dorus
4
@ Dorus: A expressão que eu escolhi é apenas um representante arbitrário para "expressão complexa". Sinta-se livre para substituí-lo por algo em sua cabeça, que você não vai procurar defeitos sobre;)
back2dos
9
+1: esse é o principal motivo dessa otimização - sempre está correto, não importa quão complexa seja a expressão do lado esquerdo.
31512 S.Lott
9
Colocar um erro de digitação é uma boa ilustração do que pode acontecer.
David Thornley
21

É verdade que é mais curto e fácil, e é provável que tenha sido inspirado pela linguagem assembly subjacente, mas a razão pela qual é uma prática recomendada é que ela evita toda uma classe de erros e facilita a revisão do código e a certeza o que faz.

Com

RidiculouslyComplexName += 1;

Como há apenas um nome de variável envolvido, você tem certeza do que a declaração faz.

Com RidiculouslyComplexName = RidiculosulyComplexName + 1;

Sempre há dúvidas de que os dois lados são exatamente iguais. Você viu o bug? Fica ainda pior quando há inscrições e qualificadores.

Jamie Cox
fonte
5
Piora ainda nos idiomas em que as instruções de atribuição podem criar implicitamente variáveis, especialmente se os idiomas diferenciam maiúsculas de minúsculas.
precisa
14

Embora a notação + = seja idiomática e mais curta, essas não são as razões pelas quais é mais fácil ler. A parte mais importante da leitura do código é o mapeamento da sintaxe para o significado e, quanto mais a sintaxe corresponder aos processos de pensamento do programador, mais legível será (esta também é a razão pela qual o código clichê é ruim: não faz parte do pensamento processo, mas ainda necessário para fazer o código funcionar). Nesse caso, o pensamento é "incrementar variável x por 5", não "seja x o valor de x mais 5".

Há outros casos em que uma notação menor é ruim para facilitar a leitura, por exemplo, quando você usa um operador ternário em que uma ifinstrução seria mais apropriada.

tdammers
fonte
11

Para ter uma ideia de por que esses operadores estão nos idiomas 'C-style', para começar, há um trecho da K&R 1st Edition (1978), 34 anos atrás:

Além da concisão, os operadores de atribuição têm a vantagem de corresponder melhor à maneira como as pessoas pensam. Dizemos "adicione 2 a i" ou "incremente i em 2", não "pegue i, adicione 2 e coloque o resultado novamente em i". Assim i += 2. Além disso, para uma expressão complicada como

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

o operador de atribuição facilita a compreensão do código, pois o leitor não precisa verificar meticulosamente se duas expressões longas são realmente as mesmas ou se perguntam por que não são. E um operador de atribuição pode até ajudar o compilador a produzir código mais eficiente.

Acho que, nesta passagem, ficou claro que Brian Kernighan e Dennis Ritchie (K&R) acreditavam que os operadores de atribuição composta ajudavam na legibilidade do código.

Já faz muito tempo que a K&R escreveu isso, e muitas das "melhores práticas" sobre como as pessoas deveriam escrever código mudaram ou evoluíram desde então. Mas essa pergunta programmers.stackexchange é a primeira vez que lembro que alguém fez uma reclamação sobre a legibilidade de atribuições compostas, então me pergunto se muitos programadores as consideram um problema. Então, novamente, enquanto digito isso, a pergunta tem 95 votos positivos, então talvez as pessoas os achem distraídos ao ler código.

Michael Burr
fonte
9

Além da legibilidade, eles realmente fazem coisas diferentes: +=não precisam avaliar o operando esquerdo duas vezes.

Por exemplo, expr = expr + 5seria avaliado exprduas vezes (supondo que exprseja impuro).

Pubby
fonte
Para todos, exceto os compiladores mais estranhos, isso não importa. A maioria dos compiladores são inteligentes o suficiente para gerar a mesma binário para expr = expr + 5eexpr += 5
vsz
7
@vsz Não se exprtem efeitos colaterais.
Pubby
6
@ VSZ: Em C, se exprtiver efeitos colaterais, expr = expr + 5 deve invocar esses efeitos colaterais duas vezes.
Keith Thompson
@ Pubby: se tiver efeitos colaterais, não há nenhum problema sobre "conveniência" e "legibilidade", que foi o ponto da pergunta original.
vsz
2
É melhor ter cuidado com essas declarações de "efeitos colaterais". Lê e escreve para volatileefeitos colaterais, e x=x+5e x+=5têm os mesmos efeitos colaterais quando xévolatile
MSalters
6

Além dos méritos óbvios que outras pessoas descreveram muito bem, quando você tem nomes muito longos, é mais compacto.

  MyVeryVeryVeryVeryVeryLongName += 1;

ou

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;
Andrey Rubshtein
fonte
6

É conciso.

É muito mais curto para digitar. Envolve menos operadores. Tem menos área de superfície e menos oportunidade de confusão.

Ele usa um operador mais específico.

Este é um exemplo artificial e não tenho certeza se os compiladores reais implementam isso. x + = y realmente usa um argumento e um operador e modifica x no lugar. x = x + y poderia ter uma representação intermediária de x = z onde z é x + y. Este último usa dois operadores, adição e atribuição, e uma variável temporária. O operador único deixa super claro que o lado do valor não pode ser outra coisa senão y e não precisa ser interpretado. E, teoricamente, poderia haver alguma CPU sofisticada que possua um operador mais-igual que seja executado mais rapidamente do que um operador mais e um operador de atribuição em série.

Mark Canlas
fonte
2
Teoricamente esquemático. As ADDinstruções em muitas CPUs têm variantes que operam diretamente em registradores ou memória usando outros registradores, memória ou constantes como o segundo adendo. Nem todas as combinações estão disponíveis (por exemplo, adicionar memória à memória), mas há o suficiente para ser útil. De qualquer forma, qualquer compilador com um otimizador decente saberá gerar o mesmo código para x = x + yo que seria x += y.
Blrfl
Daí "artificial".
Mark Canlas
5

É um bom idioma. Se é mais rápido ou não, depende do idioma. Em C, é mais rápido porque se traduz em uma instrução para aumentar a variável no lado direito. Linguagens modernas, incluindo Python, Ruby, C, C ++ e Java, suportam a sintaxe op =. É compacto e você se acostuma rapidamente. Como você o verá muito no código de outras pessoas (OPC), você também pode se acostumar e usá-lo. Aqui está o que acontece em alguns outros idiomas.

No Python, a digitação x += 5ainda causa a criação do objeto inteiro 1 (embora possa ser extraído de um pool) e o órfão do objeto inteiro contendo 5.

Em Java, causa uma conversão tácita. Tente digitar

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
fonte
Modernos imperativas idiomas. Em linguagens funcionais (puras), x+=5tem tão pouco significado quanto x=x+5; por exemplo, em Haskell, o último não causa xaumento de 5 - em vez disso, gera um loop infinito de recursão. Quem iria querer uma abreviação para isso?
usar o seguinte comando
Não é necessariamente mais rápido com os compiladores modernos: mesmo em C.
HörmannHH 19/05/15
A velocidade de +=não depende tanto do idioma quanto do processador . Por exemplo, o X86 é uma arquitetura de dois endereços, que suporta apenas +=nativamente. Uma declaração como a = b + c;deve ser compilada, a = b; a += c;porque simplesmente não há instruções que possam colocar o resultado da adição em qualquer outro lugar do que no lugar de um dos summands. A arquitetura Power, por outro lado, é uma arquitetura de três endereços que não possui comandos especiais +=. Nesta arquitetura, as instruções a += b;e a = a + b;sempre compilam no mesmo código.
C26
4

Operadores como esses +=são muito úteis quando você está usando uma variável como acumulador , ou seja, um total em execução:

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

É muito mais fácil ler do que:

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

No primeiro caso, conceitualmente, você está modificando o valor x. No segundo caso, você está computando um novo valor e atribuindo-o a xcada vez. E embora você provavelmente nunca escreva um código tão simples assim, a idéia permanece a mesma ... o foco está no que você está fazendo com um valor existente, em vez de criar um novo valor.

Caleb
fonte
Para mim, é exatamente o oposto - a primeira variante é como sujeira na tela e a segunda é limpa e legível.
Mikhail V
1
Traços diferentes para pessoas diferentes, @MikhailV, mas se você é novo na programação, pode mudar de ideia depois de um tempo.
Caleb #
Eu não sou tão novo em programação, acho que você simplesmente se acostumou à notação + = por um longo tempo e, portanto, pode lê-la. O que inicialmente não torna uma sintaxe bonita, portanto, um operador + cercado por espaços é objetivamente mais limpo do que + = e amigos que são quase imperceptíveis. Não que sua resposta esteja incorreta, mas não se deve confiar muito no próprio hábito, tornando o consumo "fácil de ler".
Mikhail V
Meu sentimento é que é mais a diferença conceitual entre acumular e armazenar novos valores do que tornar a expressão mais compacta. A maioria das linguagens de programação imperativas possui operadores semelhantes, por isso parece que não sou o único que a considera útil. Mas, como eu disse, acidente vascular cerebral diferente para pessoas diferentes. Não use se você não gostar. Se você quiser continuar a discussão, talvez o bate-papo sobre engenharia de software seja mais apropriado.
Caleb
1

Considere isto

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

D0 você realmente quer escrever

(some_object[index])->some_other_object[more] = (some_object[index])->some_other_object[more] + 5
S.Lott
fonte
1

As outras respostas têm como alvo os casos mais comuns, mas há outro motivo: em algumas linguagens de programação, ele pode ser sobrecarregado; por exemplo, Scala .


Pequena lição sobre Scala:

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

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

Se uma classe define apenas o +operador, x+=yé realmente um atalho de x=x+y.

Se uma classe sobrecarregar +=, no entanto, elas não são:

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

Além disso, operadores +e +=são dois operadores separados (e não apenas estes: + a, ++ a, a ++, a + ba + = b também são operadores diferentes); nos idiomas em que a sobrecarga do operador está disponível, isso pode criar situações interessantes. Assim como descrito acima - se você sobrecarregar o +operador para realizar a adição, lembre-se de que também +=será necessário sobrecarregar.

ovelhas voadoras
fonte
"Se uma classe define apenas o operador +, x + = y é realmente um atalho de x = x + y." Mas certamente também funciona ao contrário? No C ++, é muito comum definir primeiro +=e depois definir a+bcomo "fazer uma cópia a, colocar bnela usando +=, retornar a cópia de a".
leftaroundabout
1

Diga uma vez e apenas uma vez: em x = x + 1, eu digo 'x' duas vezes.

Mas nunca escreva 'a = b + = 1' ou teremos que matar 10 gatinhos, 27 ratos, um cachorro e um hamster.


Você nunca deve alterar o valor de uma variável, pois isso torna mais fácil provar que o código está correto - consulte programação funcional. No entanto, se o fizer, é melhor não dizer as coisas apenas uma vez.

richard
fonte
"nunca mude o valor de uma variável" Paradigma funcional ?
Peter Mortensen