Qual é a explicação para o resultado da operação a seguir?
k += c += k += c;
Eu estava tentando entender o resultado de saída do seguinte código:
int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70
e atualmente estou lutando para entender por que o resultado para "k" é 80. Por que a atribuição de k = 40 não está funcionando (na verdade, o Visual Studio me diz que esse valor não está sendo usado em outro lugar)?
Por que k é 80 e não 110?
Se eu dividir a operação em:
k+=c;
c+=k;
k+=c;
o resultado é k = 110.
Eu estava tentando examinar o CIL , mas não sou tão profundo em interpretar o CIL gerado e não consigo obter alguns detalhes:
// [11 13 - 11 24]
IL_0001: ldc.i4.s 10
IL_0003: stloc.0 // k
// [12 13 - 12 24]
IL_0004: ldc.i4.s 30
IL_0006: stloc.1 // c
// [13 13 - 13 30]
IL_0007: ldloc.0 // k expect to be 10
IL_0008: ldloc.1 // c
IL_0009: ldloc.0 // k why do we need the second load?
IL_000a: ldloc.1 // c
IL_000b: add // I expect it to be 40
IL_000c: dup // What for?
IL_000d: stloc.0 // k - expected to be 40
IL_000e: add
IL_000f: dup // I presume the "magic" happens here
IL_0010: stloc.1 // c = 70
IL_0011: add
IL_0012: stloc.0 // k = 80??????
c#
cil
compound-assignment
Andrii Kotliarov
fonte
fonte
Respostas:
Uma operação like
a op= b;
é equivalente aa = a op b;
. Uma atribuição pode ser usada como declaração ou expressão, enquanto como expressão ela produz o valor atribuído. Sua declaração ...... pode, uma vez que o operador de atribuição é associativo à direita, também ser escrito como
ou (expandido)
Onde durante toda a avaliação são usados os valores antigos das variáveis envolvidas. Isso é especialmente verdadeiro para o valor de
k
(veja minha análise do IL abaixo e o link fornecido por Wai Ha Lee). Portanto, você não está obtendo 70 + 40 (novo valor dek
) = 110, mas 70 + 10 (valor antigo dek
) = 80.A questão é que (de acordo com a especificação C # ) "Operandos em uma expressão são avaliados da esquerda para a direita" (os operandos são as variáveis
c
e,k
em nosso caso). Isso é independente da precedência do operador e da associatividade que, neste caso, ditam uma ordem de execução da direita para a esquerda. (Veja os comentários à resposta de Eric Lippert nesta página).Agora vamos dar uma olhada no IL. IL assume uma máquina virtual baseada em pilha, ou seja, não usa registradores.
A pilha agora se parece com isto (da esquerda para a direita; topo da pilha está à direita)
Observe que
IL_000c: dup
,IL_000d: stloc.0
isto é, a primeira atribuição ak
, pode ser otimizada. Provavelmente, isso é feito para variáveis pelo jitter ao converter IL em código de máquina.Observe também que todos os valores exigidos pelo cálculo são colocados na pilha antes de qualquer atribuição ser feita ou são calculados a partir desses valores. Os valores atribuídos (por
stloc
) nunca são reutilizados durante esta avaliação.stloc
aparece no topo da pilha.A saída do seguinte teste de console é (
Release
modo com otimizações ativadas)fonte
k = 10 + (30 + (10 + 30)) = 80
e essec
valor final é definido no primeiro parêntese que éc = 30 + (10 + 30) = 70
.k
for um local, o armazenamento morto é quase certamente removido se as otimizações estiverem ativadas e preservado se não estiverem. Uma questão interessante é se o jitter tem permissão para omitir o armazenamento morto sek
for um campo, propriedade, slot de array e assim por diante; na prática, acredito que não.k
é atribuído duas vezes se for uma propriedade.Em primeiro lugar, as respostas de Henk e Olivier estão corretas; Quero explicar de uma maneira ligeiramente diferente. Especificamente, quero abordar esse ponto que você fez. Você tem este conjunto de afirmações:
E então você conclui incorretamente que isso deve dar o mesmo resultado que este conjunto de afirmações:
É informativo saber como você errou e como fazer da maneira certa. A maneira certa de decompô-lo é assim.
Primeiro, reescreva o mais externo + =
Em segundo lugar, reescrever o + externo. Espero que você concorde que x = y + z deve ser sempre o mesmo que "avalie y para um temporário, avalie z para um temporário, some os temporários, atribua a soma a x" . Então, vamos deixar isso bem explícito:
Certifique-se de que está claro, porque essa é a etapa que você errou . Ao dividir operações complexas em operações mais simples, você deve certificar-se de fazê-lo lenta e cuidadosamente e não pular etapas . Pular etapas é onde cometemos erros.
OK, agora divida a atribuição para t2, de novo, lenta e cuidadosamente.
A atribuição atribuirá a t2 o mesmo valor atribuído a c, então digamos que:
Ótimo. Agora divida a segunda linha:
Ótimo, estamos progredindo. Divida a atribuição para t4:
Agora divida a terceira linha:
E agora podemos olhar para tudo:
Então, quando terminarmos, k é 80 e c é 70.
Agora, vamos ver como isso é implementado no IL:
Agora, isso é um pouco complicado:
Poderíamos ter implementado o acima como
mas usamos o truque "dup" porque torna o código mais curto e mais fácil no jitter, e obtemos o mesmo resultado. Em geral, o gerador de código C # tenta manter os temporários "efêmeros" na pilha tanto quanto possível. Se você achar que é mais fácil de seguir do IL com menos Ephemerals, vire otimizações off , e o gerador de código será menos agressiva.
Agora temos que fazer o mesmo truque para obter c:
e finalmente:
Como não precisamos da soma para mais nada, não a enganamos. A pilha agora está vazia e estamos no final da instrução.
A moral da história é: quando você está tentando entender um programa complicado, sempre divida as operações uma de cada vez . Não tome atalhos; eles o desviarão.
fonte
F(i) + G(i++) * H(i)
, o método F é chamado usando o valor antigo de i, então o método G é chamado com o valor antigo de i e, finalmente, o método H é chamado com o novo valor de i . Isso é separado e não relacionado à precedência do operador. " (Ênfase adicionada). Portanto, acho que estava errado quando disse que não há nenhum lugar onde "o valor antigo seja usado" ocorre! Isso ocorre em um exemplo. Mas a parte normativa é "da esquerda para a direita".+
, e então você obterá+=
gratuitamente porquex += y
é definido comox = x + y
excetox
é avaliado apenas uma vez. Isso é verdade independentemente de o+
ser integrado ou definido pelo usuário. Portanto: tente sobrecarregar+
em um tipo de referência e veja o que acontece.Tudo se resume a: o primeiro é
+=
aplicado ao originalk
ou ao valor que foi calculado mais à direita?A resposta é que, embora as atribuições sejam vinculadas da direita para a esquerda, as operações continuam da esquerda para a direita.
Portanto, o mais à esquerda
+=
está executando10 += 70
.fonte
Tentei o exemplo com gcc e pgcc e obtive 110. Verifiquei o IR que eles geraram e o compilador expandiu a expr para:
o que parece razoável para mim.
fonte
para este tipo de atribuições em cadeia, você deve atribuir os valores começando pelo lado direito. Você tem que atribuir, calcular e atribuir ao lado esquerdo, e prosseguir até o final (atribuição mais à esquerda). Claro que é calculado como k = 80.
fonte
Resposta simples: substitua vars por valores e você entendeu:
fonte
k = 10; m = (k += k) + k;
não significam = (10 + 10) + 10
. Linguagens com expressões mutantes não podem ser analisadas como se tivessem uma rápida substituição de valor . A substituição de valor ocorre em uma ordem específica em relação às mutações e você deve levar isso em consideração.Você pode resolver isso contando.
Existem dois se
c
doisk
s entãoE, como consequência dos operadores da linguagem,
k
também é igual2c + 2k
Isso funcionará para qualquer combinação de variáveis neste estilo de cadeia:
assim
E
r
será igual ao mesmo.Você pode calcular os valores dos outros números calculando apenas até sua atribuição mais à esquerda. Portanto,
m
iguais2m + n
en
iguaisn + m
.Isso demonstra que
k += c += k += c;
é diferentek += c; c += k; k += c;
e, portanto, por que você obtém respostas diferentes.Algumas pessoas nos comentários parecem estar preocupadas com a possibilidade de você tentar generalizar demais a partir desse atalho para todos os tipos possíveis de adição. Portanto, deixarei claro que este atalho só se aplica a esta situação, ou seja, encadear atribuições de adição para os tipos de número integrados. Não funciona (necessariamente) se você adicionar outros operadores em, por exemplo,
()
ou+
, ou se você chamar funções ou se tiver substituído+=
, ou se estiver usando algo diferente dos tipos básicos de número. Destina-se apenas a ajudar na situação particular da questão .fonte
x = 1;
ey = (x += x) + x;
é sua afirmação que "há três x e, portanto, y é igual a3 * x
"? Porquey
é igual a4
neste caso. Agora, oy = x + (x += x);
que você acha de que a lei algébrica "a + b = b + a" é cumprida e isso também é 4? Porque este é 3. Infelizmente, C # não segue as regras da álgebra do ensino médio se houver efeitos colaterais nas expressões . C # segue as regras de uma álgebra de efeito colateral.