Y a-t-il une explication pour les opérateurs en ligne dans «k + = c + = k + = c;»?


89

Quelle est l'explication du résultat de l'opération suivante?

k += c += k += c;

J'essayais de comprendre le résultat de sortie à partir du code suivant:

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

et actuellement j'ai du mal à comprendre pourquoi le résultat pour «k» est 80. Pourquoi l'affectation de k = 40 ne fonctionne pas (en fait, Visual Studio me dit que cette valeur n'est pas utilisée ailleurs)?

Pourquoi k 80 et pas 110?

Si je divise l'opération en:

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

le résultat est k = 110.

J'essayais de regarder à travers le CIL , mais je ne suis pas si profond dans l'interprétation du CIL généré et je ne peux pas obtenir quelques détails:

 // [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
Vous avez obtenu un résultat différent parce que vous divisez la fonction, k + = c + = k + = c = 80 parce que les valeurs de k et c restent les mêmes dans toutes les sommes, donc k + = c + = k + = c est égal à 10 + 30 + 10 + 30
João Paulo Amorim

78
Exercice intéressant, mais, en pratique, n'écrivez jamais de chaînage de code comme ça à moins que vous ne vouliez que vos collègues vous détestent. :)
UnhandledExcepSean

3
@AndriiKotliarov parce que k + = c + = k + = c est 10 + 30 + 10 + 30, donc, K reçoit toutes les valeurs, et C ne reçoit que les 3 derniers arguments 30 + 10 + 30 = 70
João Paulo Amorim

6

34
"Docteur, docteur, ça fait mal quand je fais ça!" "Alors ne fais pas ça."
David Conrad

Réponses:


104

Une opération comme a op= b;équivaut à a = a op b;. Une affectation peut être utilisée comme instruction ou comme expression, tandis que comme expression, elle produit la valeur assignée. Votre déclaration ...

k += c += k += c;

... peut, puisque l'opérateur d'affectation est associatif à droite, également être écrit comme

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

ou (développé)

k =  k +  (c = c +  (k = k  + c));
     10       30       10  30   // operand evaluation order is from left to right
      |         |            
      |            40  10 + 30   // operator evaluation
         70  30 + 40
80  10 + 70

Où pendant toute l'évaluation, les anciennes valeurs des variables impliquées sont utilisées. Cela est particulièrement vrai pour la valeur de k(voir mon examen de l'IL ci-dessous et le lien fourni par Wai Ha Lee). Par conséquent, vous n'obtenez pas 70 + 40 (nouvelle valeur de k) = 110, mais 70 + 10 (ancienne valeur de k) = 80.

Le fait est que (selon la spécification C # ) "Les opérandes dans une expression sont évalués de gauche à droite" (les opérandes sont les variables cet kdans notre cas). Ceci est indépendant de la priorité et de l'associativité des opérateurs qui, dans ce cas, dictent un ordre d'exécution de droite à gauche. (Voir les commentaires sur la réponse d' Eric Lippert sur cette page).


Regardons maintenant l'IL. IL suppose une machine virtuelle basée sur la pile, c'est-à-dire qu'il n'utilise pas de registres.

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 pile ressemble maintenant à ceci (de gauche à droite; le haut de la pile est à droite)

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

Notez que IL_000c: dup, IL_000d: stloc.0c'est-à-dire la première affectation à k , pourrait être optimisée loin. Cela est probablement fait pour les variables par la gigue lors de la conversion d'IL en code machine.

Notez également que toutes les valeurs requises par le calcul sont soit transmises à la pile avant toute affectation, soit calculées à partir de ces valeurs. Les valeurs attribuées (par stloc) ne sont jamais réutilisées lors de cette évaluation. stlocfait apparaître le haut de la pile.


La sortie du test de console suivant est ( Releasemode avec optimisations activées)

évaluation de k (10)
évaluation de c (30)
évaluation de k (10)
évaluation de c (30)
40 assigné à k
70 assigné à c
80 assigné à 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;
}

Vous pouvez ajouter le résultat final avec les nombres dans la formule pour encore plus complet: final est k = 10 + (30 + (10 + 30)) = 80et cette cvaleur finale est définie dans la première parenthèse qui est c = 30 + (10 + 30) = 70.
Franck

2
En effet, s'il ks'agit d'un local, alors le magasin mort est presque certainement supprimé si les optimisations sont activées, et conservé si elles ne le sont pas. Une question intéressante est de savoir si la gigue est autorisée à éliminer la mémoire morte s'il ks'agit d'un champ, d'une propriété, d'un emplacement de tableau, etc. dans la pratique, je pense que non.
Eric Lippert

Un test de console en mode Release montre en effet qu'il kest attribué deux fois s'il s'agit d'une propriété.
Olivier Jacot-Descombes

26

Tout d'abord, les réponses de Henk et Olivier sont correctes; Je veux l'expliquer d'une manière légèrement différente. Plus précisément, je veux parler de ce que vous avez soulevé. Vous avez cet ensemble d'instructions:

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

Et vous concluez alors à tort que cela devrait donner le même résultat que cet ensemble d'instructions:

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

Il est instructif de voir comment vous vous êtes trompé et comment le faire correctement. La bonne façon de le décomposer est comme ça.

Tout d'abord, réécrivez le + =

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

Deuxièmement, réécrivez le + extérieur. J'espère que vous êtes d'accord que x = y + z doit toujours être la même chose que "évaluer y à un temporaire, évaluer z à un temporaire, additionner les temporaires, affecter la somme à x" . Alors, rendons cela très explicite:

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

Assurez-vous que ce soit clair, car c'est l'étape que vous avez mal . Lorsque vous décomposez des opérations complexes en opérations plus simples, vous devez vous assurer de le faire lentement et prudemment et de ne pas sauter d'étapes . Sauter des étapes est l'endroit où nous commettons des erreurs.

OK, maintenant décomposez l'affectation à t2, encore une fois, lentement et soigneusement.

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

L'affectation attribuera la même valeur à t2 qu'à c, donc disons que:

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

Génial. Maintenant, décomposez la deuxième ligne:

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

Super, nous progressons. Décomposez l'affectation en t4:

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

Maintenant, décomposez la troisième ligne:

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

Et maintenant, nous pouvons tout regarder:

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

Donc, lorsque nous avons terminé, k est 80 et c est 70.

Voyons maintenant comment cela est implémenté dans l'IL:

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

Maintenant, c'est un peu délicat:

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

Nous aurions pu implémenter ce qui précède comme

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

mais nous utilisons l'astuce "dup" car elle rend le code plus court et facilite la gigue, et nous obtenons le même résultat. En général, le générateur de code C # essaie de garder les temporaires "éphémères" sur la pile autant que possible. Si vous trouvez qu'il est plus facile de suivre l'IL avec moins Ephémères, optimisations tour off , et le générateur de code sera moins agressif.

Nous devons maintenant faire la même chose pour obtenir 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.

et enfin:

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

Puisque nous n'avons besoin de la somme pour rien d'autre, nous ne la dupons pas. La pile est maintenant vide et nous sommes à la fin de l'instruction.

La morale de l'histoire est la suivante: lorsque vous essayez de comprendre un programme complexe, décomposez toujours les opérations une par une . Ne prenez pas de raccourcis; ils vous égareront.


3
@ OlivierJacot-Descombes: La ligne pertinente de la spécification se trouve dans la section "Opérateurs" et dit "Les opérandes d'une expression sont évalués de gauche à droite. Par exemple, dans F(i) + G(i++) * H(i), la méthode F est appelée en utilisant l'ancienne valeur de i, puis la méthode G est appelé avec l'ancienne valeur de i, et, enfin, la méthode H est appelée avec la nouvelle valeur de i . Ceci est séparé et sans rapport avec la priorité des opérateurs. " (Je souligne.) Donc, je suppose que je me suis trompé quand j'ai dit qu'il n'y a nulle part où "l'ancienne valeur est utilisée" se produit! Cela se produit dans un exemple. Mais le bit normatif est "de gauche à droite".
Eric Lippert

1
C'était le chaînon manquant. La quintessence est que nous devons faire la différence entre l'ordre d'évaluation des opérandes et la priorité des opérateurs . L'évaluation des opérandes va de gauche à droite et dans le cas de l'OP, l'exécution de l'opérateur de droite à gauche.
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes: C'est exactement ça. La préséance et l'associativité n'ont absolument rien à voir avec l'ordre dans lequel les sous-expressions sont évaluées, si ce n'est le fait que la précédence et l'associativité déterminent où se trouvent les limites des sous-expressions . Les sous-expressions sont évaluées de gauche à droite.
Eric Lippert

1
Ooops semble que vous ne pouvez pas surcharger les opérateurs d'affectation: /
johnny 5

1
@ johnny5: C'est exact. Mais vous pouvez surcharger +, et alors vous obtiendrez +=gratuitement car x += yest défini comme x = x + ysauf xest évalué une seule fois. Cela est vrai, que le fichier +soit intégré ou défini par l'utilisateur. Donc: essayez de surcharger +sur un type de référence et voyez ce qui se passe.
Eric Lippert

14

Cela se résume à: le tout premier est-il +=appliqué à l'original kou à la valeur qui a été calculée plus à droite?

La réponse est que bien que les affectations se lient de droite à gauche, les opérations se déroulent toujours de gauche à droite.

Donc, le plus à gauche +=est en cours d'exécution 10 += 70.


1
Cela le met bien dans une coquille de noix.
Aganju

C'est en fait les opérandes qui sont évalués de gauche à droite.
Olivier Jacot-Descombes

0

J'ai essayé l'exemple avec gcc et pgcc et j'ai obtenu 110. J'ai vérifié l'IR qu'ils ont généré, et le compilateur a étendu l'expression à:

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

ce qui me paraît raisonnable.


-1

pour ce type d'affectation en chaîne, vous devez affecter les valeurs en commençant par le côté le plus à droite. Vous devez l'assigner et le calculer et l'affecter au côté gauche, et aller sur tout le chemin jusqu'à la dernière (affectation la plus à gauche), bien sûr, il est calculé comme k = 80.


Veuillez ne pas publier de réponses qui ne font que réaffirmer ce que de nombreuses autres réponses indiquent déjà.
Eric Lippert

-1

Réponse simple: remplacez les variables par des valeurs et vous l'avez:

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

Cette réponse est fausse. Bien que cette technique fonctionne dans ce cas précis, cet algorithme ne fonctionne pas en général. Par exemple, k = 10; m = (k += k) + k;ne veut pas dire m = (10 + 10) + 10. Les langages avec des expressions mutantes ne peuvent pas être analysés comme s'ils avaient une substitution de valeur désireuse . La substitution de valeur se produit dans un ordre particulier par rapport aux mutations et vous devez en tenir compte.
Eric Lippert

-1

Vous pouvez résoudre ce problème en comptant.

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

Il y en a deux c s et deux ks donc

a = 2c + 2k

Et, en conséquence des opérateurs de la langue, kégale également2c + 2k

Cela fonctionnera pour toute combinaison de variables dans ce style de chaîne:

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

Alors

a = 2m + n + 3r

Et r égalera la même chose.

Vous pouvez calculer les valeurs des autres nombres en calculant uniquement jusqu'à leur affectation la plus à gauche. Donc mégal 2m + netn égal n + m.

Cela démontre que k += c += k += c; c'est différent k += c; c += k; k += c;et donc pourquoi vous obtenez des réponses différentes.

Certaines personnes dans les commentaires semblent craindre que vous n'essayiez de généraliser à l'excès de ce raccourci à tous les types d'ajout possibles. Donc, je vais préciser que ce raccourci n'est applicable qu'à cette situation, c'est-à-dire enchaîner les affectations d'addition pour les types de nombres intégrés. Cela ne fonctionne pas (nécessairement) si vous ajoutez d'autres opérateurs dans, par exemple ()ou +, ou si vous appelez des fonctions ou si vous avez remplacé +=, ou si vous utilisez autre chose que les types de nombres de base.Il est uniquement destiné à aider avec la situation particulière de la question .


Cela ne répond pas à la question
johnny 5

@ johnny5 cela explique pourquoi vous obtenez le résultat que vous obtenez, c'est-à-dire parce que c'est ainsi que fonctionnent les mathématiques.
Matt Ellen

2
Les mathématiques et les ordres d'opérations qu'un compilateur évalue à une instruction sont deux choses différentes. Sous votre logique k + = c; c + = k; k + = c devrait donner le même résultat.
johnny 5

Non, Johnny 5, ce n'est pas ce que cela signifie. Mathématiquement, ce sont des choses différentes. Les trois opérations séparées évaluent à 3c + 2k.
Matt Ellen

2
Malheureusement, votre solution «algébrique» n'est correcte que par coïncidence . Votre technique ne fonctionne pas en général . Considérerx = 1; et y = (x += x) + x;est-ce que votre affirmation est qu '«il y a trois x et donc y est égal à 3 * x»? Parce que yest égal à 4dans ce cas. Maintenant, qu'en y = x + (x += x);est-il de votre affirmation que la loi algébrique "a + b = b + a" est remplie et que c'est aussi 4? Parce que c'est 3. Malheureusement, C # ne suit pas les règles de l'algèbre du lycée s'il y a des effets secondaires dans les expressions . C # suit les règles d'une algèbre à effet secondaire.
Eric Lippert
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.