Mengapa pernyataan penugasan mengembalikan nilai?

126

Ini diperbolehkan:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

Menurut pemahaman saya, penugasan s = ”Hello”;seharusnya hanya menyebabkan “Hello”ditugaskan s, tetapi operasi seharusnya tidak mengembalikan nilai apa pun. Jika itu benar, maka ((s = "Hello") != null)akan menghasilkan kesalahan, karena nullakan dibandingkan dengan apa pun.

Apa alasan di balik memungkinkan pernyataan tugas untuk mengembalikan nilai?

pengguna437291
sumber
44
Untuk referensi, perilaku ini didefinisikan dalam C # spec : "Hasil dari ekspresi penugasan sederhana adalah nilai yang ditetapkan untuk operan kiri. Hasilnya memiliki jenis yang sama dengan operan kiri dan selalu diklasifikasikan sebagai nilai."
Michael Petrotta
1
@ Skurmedel Saya bukan downvoter, saya setuju dengan Anda. Tapi mungkin karena itu dianggap pertanyaan retoris?
jsmith
Dalam tugas pascal / delphi: = mengembalikan apa-apa. saya membencinya.
Andrey
20
Standar untuk bahasa di cabang C. Tugas adalah ekspresi.
Hans Passant
"tugas ... seharusnya hanya menyebabkan" Hello "ditugaskan, tetapi operasi seharusnya tidak mengembalikan nilai apa pun" - Mengapa? "Apa alasan di balik memungkinkan pernyataan tugas untuk mengembalikan nilai?" - Karena ini berguna, seperti yang ditunjukkan oleh contoh Anda sendiri. (Yah, contoh kedua Anda konyol, tetapi while ((s = GetWord()) != null) Process(s);tidak).
Jim Balter

Jawaban:

160

Menurut pemahaman saya, tugas s = "Halo"; seharusnya hanya menyebabkan "Hello" ditugaskan, tetapi operasi seharusnya tidak mengembalikan nilai apa pun.

Pemahaman Anda 100% salah. Bisakah Anda menjelaskan mengapa Anda percaya hal yang salah ini?

Apa alasan di balik memungkinkan pernyataan tugas untuk mengembalikan nilai?

Pertama, pernyataan penugasan tidak menghasilkan nilai. Ekspresi penugasan menghasilkan nilai. Ekspresi penugasan adalah pernyataan hukum; hanya ada beberapa ekspresi yang merupakan pernyataan hukum dalam C #: menunggu ekspresi, contoh konstruksi, kenaikan, penurunan, pemanggilan dan penugasan ekspresi dapat digunakan di mana pernyataan diharapkan.

Hanya ada satu jenis ekspresi dalam C # yang tidak menghasilkan semacam nilai, yaitu, doa sesuatu yang diketikkan sebagai kembalian yang batal. (Atau, setara, menunggu tugas tanpa nilai hasil yang terkait.) Setiap jenis ekspresi lainnya menghasilkan nilai atau variabel atau referensi atau akses properti atau akses acara, dan sebagainya.

Perhatikan bahwa semua ekspresi yang sah sebagai pernyataan berguna untuk efek sampingnya . Itulah wawasan kunci di sini, dan saya pikir mungkin penyebab intuisi Anda bahwa tugas harus berupa pernyataan dan bukan ekspresi. Idealnya, kita memiliki satu efek samping per pernyataan, dan tidak ada efek samping dalam ekspresi. Ini adalah agak aneh bahwa kode sisi-mempengaruhi dapat digunakan dalam konteks ekspresi sama sekali.

Alasan di balik mengizinkan fitur ini adalah karena (1) sering nyaman dan (2) idiomatik dalam bahasa mirip-C.

Orang mungkin mencatat bahwa pertanyaannya telah diajukan: mengapa ini idiomatis dalam bahasa C-like?

Sayangnya, Dennis Ritchie tidak lagi dapat ditanyakan, tetapi tebakan saya adalah bahwa suatu tugas hampir selalu meninggalkan nilai yang baru saja ditetapkan dalam register. C adalah bahasa yang sangat "dekat dengan mesin". Tampaknya masuk akal dan sesuai dengan desain C bahwa ada fitur bahasa yang pada dasarnya berarti "tetap menggunakan nilai yang baru saja saya tetapkan". Sangat mudah untuk menulis generator kode untuk fitur ini; Anda tetap menggunakan register yang menyimpan nilai yang ditugaskan.

Eric Lippert
sumber
1
Tidak menyadari bahwa apa pun yang mengembalikan nilai (bahkan konstruksi contoh dianggap sebagai ekspresi)
user437291
9
@ user437291: Jika konstruksi instance bukan ekspresi, Anda tidak bisa melakukan apa pun dengan objek yang dibuat - Anda tidak bisa menugaskannya ke sesuatu, Anda tidak bisa meneruskannya ke suatu metode, dan Anda tidak bisa memanggil metode apa pun di atasnya. Akan sangat tidak berguna, bukan?
Timwi
1
Mengubah variabel sebenarnya "hanya" efek samping dari operator penugasan, yang, menciptakan ekspresi, menghasilkan nilai sebagai tujuan utamanya.
mk12
2
"Pemahamanmu 100% salah." - Meskipun OP menggunakan bahasa Inggris bukan yang terbaik, "pemahaman" nya adalah tentang apa yang seharusnya terjadi, menjadikannya sebuah opini, jadi itu bukan hal yang bisa "100% salah" . Beberapa perancang bahasa setuju dengan "harus" OP, dan membuat penugasan tidak memiliki nilai atau melarang mereka untuk menjadi subekspresi. Saya pikir itu adalah reaksi berlebihan terhadap =/ ==kesalahan ketik, yang mudah diatasi dengan melarang menggunakan nilai =kecuali tanda kurung. misalnya, if ((x = y))atau if ((x = y) == true)diizinkan tetapi if (x = y)tidak.
Jim Balter
"Tidak menyadari bahwa apa pun yang mengembalikan nilai ... dianggap sebagai ekspresi" - Hmm ... segala sesuatu yang mengembalikan nilai dianggap sebagai ekspresi - keduanya sebenarnya sama.
Jim Balter
44

Apakah Anda tidak memberikan jawabannya? Ini untuk mengaktifkan secara tepat jenis konstruksi yang telah Anda sebutkan.

Kasus umum di mana properti dari operator penugasan ini digunakan adalah membaca baris dari file ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Timwi
sumber
1
+1 Ini harus menjadi salah satu penggunaan paling praktis dari konstruk ini
Mike Burton
11
Saya benar-benar menyukainya untuk inisialisasi properti yang sangat sederhana, tetapi Anda harus berhati-hati dan menarik garis Anda untuk "sederhana" rendah agar mudah dibaca: return _myBackingField ?? (_myBackingField = CalculateBackingField());Jauh lebih sedikit sekam daripada memeriksa nol dan menetapkan.
Tom Mayfield
35

Penggunaan ekspresi penugasan favorit saya adalah untuk properti yang diinisialisasi dengan malas.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Nathan Baulch
sumber
5
Itulah sebabnya begitu banyak orang menyarankan ??=sintaksis.
konfigurator
27

Untuk satu, itu memungkinkan Anda untuk rantai tugas Anda, seperti dalam contoh Anda:

a = b = c = 16;

Untuk yang lain, ini memungkinkan Anda untuk menetapkan dan memeriksa hasil dalam satu ekspresi:

while ((s = foo.getSomeString()) != null) { /* ... */ }

Keduanya mungkin merupakan alasan yang meragukan, tetapi pasti ada orang yang menyukai konstruksi ini.

Michael Burr
sumber
5
Chaining +1 bukan fitur penting tetapi senang melihatnya "hanya bekerja" ketika banyak hal tidak.
Mike Burton
+1 untuk merantai juga; Saya selalu menganggap itu sebagai kasus khusus yang ditangani oleh bahasa daripada efek samping alami dari "ekspresi mengembalikan nilai".
Caleb Bell
2
Ini juga memungkinkan Anda untuk menggunakan penugasan sebagai imbalan:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz
14

Selain alasan yang telah disebutkan (tugas chaining, set-dan-uji dalam sementara loop, dll), untuk benar menggunakan usingpernyataan Anda membutuhkan fitur ini:

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

MSDN tidak menyarankan untuk mendeklarasikan objek sekali pakai di luar pernyataan menggunakan, karena ia akan tetap berada dalam ruang lingkup bahkan setelah dibuang (lihat artikel MSDN yang saya tautkan).

Martin Törnwall
sumber
4
Saya tidak berpikir ini benar-benar tugas chaining per se.
kenny
1
@kenny: Uh ... Tidak, tapi saya tidak pernah mengklaim itu juga. Seperti yang saya katakan, terlepas dari alasan yang telah disebutkan - yang mencakup penugasan penugasan - pernyataan penggunaan akan jauh lebih sulit untuk digunakan jika bukan karena fakta bahwa operator penugasan mengembalikan hasil operasi penugasan. Ini tidak benar-benar terkait dengan penugasan tugas sama sekali.
Martin Törnwall
1
Tetapi seperti yang Eric katakan di atas, "pernyataan tugas tidak mengembalikan nilai". Ini sebenarnya hanya sintaks bahasa dari pernyataan using, bukti bahwa orang tidak bisa menggunakan "ekspresi penugasan" dalam pernyataan using.
kenny
@kenny: Permintaan maaf, terminologi saya jelas salah. Saya menyadari bahwa pernyataan menggunakan dapat diimplementasikan tanpa bahasa memiliki dukungan umum untuk ekspresi penugasan yang mengembalikan nilai. Namun, menurut saya, itu akan sangat tidak konsisten.
Martin Törnwall
2
@kenny: Sebenarnya, seseorang dapat menggunakan pernyataan definisi-dan-penugasan, atau ekspresi apa pun di using. Semua ini adalah hukum: using (X x = new X()), using (x = new X()), using (x). Namun, dalam contoh ini, isi pernyataan menggunakan adalah sintaks khusus yang sama sekali tidak bergantung pada tugas mengembalikan nilai - Font font3 = new Font("Arial", 10.0f)bukan ekspresi dan tidak valid di tempat yang mengharapkan ekspresi.
konfigurator
10

Saya ingin menguraikan poin spesifik yang dibuat Eric Lippert dalam jawabannya dan menyoroti pada kesempatan tertentu yang sama sekali belum disentuh oleh orang lain. Eric berkata:

[...] tugas hampir selalu meninggalkan nilai yang baru saja ditugaskan dalam register.

Saya ingin mengatakan bahwa penugasan akan selalu meninggalkan nilai yang kami coba tetapkan untuk operan kiri kami. Bukan hanya "hampir selalu". Tetapi saya tidak tahu karena saya belum menemukan masalah ini dikomentari dalam dokumentasi. Secara teori mungkin ini merupakan prosedur yang diimplementasikan sangat efektif untuk "meninggalkan" dan tidak mengevaluasi kembali operan kiri, tetapi apakah itu efisien?

'Efisien' ya untuk semua contoh sejauh ini dibangun dalam jawaban utas ini. Tetapi efisien dalam hal properti dan pengindeks yang menggunakan aksesor dan atur? Tidak semuanya. Pertimbangkan kode ini:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

Di sini kita memiliki properti, yang bahkan bukan pembungkus untuk variabel pribadi. Setiap kali dipanggil dia akan kembali benar, setiap kali seseorang mencoba untuk menetapkan nilainya dia tidak akan melakukan apa pun. Jadi, setiap kali properti ini dievaluasi, ia harus jujur. Mari lihat apa yang terjadi:

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

Coba tebak apa yang dicetaknya? Mencetak Unexpected!!. Ternyata, set accessor memang dipanggil, yang tidak melakukan apa-apa. Namun setelah itu, accessor get tidak pernah dipanggil sama sekali. Tugas hanya meninggalkan falsenilai yang kami coba tetapkan untuk properti kami. Dan falsenilai ini adalah apa yang pernyataan if mengevaluasi.

Saya akan menyelesaikannya dengan contoh dunia nyata yang membuat saya meneliti masalah ini. Saya membuat pengindeks yang merupakan pembungkus yang nyaman untuk koleksi ( List<string>) yang dimiliki kelas saya sebagai variabel pribadi.

Parameter yang dikirim ke pengindeks adalah string, yang harus diperlakukan sebagai nilai dalam koleksi saya. Pengakses get hanya akan mengembalikan benar atau salah jika nilai itu ada dalam daftar atau tidak. Jadi get accessor adalah cara lain untuk menggunakan List<T>.Containsmetode ini.

Jika set accessor pengindeks dipanggil dengan string sebagai argumen dan operan yang tepat adalah bool true, ia akan menambahkan parameter itu ke daftar. Tetapi jika parameter yang sama dikirim ke accessor dan operan yang tepat adalah bool false, ia malah akan menghapus elemen dari daftar. Dengan demikian accessor set digunakan sebagai alternatif yang nyaman untuk keduanya List<T>.Adddan List<T>.Remove.

Saya pikir saya punya "API" yang rapi dan kompak yang membungkus daftar dengan logika saya sendiri diimplementasikan sebagai gateway. Dengan bantuan pengindeks saja aku bisa melakukan banyak hal dengan beberapa set penekanan tombol. Misalnya, bagaimana saya bisa mencoba menambahkan nilai ke daftar saya dan memverifikasi bahwa itu ada di sana? Saya pikir ini adalah satu-satunya baris kode yang diperlukan:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

Tapi seperti contoh saya sebelumnya menunjukkan, dapatkan accessor yang seharusnya melihat apakah nilainya benar-benar ada dalam daftar bahkan tidak dipanggil. The truenilai selalu tertinggal secara efektif menghancurkan logika apa pun saya telah diimplementasikan dalam mendapatkan accessor saya.

Martin Andersson
sumber
Jawaban yang sangat menarik. Dan itu membuat saya berpikir positif tentang pengembangan berbasis tes.
Elliot
1
Meskipun ini menarik, ini adalah komentar atas jawaban Eric, bukan jawaban untuk pertanyaan OP.
Jim Balter
Saya tidak akan mengharapkan ekspresi penugasan yang dicoba untuk pertama-tama mendapatkan nilai properti target. Jika variabel bisa berubah, dan jenisnya cocok, mengapa itu penting apa nilai lama itu? Tetapkan nilai baru. Saya bukan penggemar tugas dalam tes IF. Apakah ini mengembalikan true karena tugasnya berhasil, atau itu beberapa nilai misalnya bilangan bulat positif yang dipaksa menjadi benar, atau karena Anda menugaskan boolean? Terlepas dari implementasinya, logikanya tidak jelas.
Davos
6

Jika tugas tidak mengembalikan nilai, garis a = b = c = 16tidak akan berfungsi.

Juga bisa menulis hal-hal seperti while ((s = readLine()) != null)terkadang bermanfaat.

Jadi alasan di balik membiarkan tugas mengembalikan nilai yang diberikan, adalah untuk membiarkan Anda melakukan hal-hal itu.

sepp2k
sumber
Kerugian yang disebutkan tidak ada - saya datang ke sini bertanya-tanya mengapa tugas tidak dapat mengembalikan nol, agar tugas umum vs bug perbandingan 'jika (sesuatu = 1) {}' akan gagal dikompilasi daripada selalu kembali benar. Saya sekarang melihat bahwa itu akan membuat ... masalah lain!
Jon
1
@Jon bug itu dapat dengan mudah dicegah dengan menolak atau memperingatkan tentang =terjadi dalam ekspresi yang tidak di-kurung (tidak termasuk parens dari pernyataan if / while itu sendiri). gcc memberikan peringatan semacam itu dan karenanya pada dasarnya menghilangkan kelas bug ini dari program C / C ++ yang dikompilasi dengannya. Ini memalukan bahwa penulis kompiler lain telah menaruh perhatian sangat sedikit untuk ini dan beberapa ide bagus lainnya di gcc.
Jim Balter
4

Saya pikir Anda salah paham bagaimana parser akan menafsirkan sintaks itu. Tugas akan dievaluasi terlebih dahulu , dan hasilnya kemudian akan dibandingkan dengan NULL, yaitu pernyataan itu setara dengan:

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

Seperti yang telah ditunjukkan orang lain, hasil penugasan adalah nilai yang diberikan. Saya merasa sulit membayangkan keuntungan memiliki

((s = "Hello") != null)

dan

s = "Hello";
s != null;

tidak setara ...

Dan J
sumber
Meskipun Anda benar dari pengertian praktis bahwa dua baris Anda setara dengan pernyataan Anda bahwa tugas tidak mengembalikan nilai secara teknis tidak benar. Pemahaman saya adalah bahwa hasil penugasan adalah nilai (per spec C #), dan dalam arti itu tidak berbeda dengan mengatakan operator tambahan dalam ekspresi seperti 1 + 2 + 3
Steve Ellinger
3

Saya pikir alasan utama adalah kesamaan (disengaja) dengan C ++ dan C. Membuat operator assigment (dan banyak konstruksi bahasa lainnya) berperilaku seperti rekan-rekan C ++ mereka hanya mengikuti prinsip paling tidak mengejutkan, dan setiap programmer yang datang dari curly- lain bahasa braket dapat menggunakannya tanpa menghabiskan banyak pemikiran. Menjadi mudah diambil untuk programmer C ++ adalah salah satu tujuan desain utama untuk C #.

tammmer
sumber
2

Karena dua alasan Anda termasuk dalam posting Anda
1) sehingga Anda dapat melakukan a = b = c = 16
2) sehingga Anda dapat menguji apakah tugas berhasil if ((s = openSomeHandle()) != null)

JamesMLV
sumber
1

Fakta bahwa 'a ++' atau 'printf ("foo")' dapat berguna baik sebagai pernyataan mandiri atau sebagai bagian dari ekspresi yang lebih besar berarti bahwa C harus memungkinkan untuk kemungkinan bahwa hasil ekspresi mungkin atau mungkin tidak bekas. Mengingat hal itu, ada anggapan umum bahwa ekspresi yang mungkin berguna 'mengembalikan' nilai mungkin juga melakukannya. Penugasan tugas dapat sedikit "menarik" dalam C, dan bahkan lebih menarik dalam C ++, jika semua variabel yang dimaksud tidak memiliki tipe yang sama persis. Penggunaan seperti itu mungkin sebaiknya dihindari.

supercat
sumber
1

Contoh penggunaan yang bagus, saya menggunakan ini setiap saat:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once
Jazzcat
sumber
0

Keuntungan tambahan yang tidak saya lihat dalam jawaban di sini, adalah bahwa sintaks untuk tugas didasarkan pada aritmatika.

Sekarang x = y = b = c = 2 + 3berarti sesuatu yang berbeda dalam aritmatika daripada bahasa gaya-C; dalam aritmatika ini merupakan pernyataan, kami menyatakan bahwa x sama dengan y dll dan dalam bahasa C-style itu adalah instruksi yang membuat x sama dengan y dll setelah dieksekusi.

Ini mengatakan, masih ada hubungan yang cukup antara aritmatika dan kode yang tidak masuk akal untuk melarang apa yang alami dalam aritmatika kecuali ada alasan yang bagus. (Hal lain yang diambil oleh bahasa C-style dari penggunaan simbol equals adalah penggunaan == untuk perbandingan kesetaraan. Namun karena sebagian besar == mengembalikan nilai rangkaian seperti ini mustahil dilakukan.)

Jon Hanna
sumber
@ JimBalter Saya bertanya-tanya apakah, dalam waktu lima tahun, Anda mungkin benar-benar membuat argumen melawan.
Jon Hanna
@ JimBalter tidak, komentar kedua Anda sebenarnya adalah argumen balasan, dan yang sehat. Retort berpikir saya adalah karena komentar pertama Anda tidak. Namun, ketika belajar aritmatika, kita belajar a = b = cberarti adan bdan cmerupakan hal yang sama. Saat mempelajari bahasa gaya C, kami mempelajarinya setelahnya a = b = c, adan bsama seperti itu c. Tentu saja ada perbedaan dalam semantik, seperti jawaban saya sendiri, tetapi masih ketika saya masih anak-anak saya belajar untuk memprogram dalam bahasa yang memang digunakan =untuk tugas tetapi tidak membiarkan a = b = cini tampak tidak masuk akal bagi saya, meskipun ...
Jon Hanna
... tidak dapat dihindari karena bahasa itu juga digunakan =untuk perbandingan kesetaraan, jadi dalam hal a = b = citu harus berarti apa a = b == cartinya dalam bahasa gaya-C. Saya menemukan rantai yang diizinkan dalam C jauh lebih intuitif karena saya bisa menggambar analogi ke aritmatika.
Jon Hanna
0

Saya suka menggunakan nilai pengembalian tugas ketika saya perlu memperbarui banyak hal dan mengembalikan apakah ada perubahan:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

Berhati-hatilah. Anda mungkin berpikir Anda dapat mempersingkat ini menjadi ini:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

Tetapi ini benar-benar akan berhenti mengevaluasi atau pernyataan setelah menemukan yang pertama benar. Dalam hal ini berarti berhenti menetapkan nilai berikutnya setelah menetapkan nilai pertama yang berbeda.

Lihat https://dotnetfiddle.net/e05Rh8 untuk bermain-main dengan ini

Luke Kubat
sumber