Apakah ada penjelasan untuk operator sebaris di “k + = c + = k + = c;”?

89

Apa penjelasan dari hasil operasi berikut ini?

k += c += k += c;

Saya mencoba memahami hasil keluaran dari kode berikut:

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

dan saat ini saya kesulitan memahami mengapa hasil untuk "k" adalah 80. Mengapa menetapkan k = 40 tidak berfungsi (sebenarnya Visual Studio memberi tahu saya bahwa nilai itu tidak digunakan di tempat lain)?

Mengapa k 80 dan bukan 110?

Jika saya membagi operasi menjadi:

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

hasilnya k = 110.

Saya mencoba melihat-lihat CIL , tetapi saya tidak begitu ahli dalam menafsirkan CIL yang dihasilkan dan tidak bisa mendapatkan sedikit detail:

 // [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??????
Andrii Kotliarov
sumber
3
Anda mendapatkan hasil yang berbeda karena Anda membagi fungsinya, k + = c + = k + = c = 80 karena nilai k dan c tetap sama di semua penjumlahan, jadi k + = c + = k + = c sama hingga 10 + 30 + 10 + 30
João Paulo Amorim
78
Latihan yang menarik, tetapi, dalam praktiknya, jangan pernah menulis rangkaian kode seperti itu kecuali jika Anda ingin rekan kerja membenci Anda. :)
UnhandledExcepSean
3
@AndriiKotliarov karena k + = c + = k + = c adalah 10 + 30 + 10 + 30, jadi, K menerima semua nilai, dan C hanya mendapatkan 3 argumen terakhir 30 + 10 + 30 = 70
João Paulo Amorim
6
Juga layak dibaca - Jawaban Eric Lippert untuk Apa perbedaan antara i ++ dan ++ i?
Wai Ha Lee
34
"Dokter, dokter, sakit saat saya melakukan ini!" "Jadi, jangan LAKUKAN itu."
David Conrad

Jawaban:

104

Operasi seperti a op= b;itu setara dengan a = a op b;. Sebuah tugas dapat digunakan sebagai pernyataan atau sebagai ekspresi, sedangkan sebagai ekspresi itu menghasilkan nilai yang ditugaskan. Pernyataan Anda ...

k += c += k += c;

... bisa, karena operator penugasan adalah asosiatif-kanan, juga ditulis sebagai

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

atau (diperluas)

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

Dimana selama evaluasi keseluruhan digunakan nilai lama dari variabel yang terlibat. Hal ini terutama berlaku untuk nilaik (lihat ulasan saya tentang IL di bawah dan tautan yang disediakan Wai Ha Lee). Oleh karena itu, Anda tidak mendapatkan 70 + 40 (nilai baru k) = 110, tetapi 70 + 10 (nilai lama k) = 80.

Intinya adalah bahwa (menurut spesifikasi C # ) "Operand dalam ekspresi dievaluasi dari kiri ke kanan" (operan adalah variabel cdan kdalam kasus kami). Ini tidak tergantung pada prioritas dan asosiatif operator yang dalam hal ini menentukan perintah eksekusi dari kanan ke kiri. (Lihat komentar untuk jawaban Eric Lippert di halaman ini).


Sekarang mari kita lihat IL. IL mengasumsikan mesin virtual berbasis stack, yaitu tidak menggunakan register.

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)

Tumpukan sekarang terlihat seperti ini (dari kiri ke kanan; atas tumpukan di kanan)

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

Catat itu IL_000c: dup ,IL_000d: stloc.0 yaitu tugas pertama untukk , dapat dioptimalkan. Mungkin ini dilakukan untuk variabel oleh jitter saat mengubah IL ke kode mesin.

Perhatikan juga bahwa semua nilai yang diperlukan oleh penghitungan akan didorong ke tumpukan sebelum penugasan apa pun dibuat atau dihitung dari nilai-nilai ini. Nilai yang ditetapkan (oleh stloc) tidak pernah digunakan kembali selama evaluasi ini.stlocmuncul di bagian atas tumpukan.


Output dari pengujian konsol berikut adalah (Release mode dengan pengoptimalan aktif)

mengevaluasi k (10)
mengevaluasi c (30)
mengevaluasi k (10)
mengevaluasi c (30)
40 ditugaskan ke k
70 ditugaskan ke c
80 ditugaskan ke 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;
}
Olivier Jacot-Descombes
sumber
Anda dapat menambahkan hasil akhir dengan angka-angka dalam rumus untuk lebih lengkap: final adalah k = 10 + (30 + (10 + 30)) = 80dan cnilai akhir ditetapkan dalam tanda kurung pertama yaitu c = 30 + (10 + 30) = 70.
Franck
2
Memang jika kbersifat lokal maka penyimpanan yang mati hampir pasti dihapus jika pengoptimalan aktif, dan dipertahankan jika tidak. Sebuah pertanyaan yang menarik adalah apakah jitter diijinkan untuk menghilangkan penyimpanan yang mati jika kmerupakan field, property, slot array, dan seterusnya; dalam praktiknya saya yakin tidak.
Eric Lippert
Tes konsol dalam mode Rilis memang menunjukkan bahwa kditugaskan dua kali jika itu adalah properti.
Olivier Jacot-Descombes
26

Pertama, jawaban Henk dan Olivier benar; Saya ingin menjelaskannya dengan cara yang sedikit berbeda. Secara khusus, saya ingin membahas poin yang Anda buat ini. Anda memiliki kumpulan pernyataan ini:

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

Dan Anda kemudian salah menyimpulkan bahwa ini harus memberikan hasil yang sama seperti kumpulan pernyataan ini:

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

Adalah informatif untuk mengetahui bagaimana Anda melakukan kesalahan itu, dan bagaimana melakukannya dengan benar. Cara yang tepat untuk memecahnya adalah seperti ini.

Pertama, tulis ulang + = terluar

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

Kedua, tulis ulang + terluar. Saya harap Anda setuju bahwa x = y + z harus selalu sama dengan "evaluasi y untuk sementara, evaluasi z ke sementara, jumlahkan sementara, berikan jumlah ke x" . Jadi mari kita buat itu sangat eksplisit:

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

Pastikan itu jelas, karena ini adalah langkah yang Anda keliru . Saat memecah operasi yang rumit menjadi operasi yang lebih sederhana, Anda harus memastikan bahwa Anda melakukannya dengan lambat dan hati - hati serta tidak melewatkan langkah-langkah . Melompati langkah adalah saat kita membuat kesalahan.

Oke, sekarang uraikan tugas ke t2, lagi, perlahan dan hati-hati.

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

Tugas akan menetapkan nilai yang sama ke t2 seperti yang ditetapkan ke c, jadi katakanlah:

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

Bagus. Sekarang hancurkan baris kedua:

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

Bagus, kami membuat kemajuan. Pecahkan tugas ke t4:

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

Sekarang hancurkan baris ketiga:

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

Dan sekarang kita bisa melihat semuanya:

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

Jadi setelah kita selesai, k adalah 80 dan c adalah 70.

Sekarang mari kita lihat bagaimana ini diterapkan di IL:

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

Sekarang ini agak rumit:

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

Kami bisa menerapkan di atas sebagai

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

tetapi kami menggunakan trik "dup" karena itu membuat kode lebih pendek dan membuatnya lebih mudah di jitter, dan kami mendapatkan hasil yang sama. Secara umum, generator kode C # mencoba untuk menyimpan temporer "singkat" di stack sebanyak mungkin. Jika Anda merasa lebih mudah untuk mengikuti IL dengan ephemerals lebih sedikit, giliran optimasi off , dan kode generator akan kurang agresif.

Kita sekarang harus melakukan trik yang sama untuk mendapatkan 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.

dan akhirnya:

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

Karena kami tidak memerlukan jumlah untuk hal lain, kami tidak menipunya. Tumpukan sekarang kosong, dan kita berada di akhir pernyataan.

Moral dari cerita ini adalah: ketika Anda mencoba untuk memahami program yang rumit, selalu hancurkan operasi satu per satu . Jangan mengambil jalan pintas; mereka akan menyesatkan Anda.

Eric Lippert
sumber
3
@ OlivierJacot-Descombes: Baris spesifikasi yang relevan ada di bagian "Operator" dan bertuliskan "Operand dalam ekspresi dievaluasi dari kiri ke kanan. Misalnya, dalam F(i) + G(i++) * H(i), metode F dipanggil menggunakan nilai lama i, lalu metode G dipanggil dengan nilai lama i, dan akhirnya, metode H dipanggil dengan nilai baru i . Ini terpisah dari dan tidak terkait dengan prioritas operator. " (Penekanan ditambahkan.) Jadi saya kira saya salah ketika saya mengatakan tidak ada tempat di mana "nilai lama digunakan" terjadi! Itu terjadi dalam sebuah contoh. Tetapi bagian normatifnya adalah "kiri ke kanan".
Eric Lippert
1
Ini adalah mata rantai yang hilang. Intinya adalah kita harus membedakan antara urutan evaluasi operan dan prioritas operator . Evaluasi operand dari kiri ke kanan dan dalam kasus OP pelaksanaan operator dari kanan ke kiri.
Olivier Jacot-Descombes
4
@ OlivierJacot-Descombes: Itu benar sekali. Presedensi dan asosiativitas sama sekali tidak ada hubungannya dengan urutan subekspresi dievaluasi, selain fakta bahwa presedensi dan asosiativitas menentukan di mana batas subekspresi berada . Sub-ekspresi dievaluasi dari kiri ke kanan.
Eric Lippert
1
Ups sepertinya Anda tidak dapat membebani operator penugasan: /
johnny 5
1
@ johnny5: Itu benar. Tetapi Anda dapat membebani +, dan kemudian Anda akan mendapatkan +=gratis karena x += ydidefinisikan sebagai x = x + ykecuali xdievaluasi hanya sekali. Itu benar terlepas dari apakah +itu bawaan atau ditentukan pengguna. Jadi: coba overload +pada jenis referensi dan lihat apa yang terjadi.
Eric Lippert
14

Intinya adalah: apakah yang pertama +=diterapkan ke aslinya katau ke nilai yang dihitung lebih ke kanan?

Jawabannya adalah meskipun tugas mengikat dari kanan ke kiri, operasi masih berjalan dari kiri ke kanan.

Jadi yang paling kiri +=adalah mengeksekusi 10 += 70.

Henk Holterman
sumber
1
Ini menempatkannya dengan baik di kulit kacang.
Aganju
Ini sebenarnya adalah operan yang dievaluasi dari kiri ke kanan.
Olivier Jacot-Descombes
0

Saya mencoba contoh dengan gcc dan pgcc dan mendapatkan 110. Saya memeriksa IR yang mereka hasilkan, dan kompiler memperluas expr ke:

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

yang terlihat masuk akal bagi saya.

Brian Yang
sumber
-1

untuk tugas rantai semacam ini, Anda harus menetapkan nilai mulai dari sisi paling kanan. Anda harus menetapkan dan menghitung dan menugaskannya ke sisi kiri, dan teruskan ini sampai ke akhir (tugas paling kiri), Tentu itu dihitung sebagai k = 80.

Hasan Zeki Alp
sumber
Harap jangan memposting jawaban yang hanya menyatakan kembali apa yang telah dinyatakan oleh banyak jawaban lain.
Eric Lippert
-1

Jawaban sederhana: Ganti vars dengan nilai dan Anda mendapatkannya:

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!
Thomas Michael
sumber
Jawaban ini salah. Meskipun teknik ini berfungsi dalam kasus khusus ini, algoritme tersebut tidak berfungsi secara umum. Misalnya k = 10; m = (k += k) + k;tidak berarti m = (10 + 10) + 10. Bahasa dengan ekspresi yang bermutasi tidak dapat dianalisis seolah-olah memiliki substitusi nilai yang menarik . Substitusi nilai terjadi dalam urutan tertentu sehubungan dengan mutasi dan Anda harus memperhitungkannya.
Eric Lippert
-1

Anda bisa menyelesaikannya dengan menghitung.

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

Ada dua cdan dua kjadi

a = 2c + 2k

Dan, sebagai konsekuensi dari operator bahasa, kjuga sama2c + 2k

Ini akan berfungsi untuk kombinasi variabel apa pun dalam gaya rantai ini:

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

Begitu

a = 2m + n + 3r

Dan rakan sama.

Anda dapat menghitung nilai bilangan lain hanya dengan menghitung hingga tugas paling kiri. Jadi msama 2m + ndan nsama n + m.

Ini menunjukkan bahwa k += c += k += c;berbeda dengan k += c; c += k; k += c;dan oleh karena itu mengapa Anda mendapatkan jawaban yang berbeda.

Beberapa orang di komentar tampaknya khawatir bahwa Anda mungkin mencoba menggeneralisasi secara berlebihan dari pintasan ini ke semua kemungkinan jenis penambahan. Jadi, saya akan menjelaskan bahwa pintasan ini hanya berlaku untuk situasi ini, yaitu merangkai tugas penjumlahan bersama untuk tipe bilangan bawaan. Ini tidak (harus) berfungsi jika Anda menambahkan operator lain, misalnya ()atau +, atau jika Anda memanggil fungsi atau jika Anda telah mengganti +=, atau jika Anda menggunakan sesuatu selain tipe nomor dasar. Ini hanya dimaksudkan untuk membantu situasi tertentu dalam pertanyaan .

Matt Ellen
sumber
Ini tidak menjawab pertanyaan
johnny 5
@ johnny5 menjelaskan mengapa Anda mendapatkan hasil yang Anda dapatkan, yaitu karena itulah cara kerja matematika.
Matt Ellen
2
Matematika dan urutan operasi yang dievaulasi oleh compiler adalah dua hal yang berbeda. Di bawah logika Anda k + = c; c + = k; k + = c harus mengevaluasi hasil yang sama.
johnny 5
Tidak, johnny 5, bukan itu artinya. Secara matematis mereka adalah hal yang berbeda. Tiga operasi terpisah dievaluasi menjadi 3c + 2k.
Matt Ellen
2
Sayangnya solusi "aljabar" Anda hanya kebetulan benar. Teknik Anda tidak bekerja secara umum . Pertimbangkan x = 1;dan y = (x += x) + x;Apakah pendapat Anda bahwa "ada tiga x sehingga y sama dengan 3 * x"? Karena ysama dengan 4dalam kasus ini. Sekarang bagaimana dengan y = x + (x += x);pendapat Anda bahwa hukum aljabar "a + b = b + a" terpenuhi dan ini juga 4? Karena ini 3. Sayangnya, C # tidak mengikuti aturan aljabar sekolah menengah jika ada efek samping dalam ekspresi . C # mengikuti aturan aljabar efek samping.
Eric Lippert