Jakie jest wyjaśnienie wyniku następującej operacji?
k += c += k += c;
Próbowałem zrozumieć wynik wyjściowy z następującego kodu:
int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70
i obecnie nie rozumiem, dlaczego wynik „k” to 80. Dlaczego przypisanie k = 40 nie działa (w rzeczywistości program Visual Studio informuje mnie, że ta wartość nie jest używana gdzie indziej)?
Dlaczego k 80 a nie 110?
Jeśli podzielę operację na:
k+=c;
c+=k;
k+=c;
wynik to k = 110.
Próbowałem przejrzeć CIL , ale nie jestem tak dogłębny w interpretacji wygenerowanego CIL i nie mogę uzyskać kilku szczegółów:
// [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
źródło
źródło
Odpowiedzi:
Operacja taka jak
a op= b;
jest równoważnaa = a op b;
. Przypisanie może być użyte jako instrukcja lub jako wyrażenie, podczas gdy jako wyrażenie daje przypisaną wartość. Twoje oświadczenie ...... może, ponieważ operator przypisania jest prawostronny, może być również zapisany jako
lub (rozwinięte)
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
Gdzie podczas całej oceny używane są stare wartości zaangażowanych zmiennych. Jest to szczególnie prawdziwe w przypadku wartości
k
(zobacz moją recenzję IL poniżej i link podany przez Wai Ha Lee). Dlatego nie otrzymujesz 70 + 40 (nowa wartośćk
) = 110, ale 70 + 10 (stara wartośćk
) = 80.Chodzi o to, że (według C # specyfikacji ) „Argumenty operacji w wyrażeniu ocenia się od lewej do prawej” (argumenty są zmienne
c
ik
w naszym przypadku). Jest to niezależne od pierwszeństwa i asocjatywności operatorów, które w tym przypadku narzucają kolejność wykonania od prawej do lewej. (Patrz komentarze Eric Lippert jest odpowiedzią na tej stronie).Teraz spójrzmy na IL. IL zakłada maszynę wirtualną opartą na stosie, tj. Nie używa rejestrów.
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)
Stos wygląda teraz następująco (od lewej do prawej; góra stosu jest po prawej)
IL_000b: add // pops the 2 top (right) positions, adds them and pushes the sum back
IL_000d: stloc.0 // k <-- 40
IL_000e: add
IL_0010: stloc.1 // c <-- 70
IL_0011: add
IL_0012: stloc.0 // k <-- 80
Zwróć na to uwagę
IL_000c: dup
,IL_000d: stloc.0
tj. Pierwsze przypisanie dok
mógł zostać zoptymalizowane z dala. Prawdopodobnie jest to robione dla zmiennych przez jitter podczas konwersji IL na kod maszynowy.Należy również zauważyć, że wszystkie wartości wymagane do obliczeń są umieszczane na stosie przed dokonaniem jakiegokolwiek przypisania lub są obliczane na podstawie tych wartości. Wartości przypisane (przez
stloc
) nigdy nie są ponownie używane podczas tej oceny.stloc
zdejmuje szczyt stosu.Wynik poniższego testu konsoli to (
Release
tryb z włączonymi optymalizacjami)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; }
źródło
k = 10 + (30 + (10 + 30)) = 80
a tac
końcowa wartość jest umieszczona w pierwszym nawiasie, czylic = 30 + (10 + 30) = 70
.k
jest lokalny, martwy magazyn jest prawie na pewno usuwany, jeśli optymalizacje są włączone, i zachowywany, jeśli nie są. Interesującym pytaniem jest to, czy jitter może usunąć martwy magazyn, jeślik
jest to pole, właściwość, miejsce w tablicy itd.; w praktyce uważam, że nie.k
jest to przypisane dwukrotnie, jeśli jest to właściwość.Po pierwsze, odpowiedzi Henka i Oliviera są poprawne; Chcę to wyjaśnić w nieco inny sposób. W szczególności chciałbym odnieść się do tego, co przedstawiłeś. Masz ten zestaw instrukcji:
int k = 10; int c = 30; k += c += k += c;
I wtedy błędnie dochodzisz do wniosku, że powinno to dać taki sam wynik jak ten zestaw instrukcji:
int k = 10; int c = 30; k += c; c += k; k += c;
Warto zobaczyć, jak się pomyliłeś i jak to zrobić dobrze. Właściwy sposób na rozbicie tego jest taki.
Najpierw przepisz najbardziej zewnętrzny + =
Po drugie, przepisz najbardziej zewnętrzny znak +. Mam nadzieję, że zgodzisz się, że x = y + z musi zawsze być tym samym, co „oszacuj y na tymczasowe, oszacuj z na tymczasowe, zsumuj tymczasowe, przypisz sumę do x” . Zróbmy to bardzo wyraźnie:
int t1 = k; int t2 = (c += k += c); k = t1 + t2;
Upewnij się, że jest to jasne, ponieważ jest to krok, który popełniłeś źle . Rozbijając złożone operacje na prostsze, musisz upewnić się, że robisz to powoli i ostrożnie oraz nie pomijaj poszczególnych kroków . Pomijanie kroków jest tym, gdzie popełniamy błędy.
OK, teraz ponownie, powoli i ostrożnie, podziel przypisanie na t2.
int t1 = k; int t2 = (c = c + (k += c)); k = t1 + t2;
Przypisanie przypisze tę samą wartość do t2, jaka jest przypisana do c, więc powiedzmy, że:
int t1 = k; int t2 = c + (k += c); c = t2; k = t1 + t2;
Świetny. Teraz podziel drugą linię:
int t1 = k; int t3 = c; int t4 = (k += c); int t2 = t3 + t4; c = t2; k = t1 + t2;
Świetnie, robimy postępy. Podziel przypisanie do t4:
int t1 = k; int t3 = c; int t4 = (k = k + c); int t2 = t3 + t4; c = t2; k = t1 + t2;
Teraz podziel trzecią linię:
int t1 = k; int t3 = c; int t4 = k + c; k = t4; int t2 = t3 + t4; c = t2; k = t1 + t2;
A teraz możemy spojrzeć na całość:
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
Więc kiedy skończymy, k wynosi 80, a c wynosi 70.
Teraz spójrzmy, jak to jest zaimplementowane w IL:
int t1 = k; int t3 = c; is implemented as ldloc.0 // stack slot 1 is t1 ldloc.1 // stack slot 2 is t3
Teraz jest to trochę trudne:
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
Mogliśmy zaimplementować powyższe jako
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
ale używamy sztuczki „dup”, ponieważ sprawia, że kod jest krótszy i łatwiejszy w przypadku jittera, i otrzymujemy ten sam wynik. Ogólnie rzecz biorąc, generator kodu C # stara się zachować jak najwięcej tymczasowych „efemerycznych” na stosie. Jeśli okaże się, że łatwiej śledzić IL mniej ephemerals skręcić optymalizacji off , a generator kodu będzie mniej agresywny.
Teraz musimy zrobić tę samą sztuczkę, aby uzyskać 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.
i w końcu:
k = t1 + t2; is implemented as add // stack slots 1 and 2 are t1 and t2. stloc.0 // Store the sum to k.
Ponieważ nie potrzebujemy kwoty do niczego innego, nie oszukujemy jej. Stos jest teraz pusty, a my jesteśmy na końcu instrukcji.
Morał tej historii jest taki: kiedy próbujesz zrozumieć skomplikowany program, zawsze wykonuj operacje pojedynczo . Nie idź na skróty; sprowadzą cię na manowce.
źródło
F(i) + G(i++) * H(i)
, Metoda F nazywa się stosując starą wartość I, a następnie metodą G jest wywoływana ze starą wartością i, a na koniec metoda H jest wywoływana z nową wartością i . Jest to oddzielne i niezwiązane z pierwszeństwem operatorów. " (Podkreślenie dodane.) Więc chyba się myliłem, mówiąc, że nigdzie nie ma miejsca, w którym „stara wartość jest używana”! Występuje w przykładzie. Ale bit normatywny to „od lewej do prawej”.+
, a wtedy otrzymasz+=
za darmo, ponieważx += y
jest zdefiniowany jakox = x + y
wyjątekx
jest oceniany tylko raz. Dzieje się tak niezależnie od tego, czy+
jest wbudowany, czy zdefiniowany przez użytkownika. A więc: spróbuj przeładować+
typ referencyjny i zobacz, co się stanie.Sprowadza się to do: czy pierwsza jest
+=
stosowana do oryginału,k
czy do wartości, która została obliczona bardziej w prawo?Odpowiedź jest taka, że chociaż przypisania są powiązane od prawej do lewej, operacje nadal są wykonywane od lewej do prawej.
Więc
+=
wykonuje najbardziej lewy10 += 70
.źródło
Wypróbowałem przykład z gcc i pgcc i otrzymałem 110. Sprawdziłem wygenerowany IR, a kompilator rozszerzył wyrażenie do:
k = 10; c = 30; k = c+k; c = c+k; k = c+k;
co wydaje mi się rozsądne.
źródło
w przypadku tego rodzaju przypisań łańcuchowych należy przypisać wartości zaczynając od najbardziej prawej strony. Musisz przypisać i obliczyć i przypisać go do lewej strony, i przejść przez to aż do ostatniego (przypisanie z lewej strony), Jasne, że jest obliczane jako k = 80.
źródło
Prosta odpowiedź: Zastąp zmienne wartościami i masz to:
int k = 10; int c = 30; k += c += k += c; 10 += 30 += 10 += 30 = 10 + 30 + 10 + 30 = 80 !!!
źródło
k = 10; m = (k += k) + k;
nie oznaczam = (10 + 10) + 10
. Języki z mutującymi wyrażeniami nie mogą być analizowane tak, jakby miały chętnie zastępowane wartości . Podstawianie wartości odbywa się w określonej kolejności w odniesieniu do mutacji i należy to wziąć pod uwagę.Możesz rozwiązać ten problem, licząc.
Istnieją dwa
c
s, a dwak
jest taka = 2c + 2k
A w konsekwencji operatorów języka
k
równa się również2c + 2k
To zadziała dla dowolnej kombinacji zmiennych w tym stylu łańcucha:
Więc
a = 2m + n + 3r
I
r
będzie równy temu samemu.Możesz obliczyć wartości innych liczb, obliczając tylko ich przypisanie po lewej stronie. Więc
m
jest równy2m + n
in
równyn + m
.To pokazuje, że
k += c += k += c;
jest inaczejk += c; c += k; k += c;
i dlatego otrzymujesz różne odpowiedzi.Niektórzy ludzie w komentarzach wydają się być zaniepokojeni, że możesz próbować nadmiernie uogólniać ten skrót na wszystkie możliwe typy dodatków. Dlatego wyjaśnię, że ten skrót ma zastosowanie tylko do tej sytuacji, tj. Łączenia w łańcuch dodawania przypisań dla wbudowanych typów liczb. Nie działa (koniecznie), jeśli dodasz inne operatory, np.
()
Lub+
, lub jeśli wywołasz funkcje lub jeśli nadpisałeś+=
, lub jeśli używasz czegoś innego niż podstawowe typy liczb. Ma to tylko pomóc w konkretnej sytuacji w pytaniu .źródło
x = 1;
iy = (x += x) + x;
czy twierdzisz, że „są trzy x, więc y jest równe3 * x
”? Ponieważy
jest równy4
w tym przypadku. A co zy = x + (x += x);
twoim twierdzeniem, że prawo algebraiczne „a + b = b + a” jest spełnione i to też jest 4? Ponieważ jest to 3. Niestety, C # nie przestrzega zasad algebry w liceum, jeśli w wyrażeniach występują efekty uboczne . C # przestrzega zasad algebry efektów ubocznych.