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??????
c#
cil
compound-assignment
Andrii Kotliarov
la source
la source
Réponses:
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 ...... peut, puisque l'opérateur d'affectation est associatif à droite, également être écrit comme
ou (développé)
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 dek
) = 110, mais 70 + 10 (ancienne valeur dek
) = 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
c
etk
dans 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.
La pile ressemble maintenant à ceci (de gauche à droite; le haut de la pile est à droite)
Notez que
IL_000c: dup
,IL_000d: stloc.0
c'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.stloc
fait apparaître le haut de la pile.La sortie du test de console suivant est (
Release
mode avec optimisations activées)la source
k = 10 + (30 + (10 + 30)) = 80
et cettec
valeur finale est définie dans la première parenthèse qui estc = 30 + (10 + 30) = 70
.k
s'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'ilk
s'agit d'un champ, d'une propriété, d'un emplacement de tableau, etc. dans la pratique, je pense que non.k
est attribué deux fois s'il s'agit d'une propriété.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:
Et vous concluez alors à tort que cela devrait donner le même résultat que cet ensemble d'instructions:
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 + =
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:
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.
L'affectation attribuera la même valeur à t2 qu'à c, donc disons que:
Génial. Maintenant, décomposez la deuxième ligne:
Super, nous progressons. Décomposez l'affectation en t4:
Maintenant, décomposez la troisième ligne:
Et maintenant, nous pouvons tout regarder:
Donc, lorsque nous avons terminé, k est 80 et c est 70.
Voyons maintenant comment cela est implémenté dans l'IL:
Maintenant, c'est un peu délicat:
Nous aurions pu implémenter ce qui précède comme
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:
et enfin:
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.
la source
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".+
, et alors vous obtiendrez+=
gratuitement carx += y
est défini commex = x + y
saufx
est é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.Cela se résume à: le tout premier est-il
+=
appliqué à l'originalk
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 déroulent toujours de gauche à droite.
Donc, le plus à gauche
+=
est en cours d'exécution10 += 70
.la source
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 à:
ce qui me paraît raisonnable.
la source
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.
la source
Réponse simple: remplacez les variables par des valeurs et vous l'avez:
la source
k = 10; m = (k += k) + k;
ne veut pas direm = (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.Vous pouvez résoudre ce problème en comptant.
Il y en a deux
c
s et deuxk
s doncEt, 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:
Alors
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
égal2m + n
etn
égaln + m
.Cela démontre que
k += c += k += c;
c'est différentk += 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 .la source
x = 1;
ety = (x += x) + x;
est-ce que votre affirmation est qu '«il y a trois x et donc y est égal à3 * x
»? Parce quey
est égal à4
dans ce cas. Maintenant, qu'eny = 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.