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??????
c#
cil
compound-assignment
Andrii Kotliarov
sumber
sumber
Jawaban:
Operasi seperti
a op= b;
itu setara dengana = a op b;
. Sebuah tugas dapat digunakan sebagai pernyataan atau sebagai ekspresi, sedangkan sebagai ekspresi itu menghasilkan nilai yang ditugaskan. Pernyataan Anda ...... bisa, karena operator penugasan adalah asosiatif-kanan, juga ditulis sebagai
atau (diperluas)
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
Dimana selama evaluasi keseluruhan digunakan nilai lama dari variabel yang terlibat. Hal ini terutama berlaku untuk nilai
k
(lihat ulasan saya tentang IL di bawah dan tautan yang disediakan Wai Ha Lee). Oleh karena itu, Anda tidak mendapatkan 70 + 40 (nilai baruk
) = 110, tetapi 70 + 10 (nilai lamak
) = 80.Intinya adalah bahwa (menurut spesifikasi C # ) "Operand dalam ekspresi dievaluasi dari kiri ke kanan" (operan adalah variabel
c
dank
dalam 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)
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
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.stloc
muncul di bagian atas tumpukan.Output dari pengujian konsol berikut adalah (
Release
mode dengan pengoptimalan aktif)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; }
sumber
k = 10 + (30 + (10 + 30)) = 80
danc
nilai akhir ditetapkan dalam tanda kurung pertama yaituc = 30 + (10 + 30) = 70
.k
bersifat 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 jikak
merupakan field, property, slot array, dan seterusnya; dalam praktiknya saya yakin tidak.k
ditugaskan dua kali jika itu adalah properti.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
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.
sumber
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".+
, dan kemudian Anda akan mendapatkan+=
gratis karenax += y
didefinisikan sebagaix = x + y
kecualix
dievaluasi hanya sekali. Itu benar terlepas dari apakah+
itu bawaan atau ditentukan pengguna. Jadi: coba overload+
pada jenis referensi dan lihat apa yang terjadi.Intinya adalah: apakah yang pertama
+=
diterapkan ke aslinyak
atau 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 mengeksekusi10 += 70
.sumber
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.
sumber
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.
sumber
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 !!!
sumber
k = 10; m = (k += k) + k;
tidak berartim = (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.Anda bisa menyelesaikannya dengan menghitung.
Ada dua
c
dan duak
jadia = 2c + 2k
Dan, sebagai konsekuensi dari operator bahasa,
k
juga sama2c + 2k
Ini akan berfungsi untuk kombinasi variabel apa pun dalam gaya rantai ini:
Begitu
a = 2m + n + 3r
Dan
r
akan sama.Anda dapat menghitung nilai bilangan lain hanya dengan menghitung hingga tugas paling kiri. Jadi
m
sama2m + n
dann
saman + m
.Ini menunjukkan bahwa
k += c += k += c;
berbeda dengank += 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 .sumber
x = 1;
dany = (x += x) + x;
Apakah pendapat Anda bahwa "ada tiga x sehingga y sama dengan3 * x
"? Karenay
sama dengan4
dalam kasus ini. Sekarang bagaimana dengany = 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.