web-dev-qa-db-fra.com

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

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 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 est-ce que l'attribution 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 non 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??????
88
Andrii Kotliarov

Une opération comme a op= b; est équivalent à a = a op b;. Une affectation peut être utilisée comme instruction ou comme expression, tandis que l'expression donne la valeur affectée. Votre déclaration ...

k += c += k += c;

... peut, puisque l'opérateur d'affectation est associatif à droite, être également é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 Wai Ha Lee fourni). 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 le C # spec ) "Les opérandes dans une expression sont évalués de gauche à droite" (le les opérandes sont les variables c et k dans notre cas). Ceci est indépendant de la priorité et de l'associativité de l'opérateur qui dans ce cas dictent un ordre d'exécution de droite à gauche. (Voir les commentaires d'Eric Lippert réponse sur cette page).


Voyons maintenant l'IL. IL suppose une machine virtuelle basée sur la pile, c'est-à-dire qu'elle 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.0, c'est-à-dire la première affectation à k, pourrait être optimisée. Cela est probablement fait pour les variables par la gigue lors de la conversion de l'IL en code machine.

Notez également que toutes les valeurs requises par le calcul sont soit poussées vers 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. stloc fait apparaître le haut de la pile.


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

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

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 aborder ce point que vous avez soulevé. Vous avez cet ensemble de déclarations:

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

Et vous concluez ensuite à 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 plus externe + =

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

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

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

Assurez-vous que c'est clair, car c'est l'étape que vous vous êtes trompée. Lorsque vous décomposez des opérations complexes en opérations plus simples, vous devez vous assurer de le faire lentement et soigneusement et ne pas sauter étapes . Sauter les étapes est l'endroit où nous commettons des erreurs.

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

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

L'affectation affectera la même valeur à t2 que celle affectée à c, alors disons que:

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

Génial. Décomposez maintenant la deuxième ligne:

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

Génial, nous progressons. Décomposer l'affectation en t4:

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

Décomposez maintenant 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 regarder le tout:

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, quand 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 mettre en œuvre 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 raccourcit le code 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 éphémères temporaires sur la pile autant que possible. Si vous trouvez plus facile de suivre l'IL avec moins d'éphémères, tournez les optimisations off , et le générateur de code sera moins agressif.

Nous devons maintenant faire la même astuce 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 compliqué, décomposez toujours les opérations une par une. Ne prenez pas de raccourcis; ils vous égareront.

25
Eric Lippert

Cela se résume à: est le tout premier += appliqué à l'original k ou à 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 poursuivent de gauche à droite.

Donc, le plus à gauche += exécute 10 += 70.

14
Henk Holterman

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

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

ce qui me semble raisonnable.

0
Brian Yang