Satır içi operatörler için "k + = c + = k + = c;" şeklinde bir açıklama var mı?


89

Aşağıdaki işlemin sonucunun açıklaması nedir?

k += c += k += c;

Aşağıdaki koddan çıktı sonucunu anlamaya çalışıyordum:

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

ve şu anda "k" için sonucun neden 80 olduğunu anlamakta zorlanıyorum. Neden k = 40 atamak işe yaramıyor (aslında Visual Studio bana bu değerin başka bir yerde kullanılmadığını söylüyor)?

Neden k 80 ve 110 değil?

İşlemi şu şekilde bölersem:

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

sonuç k = 110'dur.

CIL'e bakmaya çalışıyordum , ancak oluşturulan CIL'i yorumlama konusunda çok derin değilim ve birkaç ayrıntıya ulaşamıyorum:

 // [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
Fonksiyonu böldüğünüz için farklı sonuç elde edersiniz, k + = c + = k + = c = 80 çünkü k ve c'nin değerleri tüm toplamlarda aynı kalır, bu nedenle k + = c + = k + = c eşittir to 10 + 30 + 10 + 30
João Paulo Amorim

78
İlginç bir egzersiz, ancak pratikte, iş arkadaşlarınızın sizden nefret etmesini istemediğiniz sürece asla böyle kod zinciri yazmayın. :)
UnhandledExcepSean

3
@AndriiKotliarov, çünkü k + = c + = k + = c 10 + 30 + 10 + 30'dur, bu nedenle K tüm değerleri alır ve C yalnızca son 3 bağımsız değişkeni alır 30 + 10 + 30 = 70
João Paulo Amorim

6
Ayrıca okunmaya değer - Eric Lippert en cevabı için i arasındaki fark ++ ve ++ i nedir?
Wai Ha Lee

34
"Doktor, doktor, bunu yaptığımda acıyor!" "Öyleyse bunu YAPMA."
David Conrad

Yanıtlar:


104

Gibi bir işlem a op= b;eşdeğerdir a = a op b;. Bir atama, ifade veya ifade olarak kullanılabilirken, ifade olarak atanan değeri verir. İfadeniz ...

k += c += k += c;

... atama operatörü doğru ilişkilendirilebilir olduğundan, şu şekilde de yazılabilir:

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

veya (genişletilmiş)

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

Tüm değerlendirme sırasında ilgili değişkenlerin eski değerlerinin kullanıldığı yerde. Bu özellikle değeri için geçerlidir k(aşağıdaki IL incelememe ve Wai Ha Lee'nin sağladığı bağlantıya bakın ). Bu nedenle, 70 + 40 (yeni değeri k) = 110 değil, 70 + 10 (eski değeri k) = 80 alıyorsunuz .

Buradaki nokta (C # spesifikasyonuna göre ) "Bir ifadedeki işlenenler soldan sağa doğru değerlendirilir" (işlenenler değişkenlerdir cve kbizim durumumuzda). Bu, operatör önceliğinden ve bu durumda sağdan sola bir yürütme emri dikte eden ilişkiden bağımsızdır. ( Bu sayfadaki Eric Lippert'in cevabına verilen yorumlara bakın ).


Şimdi IL'ye bakalım. IL, yığın tabanlı bir sanal makine varsayar, yani kayıt kullanmaz.

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)

Yığın artık şöyle görünüyor (soldan sağa; yığının üstü sağ)

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

Unutmayın ki IL_000c: dup, IL_000d: stloc.0yani ilk atama k optimize edilebilir. Muhtemelen bu, IL'yi makine koduna dönüştürürken jitter tarafından değişkenler için yapılır.

Ayrıca, hesaplamanın gerektirdiği tüm değerlerin herhangi bir atama yapılmadan önce yığına itildiğini veya bu değerlerden hesaplandığını unutmayın. Atanan değerler (tarafından stloc) bu değerlendirme sırasında asla tekrar kullanılmaz. stlocyığının üst kısmını açar.


Aşağıdaki konsol testinin çıktısı ( Releaseoptimizasyonların açık olduğu mod)

k (10) 'u
değerlendirmek c (30)' u
değerlendirmek k (10) 'u
değerlendirmek
k
70' e atanan c (30) 40 'ı k' ye
atanmış

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;
}

Daha da eksiksiz olması için nihai sonucu formüldeki sayılarla birlikte ekleyebilirsiniz: sondur k = 10 + (30 + (10 + 30)) = 80ve bu cson değer olan ilk parantez içinde ayarlanır c = 30 + (10 + 30) = 70.
Franck

2
Nitekim kyerel ise , optimizasyonlar açıksa ölü depo neredeyse kesin olarak kaldırılır ve değilse korunur. İlginç bir soru, bir alan, özellik, dizi yuvası vb. İse , seğirmenin ölü depoyu eleme izni olup olmadığıdır k; pratikte öyle olmadığına inanıyorum.
Eric Lippert

Yayın kipindeki bir konsol testi, bir özellikse kbunun iki kez atandığını gösterir .
Olivier Jacot-Descombes

26

Öncelikle, Henk ve Olivier'in cevapları doğrudur; Bunu biraz farklı bir şekilde açıklamak istiyorum. Özellikle, belirttiğiniz bu noktaya değinmek istiyorum. Şu ifadelere sahipsiniz:

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

Ve sonra yanlış bir şekilde bunun şu ifadeler dizisiyle aynı sonucu vermesi gerektiği sonucuna varırsınız:

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

Bunu nasıl yanlış anladığınızı ve bunu nasıl doğru yapacağınızı görmek bilgilendirici. Bunu parçalamanın doğru yolu şudur.

İlk olarak, en dıştaki + =

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

İkinci olarak, en dıştaki + 'yı yeniden yazın. Umarım x = y + z'nin her zaman "y'yi geçici olarak değerlendir, z'yi geçici olarak değerlendir, geçici değerleri toplamı, toplamı x'e atama" ile aynı olması gerektiğini kabul edersiniz . Öyleyse bunu çok açık hale getirelim:

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

Bunun açık olduğundan emin olun, çünkü yanlış yaptığınız adım budur . Karmaşık işlemleri daha basit işlemlere böldüğünüzde, bunu yavaş ve dikkatli yaptığınızdan ve adımları atlamadığınızdan emin olmalısınız . Adımları atlamak, hata yaptığımız yerdir.

Tamam, şimdi tekrar, yavaş ve dikkatli bir şekilde t2'ye atamayı parçalayın.

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

Atama, c'ye atananla aynı değeri t2'ye atayacaktır, öyleyse şunu söyleyelim:

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

Harika. Şimdi ikinci satırı parçalayın:

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

Harika, ilerleme kaydediyoruz. Atamayı t4'e ayırın:

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

Şimdi üçüncü satırı parçalayın:

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

Ve şimdi her şeye bakabiliriz:

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

Yani işimiz bittiğinde k 80 ve c 70'tir.

Şimdi bunun bilgi düzeyinde nasıl uygulandığına bakalım:

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

Şimdi bu biraz aldatıcı:

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

Yukarıdakileri şu şekilde uygulayabilirdik:

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

ama biz "dup" hilesini kullanırız çünkü kodu kısaltır ve seğirmeyi kolaylaştırır ve aynı sonucu alırız. Genel olarak, C # kod üreteci, yığın üzerinde mümkün olduğunca geçici "geçici" değerleri tutmaya çalışır. IL'yi daha az kısa ömürlü takip etmeyi daha kolay bulursanız, optimizasyonları kapatın ; kod oluşturucu daha az agresif olacaktır.

Şimdi c elde etmek için aynı numarayı yapmalıyız:

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.

ve sonunda:

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

Başka hiçbir şey için meblağa ihtiyacımız olmadığından, onu kopyalamıyoruz. Yığın artık boş ve açıklamanın sonundayız.

Hikayenin ahlakı şudur: Karmaşık bir programı anlamaya çalışırken, her zaman işlemleri birer birer parçalayın . Kestirme yollara gitmeyin; Seni saptırırlar.


3
@ OlivierJacot-Descombes: spec ilgili hat "Operatörler" ve soldan sağa doğru bir ifadede işleneninin" yazan bölümünde örneğin, in. F(i) + G(i++) * H(i), Yöntem F'ye i eski değerini kullanarak denir, daha sonra Yöntem G, i'nin eski değeriyle çağrılır ve son olarak, yöntem H, i'nin yeni değeriyle çağrılır . Bu, operatör önceliğinden ayrıdır ve bununla ilgisi yoktur. " (Vurgu eklendi.) Yani "eski değerin kullanıldığı" hiçbir yerde olmadığını söylediğimde yanılmışım sanırım! Bir örnekte ortaya çıkar. Ancak normatif kısım "soldan sağa" dır.
Eric Lippert

1
Bu kayıp halka idi. İşin özü, işlenen değerlendirme sırası ile operatör önceliği arasında ayrım yapmamız gerektiğidir . Operand değerlendirmesi soldan sağa ve OP durumunda operatör yürütmesi sağdan sola doğru gider.
Olivier Jacot-Descombes

4
@ OlivierJacot-Descombes: Bu kesinlikle doğru. Öncelik ve ilişkilendirilebilirliğin , alt ifade sınırlarının nerede olduğunu belirleyen öncelik ve ilişkisellik olgusu dışında, alt ifadelerin değerlendirildiği sırayla hiçbir ilgisi yoktur . Alt ifadeler soldan sağa doğru değerlendirilir.
Eric Lippert

1
Ooops, atama operatörlerini aşırı yükleyemezsiniz gibi görünüyor: /
johnny 5

1
@ johnny5: Bu doğru. Ama aşırı yükleyebilirsiniz +ve o zaman +=ücretsiz alırsınız çünkü hariç x += ytanımlanır, sadece bir kez değerlendirilir. Bu , yerleşik veya kullanıcı tanımlı olup olmadığına bakılmaksızın doğrudur . Yani: bir referans türüne aşırı yükleme yapmayı deneyin ve ne olduğunu görün. x = x + yx++
Eric Lippert

14

Kaynaklar şu şekildedir: İlk önce +=orijinale kmi uygulanmıştır yoksa daha sağa hesaplanan değere mi?

Cevap, atamalar sağdan sola bağlansa da, işlemler hala soldan sağa doğru ilerlemektedir.

Yani en soldaki +=çalıştırılıyor 10 += 70.


1
Bu, onu bir ceviz kabuğuna güzel bir şekilde yerleştirir.
Aganju

Aslında soldan sağa doğru değerlendirilen işlenenlerdir.
Olivier Jacot-Descombes

0

Örneği gcc ve pgcc ile denedim ve 110 aldım. Oluşturdukları IR'yi kontrol ettim ve derleyici ifadeyi şu şekilde genişletti:

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

bu bana makul görünüyor.


-1

bu tür zincir atamaları için değerleri en sağ taraftan başlayarak atamanız gerekir. Bunu sol tarafa atamalı, hesaplamalı ve atamalısınız ve bunu sonuna kadar devam ettirmelisiniz (en soldaki atama), Elbette k = 80 olarak hesaplanır.


Lütfen diğer yanıtların halihazırda ifade ettiklerini basitçe yeniden belirten yanıtlar göndermeyin.
Eric Lippert

-1

Basit cevap: Değişkenleri değerlerle değiştirin ve anladınız:

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

Bu cevap yanlış. Bu teknik bu özel durumda işe yarasa da, bu algoritma genel olarak çalışmaz. Mesela k = 10; m = (k += k) + k;demek değil m = (10 + 10) + 10. Değişen ifadelere sahip diller, istekli bir değer ikamesi varmış gibi analiz edilemez . Değer ikamesi , mutasyonlara göre belirli bir sırada gerçekleşir ve bunu hesaba katmalısınız.
Eric Lippert

-1

Bunu sayarak çözebilirsiniz.

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

İki cs ve iki ks var

a = 2c + 2k

Ve dilin operatörlerinin bir sonucu olarak, k da eşittir2c + 2k

Bu, bu zincir tarzındaki herhangi bir değişken kombinasyonu için çalışacaktır:

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

Yani

a = 2m + n + 3r

Ve r aynı olacak.

Diğer sayıların değerlerini, yalnızca en soldaki atamalarına kadar hesaplayarak hesaplayabilirsiniz. Yani meşittir 2m + nve neşittir n + m.

Bu, bunun k += c += k += c;farklı olduğunu k += c; c += k; k += c;ve dolayısıyla neden farklı yanıtlar aldığınızı gösterir.

Yorumlardaki bazı kişiler, bu kısayoldan tüm olası ekleme türlerine aşırı genelleme yapmaya çalışabileceğiniz konusunda endişeli görünüyor. Bu nedenle, bu kısayolun yalnızca bu durum için geçerli olduğunu açıklığa kavuşturacağım, yani yerleşik sayı türleri için ek atamaları zincirleme. Başka operatörler eklerseniz, örneğin ()veya +, veya işlevleri çağırırsanız veya geçersiz kılarsanız +=veya temel numara türlerinden başka bir şey kullanıyorsanız (mutlaka) çalışmaz . Yalnızca sorudaki belirli duruma yardımcı olmak içindir .


Bu soruya cevap vermiyor
johnny 5

@ johnny5 neden aldığınız sonucu aldığınızı açıklıyor, yani matematik böyle işliyor.
Matt Ellen

2
Matematik ve bir derleyicinin bir ifadeyi değerlendirdiği işlem sıraları iki farklı şeydir. Mantığınız altında k + = c; c + = k; k + = c aynı sonucu değerlendirmelidir.
johnny 5

Hayır, Johnny 5, anlamı bu değil. Matematiksel olarak farklı şeylerdir. Üç ayrı işlem 3c + 2k olarak değerlendirilir.
Matt Ellen

2
Maalesef sizin "cebirsel" çözümünüz sadece tesadüfen doğrudur. Tekniğiniz genel olarak çalışmıyor . Bir düşünün x = 1;ve y = (x += x) + x;"üç x var ve bu nedenle y eşittir 3 * x" iddianız mı? Çünkü bu durumda yeşittir 4. Şimdi ne olacaky = x + (x += x); , cebir yasasının "a + b = b + a" yerine getirildiği ve bu da 4 olduğu iddiasına ? Çünkü bu 3'tür. Maalesef, ifadelerde yan etkiler varsa C # lise cebir kurallarına uymaz . C #, cebiri etkileyen bir yanın kurallarını izler.
Eric Lippert
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.