C'è una spiegazione per gli operatori in linea in "k + = c + = k + = c;"?


89

Qual è la spiegazione del risultato della seguente operazione?

k += c += k += c;

Stavo cercando di capire il risultato dell'output dal seguente codice:

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

e attualmente sto lottando per capire perché il risultato per "k" è 80. Perché l'assegnazione di k = 40 non funziona (in realtà Visual Studio mi dice che quel valore non viene utilizzato altrove)?

Perché k 80 e non 110?

Se suddivido l'operazione in:

k+=c;
c+=k;
k+=c;

il risultato è k = 110.

Stavo cercando di esaminare il CIL , ma non sono così profondo nell'interpretare il CIL generato e non riesco a ottenere alcuni dettagli:

 // [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??????

3
Hai ottenuto risultati diversi perché hai diviso la funzione, k + = c + = k + = c = 80 perché i valori di k e c rimangono gli stessi in tutte le somme, quindi k + = c + = k + = c è uguale a 10 + 30 + 10 + 30
João Paulo Amorim

78
Esercizio interessante, ma, in pratica, non scrivere mai concatenamenti di codice in questo modo a meno che tu non voglia che i tuoi colleghi ti odino. :)
UnhandledExcepSean

3
@AndriiKotliarov perché k + = c + = k + = c è 10 + 30 + 10 + 30, quindi, K riceve tutti i valori e C ottiene solo gli ultimi 3 argomenti 30 + 10 + 30 = 70
João Paulo Amorim

6
Vale anche la pena leggere: la risposta di Eric Lippert a Qual è la differenza tra i ++ e ++ i?
Wai Ha Lee

34
"Dottore, dottore, fa male quando faccio questo!" "Quindi non farlo."
David Conrad

Risposte:


104

Un'operazione simile a op= b;è equivalente a a = a op b;. Un compito può essere usato come dichiarazione o come espressione, mentre come espressione restituisce il valore assegnato. La tua dichiarazione ...

k += c += k += c;

... può, poiché l'operatore di assegnazione è associativo a destra, anche essere scritto come

k += (c += (k += c));

o (espanso)

k =  k +  (c = c +  (k = k  + c));
     10301030   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   4010 + 30   // operator evaluation7030 + 40
8010 + 70

Dove durante l'intera valutazione vengono utilizzati i vecchi valori delle variabili coinvolte. Ciò è particolarmente vero per il valore di k(vedere la mia recensione dell'IL di seguito e il collegamento fornito da Wai Ha Lee). Pertanto, non ottieni 70 + 40 (nuovo valore di k) = 110, ma 70 + 10 (vecchio valore di k) = 80.

Il punto è che (secondo la specifica C # ) "Gli operandi in un'espressione vengono valutati da sinistra a destra" (gli operandi sono le variabili ce knel nostro caso). Ciò è indipendente dalla precedenza e dall'associatività dell'operatore che in questo caso determina un ordine di esecuzione da destra a sinistra. (Vedi i commenti alla risposta di Eric Lippert su questa pagina).


Ora diamo un'occhiata all'IL. IL presuppone una macchina virtuale basata su stack, ovvero non utilizza registri.

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

La pila ora ha questo aspetto (da sinistra a destra; la parte superiore della pila è a destra)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

Si noti che IL_000c: dup, IL_000d: stloc.0ovvero il primo incarico a k , potrebbe essere ottimizzato. Probabilmente questo viene fatto per le variabili dal jitter durante la conversione di IL in codice macchina.

Si noti inoltre che tutti i valori richiesti dal calcolo vengono inseriti nello stack prima che venga effettuata qualsiasi assegnazione o vengono calcolati da questi valori. I valori assegnati (da stloc) non vengono mai riutilizzati durante questa valutazione. stlocapre la parte superiore della pila.


L'output del seguente test della console è ( Releasemodalità con ottimizzazioni attive)

valutare k (10)
valutare c (30)
valutare k (10)
valutare c (30)
40 assegnato a k
70 assegnato a c
80 assegnato a k

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

Potresti aggiungere il risultato finale con i numeri nella formula per ancora più completo: finale è k = 10 + (30 + (10 + 30)) = 80e quel cvalore finale è impostato nella prima parentesi che è c = 30 + (10 + 30) = 70.
Franck

2
In effetti, se kè un locale, il negozio morto viene quasi certamente rimosso se le ottimizzazioni sono attive e conservato se non lo sono. Una domanda interessante è se al jitter sia permesso di elide la memoria morta se kè un campo, una proprietà, uno slot di array e così via; in pratica credo di no.
Eric Lippert

Un test della console in modalità Release mostra infatti che kviene assegnato due volte se si tratta di una proprietà.
Olivier Jacot-Descombes

26

Prima di tutto, le risposte di Henk e Olivier sono corrette; Voglio spiegarlo in un modo leggermente diverso. In particolare, voglio affrontare questo punto che hai sollevato. Hai questo insieme di affermazioni:

int k = 10;
int c = 30;
k += c += k += c;

E poi concludi erroneamente che questo dovrebbe dare lo stesso risultato di questo insieme di affermazioni:

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

È informativo vedere come hai sbagliato e come farlo nel modo giusto. Il modo giusto per scomporlo è così.

Innanzitutto, riscrivi il + = più esterno

k = k + (c += k += c);

Secondo, riscrivi il + più esterno. Spero che tu sia d'accordo sul fatto che x = y + z deve sempre essere uguale a "valuta y come temporaneo, valuta z come temporaneo, somma i provvisori, assegna la somma ax" . Quindi rendiamolo molto esplicito:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

Assicurati che sia chiaro, perché questo è il passaggio che hai sbagliato . Quando si suddividono operazioni complesse in operazioni più semplici, è necessario assicurarsi di farlo lentamente e con attenzione e di non saltare i passaggi . Saltare i passaggi è dove commettiamo errori.

OK, ora scomponi il compito di t2, di nuovo, lentamente e con attenzione.

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

L'assegnazione assegnerà a t2 lo stesso valore assegnato a c, quindi diciamo che:

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

Grande. Ora scomponi la seconda riga:

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Fantastico, stiamo facendo progressi. Suddividi l'assegnazione a t4:

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Ora abbatti la terza riga:

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

E ora possiamo guardare tutto:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

Quindi, quando abbiamo finito, k è 80 ec è 70.

Ora diamo un'occhiata a come questo viene implementato nell'IL:

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

Ora questo è un po 'complicato:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

Avremmo potuto implementare quanto sopra come

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

ma usiamo il trucco "dup" perché rende il codice più corto e facilita il jitter, e otteniamo lo stesso risultato. In generale, il generatore di codice C # cerca di mantenere i provvisori "temporanei" nello stack il più possibile. Se si trova più facile seguire l'IL con un minor numero di ephemerals, girare ottimizzazioni off , e il generatore di codice sarà meno aggressivo.

Ora dobbiamo fare lo stesso trucco per ottenere c:

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

e infine:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

Poiché non abbiamo bisogno della somma per nient'altro, non la duplichiamo. Lo stack è ora vuoto e siamo alla fine dell'istruzione.

La morale della storia è: quando cerchi di capire un programma complicato, interrompi sempre le operazioni una alla volta . Non prendere scorciatoie; ti porteranno fuori strada.


3
@ OlivierJacot-Descombes: La riga pertinente della specifica si trova nella sezione "Operatori" e dice "Gli operandi in un'espressione vengono valutati da sinistra a destra. Ad esempio, in F(i) + G(i++) * H(i), il metodo F viene chiamato utilizzando il vecchio valore di i, quindi il metodo G viene chiamato con il vecchio valore di i e, infine, il metodo H viene chiamato con il nuovo valore di i . Questo è separato e non correlato alla precedenza degli operatori. " (Enfasi aggiunta.) Quindi immagino di essermi sbagliato quando ho detto che non c'è da nessuna parte che "viene usato il vecchio valore" si verifica! Si verifica in un esempio. Ma la parte normativa è "da sinistra a destra".
Eric Lippert

1
Questo era l'anello mancante. La quintessenza è che dobbiamo distinguere tra ordine di valutazione degli operandi e precedenza degli operatori . La valutazione dell'operando va da sinistra a destra e nel caso dell'OP l'esecuzione dell'operatore da destra a sinistra.
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes: È esattamente vero. La precedenza e l'associatività non hanno nulla a che fare con l'ordine in cui vengono valutate le sottoespressioni, a parte il fatto che la precedenza e l'associatività determinano dove sono i confini della sottoespressione . Le sottoespressioni vengono valutate da sinistra a destra.
Eric Lippert

1
Ops sembra che tu non possa sovraccaricare gli operatori di assegnazione: /
johnny 5

1
@ johnny5: è corretto. Ma puoi sovraccaricare +, e quindi otterrai +=gratuitamente perché x += yè definito come x = x + yeccetto xviene valutato una sola volta. Ciò è vero indipendentemente dal fatto che +sia integrato o definito dall'utente. Quindi: prova a sovraccaricare +un tipo di riferimento e guarda cosa succede.
Eric Lippert,

14

Si riduce a: il primo è +=applicato all'originale ko al valore che è stato calcolato più a destra?

La risposta è che sebbene le assegnazioni si leghino da destra a sinistra, le operazioni procedono comunque da sinistra a destra.

Quindi quello più a sinistra +=è in esecuzione 10 += 70.


1
Questo lo mette bene in un guscio di noce.
Aganju

In realtà sono gli operandi che vengono valutati da sinistra a destra.
Olivier Jacot-Descombes

0

Ho provato l'esempio con gcc e pgcc e ho ottenuto 110. Ho controllato l'IR che hanno generato e il compilatore ha espanso l'espr in:

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

che mi sembra ragionevole.


-1

per questo tipo di assegnazioni a catena, devi assegnare i valori partendo dal lato più a destra. Devi assegnarlo e calcolarlo e assegnarlo al lato sinistro, e andare avanti fino all'ultimo (compito più a sinistra), certo è calcolato come k = 80.


Si prega di non pubblicare risposte che semplicemente ribadiscono ciò che numerose altre risposte già affermano.
Eric Lippert,

-1

Risposta semplice: sostituisci vars con valori e l'hai ottenuto:

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!

Questa risposta è sbagliata. Sebbene questa tecnica funzioni in questo caso specifico, quell'algoritmo non funziona in generale. Ad esempio, k = 10; m = (k += k) + k;non significa m = (10 + 10) + 10. Le lingue con espressioni mutanti non possono essere analizzate come se avessero una rapida sostituzione di valore . La sostituzione del valore avviene in un ordine particolare rispetto alle mutazioni e devi tenerne conto.
Eric Lippert,

-1

Puoi risolverlo contando.

a = k += c += k += c

Ci sono due ce due kcosì

a = 2c + 2k

E, come conseguenza degli operatori della lingua, kanche uguale2c + 2k

Funzionerà per qualsiasi combinazione di variabili in questo stile di catena:

a = r += r += r += m += n += m

Così

a = 2m + n + 3r

E rsarà uguale lo stesso.

Puoi calcolare i valori degli altri numeri calcolando solo fino alla loro assegnazione più a sinistra. Quindi è muguale 2m + ne nuguale n + m.

Questo dimostra che k += c += k += c;è diverso k += c; c += k; k += c;e quindi perché ottieni risposte diverse.

Alcune persone nei commenti sembrano essere preoccupate che tu possa tentare di generalizzare eccessivamente da questa scorciatoia a tutti i possibili tipi di aggiunta. Quindi, chiarirò che questa scorciatoia è applicabile solo a questa situazione, vale a dire concatenare le assegnazioni di addizioni per i tipi di numeri incorporati. Non funziona (necessariamente) se aggiungi altri operatori, ad esempio ()o +, o se chiami le funzioni o se hai sovrascritto +=, o se stai usando qualcosa di diverso dai tipi di numero di base. Ha lo scopo solo di aiutare con la situazione particolare nella domanda .


Questo non risponde alla domanda
johnny 5

@ johnny5 spiega perché ottieni il risultato che ottieni, cioè perché è così che funziona la matematica.
Matt Ellen

2
La matematica e gli ordini di operazioni con cui un compilatore valuta un'istruzione sono due cose diverse. Secondo la tua logica k + = c; c + = k; k + = c dovrebbe restituire lo stesso risultato.
johnny 5

No, Johnny 5, non è questo che significa. Matematicamente sono cose diverse. Le tre operazioni separate valutano 3c + 2k.
Matt Ellen

2
Purtroppo la tua soluzione "algebrica" ​​è solo corretta coincidenza . La tua tecnica non funziona in generale . Considera x = 1;ed y = (x += x) + x;è tua affermazione che "ci sono tre x e quindi y è uguale a 3 * x"? Perché yè uguale a 4in questo caso. Ora, che dire della y = x + (x += x);tua affermazione che la legge algebrica "a + b = b + a" è soddisfatta e anche questo è 4? Perché questo è 3. Sfortunatamente, C # non segue le regole dell'algebra delle scuole superiori se ci sono effetti collaterali nelle espressioni . C # segue le regole di un'algebra con effetti collaterali.
Eric Lippert
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.