Mengapa struct yang bisa berubah "jahat"?


485

Setelah diskusi di SO, saya sudah membaca beberapa kali komentar bahwa struct yang bisa berubah adalah "jahat" (seperti dalam jawaban untuk pertanyaan ini ).

Apa masalah aktual dengan mutabilitas dan struct di C #?


12
Mengklaim struct yang bisa berubah adalah jahat sama seperti mengklaim bisa berubah int, bools, dan semua tipe nilai lainnya adalah jahat. Ada kasus-kasus untuk berubah-ubah dan untuk tidak berubah. Kasus-kasus tersebut bergantung pada peran yang dimainkan data, bukan jenis alokasi / pembagian memori.
Slipp D. Thompson

41
@slipp intdan boolyang tidak bisa berubah ..
Blorgbeard keluar

2
... .-syntax, membuat operasi dengan data yang diketik ulang dan data yang diketik nilai terlihat sama meskipun mereka berbeda nyata. Ini adalah kesalahan properti C #, bukan struct — beberapa bahasa menawarkan a[V][X] = 3.14sintaks alternatif untuk bermutasi di tempat. Dalam C #, Anda sebaiknya menawarkan metode mutator struct-member seperti 'MutateV (Action <ref Vector2> mutator) `dan menggunakannya seperti a.MutateV((v) => { v.X = 3; }) (contohnya terlalu disederhanakan karena keterbatasan C # mengenai refkata kunci, tetapi dengan beberapa solusi harus dimungkinkan) .
Slipp D. Thompson

2
@ Slipp Yah, saya pikir persis sebaliknya tentang jenis struct ini. Mengapa Anda berpikir bahwa struct yang sudah diimplementasikan di perpustakaan .NET, seperti DateTime atau TimeSpan (yang serupa) tidak dapat diubah? Mungkin bisa berguna untuk mengubah hanya satu anggota dari struct seperti itu, tetapi itu terlalu merepotkan, menyebabkan terlalu banyak masalah. Sebenarnya Anda salah tentang apa yang dihitung prosesor, karena C # tidak dapat dikompilasi ke assembler, ia mengkompilasi ke IL. Dalam IL (asalkan kita sudah memiliki variabel bernama x) operasi tunggal ini adalah 4 instruksi: ldloc.0(memuat variabel 0-indeks ke ...
Sushi271

1
... Tipe. Tadalah tipe. Ref hanyalah kata kunci yang membuat variabel diteruskan ke metode itu sendiri, bukan salinannya. Ini juga masuk akal untuk jenis referensi, karena kita dapat mengubah variabel , yaitu referensi di luar metode akan menunjuk ke objek lain setelah diubah dalam metode. Karena ref Tbukan tipe, tetapi mode meneruskan parameter metode, Anda tidak bisa memasukkannya ke dalam <>, karena hanya tipe yang bisa diletakkan di sana. Jadi itu tidak benar. Mungkin akan lebih mudah untuk melakukannya, mungkin tim C # dapat membuat ini untuk beberapa versi baru, tetapi sekarang mereka sedang mengerjakan beberapa ...
Sushi271

Jawaban:


290

Structs adalah tipe nilai yang berarti mereka disalin ketika mereka diedarkan.

Jadi, jika Anda mengubah salinan Anda hanya mengubah salinan itu, bukan yang asli dan tidak ada salinan lain yang mungkin ada.

Jika struct Anda tidak dapat diubah maka semua salinan otomatis yang dihasilkan dari diteruskan oleh nilai akan sama.

Jika Anda ingin mengubahnya, Anda harus melakukannya secara sadar dengan membuat instance baru dari struct dengan data yang dimodifikasi. (bukan salinan)


82
"Jika struct Anda tidak dapat diubah maka semua salinan akan sama." Tidak, itu berarti Anda harus secara sadar membuat salinan jika Anda menginginkan nilai yang berbeda. Ini berarti Anda tidak akan ketahuan memodifikasi salinan berpikir Anda memodifikasi yang asli.
Lucas

19
@Lucas Saya pikir Anda berbicara tentang jenis salinan yang berbeda. Saya berbicara tentang salinan otomatis yang dibuat sebagai hasil dari nilai yang diteruskan, 'Salinan yang dibuat secara sadar' Anda berbeda dengan tujuan Anda tidak membuatnya secara tidak sengaja dan tidak benar-benar salinan itu adalah instans baru yang disengaja yang berisi data yang berbeda.
trampster

3
Hasil edit Anda (16 bulan kemudian) membuatnya sedikit lebih jelas. Saya masih bertahan "(struct tidak berubah) berarti Anda tidak akan ketahuan memodifikasi salinan berpikir Anda memodifikasi yang asli", meskipun.
Lucas

6
@Lucas: Bahaya membuat salinan struct, memodifikasinya, dan entah bagaimana berpikir seseorang memodifikasi yang asli (ketika fakta bahwa seseorang menulis bidang struct membuat jelas fakta bahwa seseorang hanya menulis salinan seseorang) tampaknya cukup kecil dibandingkan dengan bahaya bahwa seseorang yang memegang objek kelas sebagai alat untuk menyimpan informasi yang terkandung di dalamnya akan bermutasi objek untuk memperbarui informasinya sendiri dan dalam proses merusak informasi yang dipegang oleh beberapa objek lain.
supercat

2
Paragraf ketiga terdengar salah atau tidak jelas. Jika struct Anda tidak dapat diubah maka Anda tidak akan dapat memodifikasi bidangnya atau bidang salinan yang dibuat. "Jika Anda ingin mengubahnya Anda harus ..." yang menyesatkan juga, Anda tidak dapat mengubah itu pernah , tidak sadar atau tidak sadar. Membuat contoh baru di mana data yang Anda inginkan tidak ada hubungannya dengan salinan asli selain memiliki struktur data yang sama.
Saeb Amini

167

Mulai dari mana ;-p

Blog Eric Lippert selalu baik untuk kutipan:

Ini adalah alasan lain mengapa tipe nilai yang bisa berubah itu jahat. Usahakan untuk selalu membuat tipe nilai tidak berubah.

Pertama, Anda cenderung kehilangan perubahan dengan mudah ... misalnya, mengeluarkan beberapa hal dari daftar:

Foo foo = list[0];
foo.Name = "abc";

apa yang berubah? Tidak ada yang berguna ...

Sama dengan properti:

myObj.SomeProperty.Size = 22; // the compiler spots this one

memaksa Anda untuk melakukan:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

kurang kritis, ada masalah ukuran; objek yang bisa berubah cenderung memiliki banyak properti; namun jika Anda memiliki struct dengan dua ints, a string, a DateTimedan a bool, Anda dapat dengan cepat membakar banyak memori. Dengan kelas, beberapa penelepon dapat membagikan referensi ke instance yang sama (referensi kecil).


5
Ya, tapi kompiler itu bodoh sekali. Tidak membiarkan tugas kepada anggota properti-struct adalah IMHO keputusan desain bodoh, karena yang diperbolehkan untuk ++operator. Dalam hal ini, kompiler hanya menulis tugas eksplisit itu sendiri daripada bergegas programmer.
Konrad Rudolph

12
@Konrad: myObj.SomeProperty.Size = 22 akan memodifikasi COPY dari myObj.SomeProperty. Kompiler menyelamatkan Anda dari bug yang jelas. Dan TIDAK diizinkan untuk ++.
Lucas

@ Lucas: Ya, kompiler Mono C # tentu memungkinkan - karena saya tidak punya Windows, saya tidak bisa memeriksa kompiler Microsoft.
Konrad Rudolph

6
@ Konrad - dengan satu tipuan yang kurang itu seharusnya bekerja; itu adalah "bermutasi nilai sesuatu yang hanya ada sebagai nilai sementara pada stack dan yang akan menguap menjadi ketiadaan" yang merupakan kasus yang diblokir.
Marc Gravell

2
@Marc Gravell: Di bagian kode sebelumnya, Anda berakhir dengan "Foo" yang namanya "abc" dan atribut lainnya adalah milik Daftar [0], tanpa Daftar [0] yang mengganggu. Jika Foo adalah kelas, perlu untuk mengkloningnya dan kemudian mengubah salinannya. Menurut saya, masalah besar dengan perbedaan nilai-tipe vs kelas adalah penggunaan "." operator untuk dua tujuan. Jika saya memiliki pemabuk saya, kelas dapat mendukung keduanya "." dan "->" untuk metode dan properti, tetapi semantik normal untuk "." properti akan membuat instance baru dengan bidang yang sesuai dimodifikasi.
supercat

71

Saya tidak akan mengatakan yang jahat, tetapi bisa berubah-ubah sering kali merupakan pertanda kerinduan para programmer untuk memberikan fungsionalitas yang maksimal. Pada kenyataannya, ini seringkali tidak diperlukan dan pada gilirannya, membuat antarmuka lebih kecil, lebih mudah digunakan dan lebih sulit untuk menggunakan yang salah (= lebih kuat).

Salah satu contohnya adalah konflik baca / tulis dan tulis / tulis dalam kondisi lomba. Ini tidak dapat terjadi dalam struktur yang tidak dapat diubah, karena penulisan bukanlah operasi yang valid.

Juga, saya mengklaim bahwa mutabilitas hampir tidak pernah benar-benar diperlukan , programmer hanya berpikir bahwa mungkin di masa depan. Misalnya, tidak masuk akal untuk mengubah tanggal. Sebaliknya, buat tanggal baru berdasarkan tanggal yang lama. Ini adalah operasi yang murah, jadi kinerja bukan pertimbangan.


1
Eric Lippert mengatakan mereka ... lihat jawabanku.
Marc Gravell

42
Meskipun saya menghormati Eric Lippert, dia bukan Tuhan (atau setidaknya belum). Posting blog yang Anda tautkan dan posting Anda di atas adalah argumen yang masuk akal untuk membuat struct berubah sebagai hal yang biasa tetapi mereka sebenarnya sangat lemah sebagai argumen untuk tidak pernah menggunakan struct yang bisa berubah. Namun, pos ini adalah +1.
Stephen Martin

2
Berkembang dalam C #, Anda biasanya membutuhkan kemampuan berubah dari waktu ke waktu - terutama dengan Model Bisnis Anda, di mana Anda ingin streaming dll untuk bekerja dengan lancar dengan solusi yang ada. Saya menulis artikel tentang cara bekerja dengan data yang bisa berubah DAN tidak berubah, menyelesaikan sebagian besar masalah seputar mutabilitas (saya harap): rickyhelgesson.wordpress.com/2012/07/17/…
Ricky Helgesson

3
@StephenMartin: Structs yang merangkum nilai tunggal seringkali harus tidak berubah, tetapi struct sejauh ini merupakan media terbaik untuk mengenkapsulasi set variabel independen tetapi terkait (seperti koordinat X dan Y suatu titik) yang tidak memiliki "identitas" sebagai kelompok. Struct yang digunakan untuk yang tujuan umumnya harus mengekspos variabel mereka sebagai bidang publik. Saya akan mempertimbangkan gagasan bahwa lebih tepat menggunakan kelas daripada struct untuk tujuan seperti itu menjadi salah. Kelas yang tidak dapat berubah seringkali kurang efisien, dan kelas yang dapat berubah sering memiliki semantik yang mengerikan.
supercat

2
@StephenMartin: Pertimbangkan, misalnya, metode atau properti yang seharusnya mengembalikan keenam floatkomponen transformasi grafik. Jika metode seperti itu mengembalikan struct bidang terbuka dengan enam komponen, jelas bahwa memodifikasi bidang struct tidak akan mengubah objek grafis dari mana ia diterima. Jika metode seperti itu mengembalikan objek kelas yang bisa berubah, mungkin mengubah propertinya akan mengubah objek grafis yang mendasarinya dan mungkin tidak - tidak ada yang tahu.
supercat

48

Struct yang bisa berubah tidak jahat.

Mereka mutlak diperlukan dalam keadaan kinerja tinggi. Misalnya ketika garis cache dan atau pengumpulan sampah menjadi hambatan.

Saya tidak akan menyebut penggunaan struct tidak berubah dalam kasus-kasus penggunaan yang benar-benar valid ini "jahat".

Saya dapat melihat poin bahwa sintaksis C # tidak membantu membedakan akses anggota dari tipe nilai atau tipe referensi, jadi saya semua lebih memilih struct yang tidak dapat diubah, yang menegakkan immutabilitas, lebih dari struct yang bisa berubah.

Namun, alih-alih hanya memberi label struct yang tidak dapat diubah sebagai "jahat", saya akan menyarankan untuk merangkul bahasa dan menganjurkan aturan praktis yang lebih bermanfaat dan konstruktif.

Misalnya: "struct adalah tipe nilai, yang disalin secara default. Anda perlu referensi jika Anda tidak ingin menyalinnya" atau "coba bekerja dengan struct yang hanya dibaca dulu" .


8
Saya juga berpendapat bahwa jika seseorang ingin mengencangkan set variabel tetap bersama dengan lakban sehingga nilai-nilai mereka dapat diproses atau disimpan baik secara terpisah atau sebagai satu unit, akan jauh lebih masuk akal untuk meminta kompiler untuk mengencangkan set tetap dari variabel bersama (yaitu mendeklarasikan structdengan bidang publik) daripada untuk mendefinisikan kelas yang dapat digunakan, dengan canggung, untuk mencapai tujuan yang sama, atau untuk menambahkan sekelompok sampah ke struct untuk membuatnya meniru kelas seperti itu (daripada memilikinya berperilaku seperti seperangkat variabel terjebak bersama dengan lakban, yang adalah apa yang sebenarnya diinginkan di tempat pertama)
supercat

41

Bangunan dengan bidang atau properti yang bisa berubah publik tidak jahat.

Metode Struct (berbeda dari setter properti) yang bermutasi "ini" agak jahat, hanya karena .net tidak menyediakan cara untuk membedakan mereka dari metode yang tidak. Metode Struct yang tidak bermutasi "ini" harus tidak dapat dimasuki bahkan pada struct read-only tanpa perlu menyalin defensif. Metode yang bermutasi "ini" tidak boleh tidak bisa disangkal sama sekali pada struct read-only. Karena .net tidak ingin melarang metode struct yang tidak mengubah "ini" agar tidak dipanggil pada struct read-only, tetapi tidak ingin memperbolehkan struct read-only dimutasi, defensif menyalin struct di read- hanya konteks, bisa dibilang mendapatkan yang terburuk dari kedua dunia.

Meskipun ada masalah dengan penanganan metode yang bermutasi sendiri dalam konteks baca-saja, namun, struktur yang dapat berubah sering menawarkan semantik yang jauh lebih unggul daripada jenis kelas yang dapat berubah. Pertimbangkan tiga tanda tangan metode berikut:

struct PointyStruct {public int x, y, z;};
kelas PointyClass {public int x, y, z;};

membatalkan Method1 (PointyStruct foo);
membatalkan Method2 (ref PointyStruct foo);
membatalkan Method3 (PointyClass foo);

Untuk setiap metode, jawab pertanyaan berikut:

  1. Dengan asumsi metode ini tidak menggunakan kode "tidak aman", mungkinkah itu memodifikasi foo?
  2. Jika tidak ada referensi luar ke 'foo' sebelum metode dipanggil, bisakah referensi luar ada setelah?

Jawaban:

Pertanyaan 1::
Method1()tidak (maksud jelas)
Method2() : ya (niat jelas)
Method3() : ya (niat tidak pasti)
Pertanyaan 2
Method1():: tidak
Method2(): tidak (kecuali tidak aman)
Method3() : ya

Method1 tidak dapat memodifikasi foo, dan tidak pernah mendapatkan referensi. Method2 mendapatkan referensi berumur pendek ke foo, yang dapat digunakan memodifikasi bidang foo beberapa kali, dalam urutan apa pun, sampai kembali, tetapi tidak dapat bertahan referensi itu. Sebelum Method2 kembali, kecuali ia menggunakan kode yang tidak aman, setiap dan semua salinan yang mungkin telah dibuat dari referensi 'foo' akan hilang. Method3, tidak seperti Method2, mendapatkan referensi foo yang bisa dibagi-bagi dengan foo, dan tidak ada yang tahu apa yang mungkin dilakukan dengan itu. Itu mungkin tidak mengubah foo sama sekali, mungkin mengubah foo dan kemudian kembali, atau mungkin memberikan referensi ke foo ke utas lain yang mungkin memutasinya dengan cara yang sewenang-wenang di waktu mendatang yang sewenang-wenang.

Susunan struktur menawarkan semantik yang indah. Diberikan RectArray [500] dari tipe Rectangle, jelas dan jelas bagaimana cara menyalin elemen 123 ke elemen 456 dan kemudian beberapa saat kemudian mengatur lebar elemen 123 hingga 555, tanpa elemen 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Lebar = 555; ". Mengetahui bahwa Rectangle adalah sebuah struct dengan bidang bilangan bulat yang disebut Width akan memberi tahu semua orang yang perlu tahu tentang pernyataan di atas.

Sekarang misalkan RectClass adalah kelas dengan bidang yang sama dengan Rectangle dan seseorang ingin melakukan operasi yang sama pada RectClassArray [500] dari jenis RectClass. Mungkin array seharusnya menampung 500 referensi yang tidak dapat diinisialisasi untuk objek RectClass yang bisa diubah. dalam kasus itu, kode yang tepat akan menjadi sesuatu seperti "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Mungkin array diasumsikan memiliki instance yang tidak akan berubah, sehingga kode yang tepat akan lebih seperti "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321] ]); RectClassArray [321] .X = 555; " Untuk mengetahui apa yang seharusnya dilakukan, kita harus tahu lebih banyak tentang RectClass (mis. Apakah itu mendukung copy constructor, metode copy-from, dll. ) dan tujuan penggunaan array. Tidak ada yang dekat sebersih menggunakan struct.

Yang pasti, sayangnya tidak ada cara yang bagus untuk kelas kontainer selain array untuk menawarkan semantik bersih dari array struct. Yang terbaik bisa dilakukan, jika seseorang ingin koleksi diindeks dengan misalnya string, mungkin akan menawarkan metode "ActOnItem" generik yang akan menerima string untuk indeks, parameter generik, dan delegasi yang akan diteruskan dengan referensi parameter generik dan item koleksi. Itu akan memungkinkan semantik yang hampir sama dengan array array, tetapi kecuali orang-orang vb.net dan C # dapat dikejar untuk menawarkan sintaks yang bagus, kode tersebut akan terlihat kikuk bahkan jika itu adalah kinerja yang wajar (melewati parameter generik akan memungkinkan untuk menggunakan delegasi statis dan akan menghindari kebutuhan untuk membuat instance kelas sementara)

Secara pribadi, saya kesal pada Eric Lippert dkk. memuntahkan tentang jenis nilai yang bisa berubah. Mereka menawarkan semantik yang lebih bersih daripada jenis referensi promiscuous yang digunakan di semua tempat. Meskipun ada beberapa batasan dengan dukungan .net untuk tipe nilai, ada banyak kasus di mana tipe nilai yang dapat berubah lebih cocok daripada jenis entitas lainnya.


1
@Ron Warholic: Tidak jelas bahwa SomeRect adalah Rectangle. Bisa jadi beberapa tipe lain yang secara implisit dapat typecast dari Rectangle. Meskipun, satu-satunya jenis sistem yang didefinisikan yang dapat secara implisit typecast dari Rectangle adalah RectangleF, dan kompiler akan mengomel jika seseorang mencoba untuk meneruskan bidang RectangleF ke konstruktor Rectangle (karena yang pertama adalah Single, dan yang terakhir Integer) , mungkin ada struct yang ditentukan pengguna yang memungkinkan typecasts implisit tersebut. BTW, pernyataan pertama akan bekerja sama baiknya apakah SomeRect adalah Rectangle atau RectangleF.
supercat

Yang Anda tunjukkan hanyalah bahwa dalam contoh yang dibuat-buat Anda yakin satu metode lebih jelas. Jika kami mengambil contoh Anda dengan Rectanglemudah, saya dapat dengan mudah menemukan posisi umum di mana Anda mendapatkan perilaku yang sangat tidak jelas . Pertimbangkan bahwa WinForms mengimplementasikan Rectangletipe yang bisa berubah yang digunakan di Boundsproperti formulir . Jika saya ingin mengubah batasan, saya ingin menggunakan sintaks Anda yang bagus: form.Bounds.X = 10;Namun, ini justru tidak mengubah apa pun di formulir (dan menghasilkan kesalahan indah yang memberi tahu Anda tentang hal itu). Ketidakkonsistenan adalah kutukan dari pemrograman dan itulah sebabnya kekekalan dibutuhkan.
Ron Warholic

3
@Ron Warholic: BTW, saya ingin dapat mengatakan "form.Bounds.X = 10;" dan hanya berfungsi, tetapi sistem tidak memberikan cara yang bersih untuk melakukannya. Sebuah konvensi untuk mengekspos properti tipe nilai sebagai metode yang menerima panggilan balik dapat menawarkan kode yang jauh lebih bersih, efisien, dan benar-benar terkonfirmasi daripada pendekatan apa pun yang menggunakan kelas.
supercat

4
Jawaban ini jauh lebih mendalam daripada beberapa jawaban pilihan teratas. Agak absurd bahwa argumen terhadap tipe nilai yang bisa berubah bergantung pada gagasan "apa yang Anda harapkan" terjadi ketika Anda mencampur alias dan mutasi. Bagaimanapun, itu hal yang mengerikan untuk dilakukan !
Eamon Nerbonne

2
@supercat: Siapa tahu, mungkin fitur ref-return yang mereka bicarakan untuk C # 7 mungkin mencakup basis itu (saya belum benar-benar melihatnya secara detail, tetapi kedengarannya mirip).
Eamon Nerbonne

24

Jenis nilai pada dasarnya mewakili konsep yang tidak berubah. Fx, tidak masuk akal untuk memiliki nilai matematika seperti integer, vektor dll dan kemudian dapat memodifikasinya. Itu akan seperti mendefinisikan kembali makna suatu nilai. Alih-alih mengubah tipe nilai, lebih masuk akal untuk menetapkan nilai unik lainnya. Pikirkan fakta bahwa tipe nilai dibandingkan dengan membandingkan semua nilai propertinya. Intinya adalah bahwa jika sifat-sifatnya sama maka itu adalah representasi universal yang sama dari nilai itu.

Seperti yang Konrad katakan, tidak masuk akal untuk mengubah tanggal juga, karena nilainya mewakili titik waktu yang unik dan bukan turunan dari objek waktu yang memiliki keadaan atau ketergantungan konteks apa pun.

Semoga ini masuk akal bagi Anda. Ini lebih tentang konsep yang Anda coba tangkap dengan tipe nilai daripada detail praktis, untuk memastikan.


Yah, mereka harus mewakili konsep berubah, setidaknya ;-p
Marc Gravell

3
Yah, saya kira mereka bisa membuat System.Drawing.Point tidak berubah tapi itu akan menjadi kesalahan desain IMHO serius. Saya pikir poin sebenarnya adalah tipe nilai tipikal dan mereka bisa berubah. Dan mereka tidak menimbulkan masalah bagi siapa pun di luar pemrograman pemula yang benar-benar awal.
Stephen Martin

3
Pada prinsipnya saya pikir poin juga harus tidak berubah tetapi jika itu membuat tipe lebih sulit atau kurang elegan untuk digunakan maka tentu saja itu harus dipertimbangkan juga. Tidak ada gunanya memiliki konstruksi kode yang menjunjung tinggi pangeran-pangeran terbaik jika tidak ada yang mau menggunakannya;)
Morten Christiansen

3
Tipe nilai berguna untuk mewakili konsep sederhana yang tidak dapat diubah, tetapi struktur bidang terbuka adalah tipe terbaik yang digunakan untuk menahan atau mengedarkan set kecil nilai-nilai terkait tetapi independen (seperti koordinat suatu titik). Lokasi penyimpanan jenis nilai seperti itu merangkum nilai bidangnya dan tidak ada yang lain. Sebaliknya, lokasi penyimpanan dari jenis referensi yang bisa berubah dapat digunakan untuk tujuan mempertahankan keadaan objek yang bisa berubah, tetapi juga merangkum identitas semua referensi lain di seluruh alam semesta yang ada pada objek yang sama.
supercat

4
"Jenis nilai pada dasarnya merupakan konsep yang tidak berubah". Tidak, mereka tidak. Salah satu aplikasi tertua dan paling berguna dari variabel tipe-nilai adalah intiterator, yang akan sama sekali tidak berguna jika itu tidak berubah. Saya pikir Anda sedang mengkonfigurasikan "implementasi kompiler / runtime tipe nilai" dengan "variabel yang diketik ke tipe nilai" - yang terakhir tentu saja bisa berubah ke salah satu nilai yang mungkin.
Slipp D. Thompson

19

Ada beberapa kasus sudut lain yang dapat menyebabkan perilaku yang tidak terduga dari sudut pandang programmer.

Jenis nilai yang tidak dapat diubah dan bidang yang hanya dapat dibaca

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Jenis dan array nilai yang dapat diubah

Misalkan kita memiliki array Mutablestruct kita dan kita memanggil IncrementImetode untuk elemen pertama array itu. Perilaku apa yang Anda harapkan dari panggilan ini? Haruskah itu mengubah nilai array atau hanya salinan?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

Jadi, struct yang bisa berubah tidak jahat selama Anda dan anggota tim lainnya memahami dengan jelas apa yang Anda lakukan. Tetapi ada terlalu banyak kasus sudut ketika perilaku program akan berbeda dari apa yang diharapkan, yang dapat menyebabkan sulit untuk diproduksi dan sulit untuk memahami kesalahan.


5
Apa yang harus terjadi, jika bahasa .net memiliki dukungan tipe nilai yang sedikit lebih baik, akan menjadi metode struct harus dilarang bermutasi 'ini' kecuali jika mereka secara eksplisit dinyatakan melakukan hal itu, dan metode yang dinyatakan harus dilarang di read-only konteks. Array struct yang dapat berubah menawarkan semantik yang bermanfaat yang tidak dapat dicapai secara efisien melalui cara lain.
supercat

2
ini adalah contoh bagus dari masalah yang sangat halus yang akan muncul dari struct yang bisa berubah. Saya tidak akan mengharapkan perilaku ini. Mengapa sebuah array memberi Anda referensi, tetapi sebuah antarmuka memberi Anda nilai? Saya akan berpikir, selain dari nilai-nilai sepanjang waktu (itulah yang benar-benar saya harapkan), bahwa setidaknya akan menjadi sebaliknya: antarmuka memberikan referensi; array memberikan nilai ...
Dave Cousineau

@ Sahuagin: Sayangnya, tidak ada mekanisme standar yang digunakan suatu antarmuka untuk mengekspos referensi. Ada beberapa cara .net dapat memungkinkan hal-hal seperti itu dilakukan dengan aman dan bermanfaat (misalnya dengan mendefinisikan "ArrayRef <T>" struct khusus yang mengandung a T[]dan indeks integer, dan dengan ketentuan bahwa akses ke properti tipe ArrayRef<T>akan ditafsirkan sebagai akses ke elemen array yang sesuai) [jika suatu kelas ingin mengekspos suatu ArrayRef<T>untuk tujuan lain apa pun, ia dapat menyediakan metode - yang bertentangan dengan properti - untuk mengambilnya]. Sayangnya, tidak ada ketentuan seperti itu.
supercat

2
Oh my ... ini membuat struct bisa berubah jahat!
nawfal

1
Saya suka jawaban ini karena mengandung informasi yang sangat berharga yang tidak jelas. Tapi sungguh, ini bukan argumen terhadap struct yang bisa berubah seperti beberapa klaim. Ya apa yang kita lihat di sini adalah "lubang keputusasaan" seperti yang akan dikatakan Eric, tetapi sumber keputusasaan ini bukanlah sifat berubah-ubah. Sumber putus asa adalah diri bermutasi struct metode . (Adapun mengapa array dan daftar berperilaku berbeda itu karena pada dasarnya satu operator yang menghitung alamat memori dan yang lainnya adalah properti. Secara umum semuanya menjadi jelas setelah Anda memahami bahwa "referensi" adalah nilai alamat .)
AnorZaken

18

Jika Anda pernah memprogram dalam bahasa seperti C / C ++, struct boleh digunakan sebagai bisa berubah. Cukup berikan mereka dengan referensi, sekitar dan tidak ada yang salah. Satu-satunya masalah yang saya temukan adalah pembatasan dari kompiler C # dan bahwa, dalam beberapa kasus, saya tidak dapat memaksa hal bodoh untuk menggunakan referensi ke struct, bukan Salin (seperti ketika sebuah struct adalah bagian dari kelas C # ).

Jadi, struct yang bisa berubah tidak jahat, C # telah membuatnya jahat. Saya menggunakan struct bisa berubah dalam C ++ sepanjang waktu dan mereka sangat nyaman dan intuitif. Sebaliknya, C # membuat saya untuk sepenuhnya meninggalkan struct sebagai anggota kelas karena cara mereka menangani objek. Kenyamanan mereka telah merugikan kita.


Memiliki kelas bidang tipe struktur seringkali dapat menjadi pola yang sangat berguna, meskipun harus diakui ada beberapa keterbatasan. Kinerja akan terdegradasi jika seseorang menggunakan properti daripada bidang, atau penggunaan readonly, tetapi jika seseorang menghindari melakukan hal-hal itu bidang kelas tipe struktur baik-baik saja. Satu-satunya batasan struktur yang benar-benar mendasar adalah bahwa bidang struct dari tipe kelas yang dapat berubah seperti int[]dapat merangkum identitas atau sekumpulan nilai yang tidak berubah, tetapi tidak dapat digunakan untuk merangkum nilai yang dapat diubah tanpa juga merangkum identitas yang tidak diinginkan.
supercat

13

Jika Anda berpegang pada apa yang dimaksudkan untuk struct (dalam C #, Visual Basic 6, Pascal / Delphi, tipe C ++ struct (atau kelas) ketika mereka tidak digunakan sebagai pointer), Anda akan menemukan bahwa struktur tidak lebih dari variabel gabungan . Ini berarti: Anda akan memperlakukan mereka sebagai satu set variabel yang dikemas, dengan nama umum (variabel rekaman yang Anda referensikan dari anggota).

Saya tahu itu akan membingungkan banyak orang yang terbiasa dengan OOP, tetapi itu tidak cukup alasan untuk mengatakan hal-hal seperti itu pada dasarnya jahat, jika digunakan dengan benar. Beberapa struktur tidak dapat diubah karena mereka berniat (ini adalah kasus Python namedtuple), tetapi ini adalah paradigma lain yang perlu dipertimbangkan.

Ya: struct melibatkan banyak memori, tetapi itu tidak akan menjadi lebih banyak memori dengan melakukan:

point.x = point.x + 1

dibandingkan dengan:

point = Point(point.x + 1, point.y)

Konsumsi memori akan setidaknya sama, atau bahkan lebih dalam kasus yang tidak dapat diputuskan (meskipun kasus itu akan bersifat sementara, untuk tumpukan saat ini, tergantung pada bahasa).

Tetapi, akhirnya, struktur adalah struktur , bukan objek. Dalam POO, properti utama suatu objek adalah identitas mereka , yang sebagian besar waktunya tidak lebih dari alamat ingatannya. Struct adalah singkatan dari struktur data (bukan objek yang tepat, dan karenanya mereka tidak memiliki identitas), dan data dapat dimodifikasi. Dalam bahasa lain, catatan (bukan struct , seperti halnya untuk Pascal) adalah kata dan memiliki tujuan yang sama: hanya variabel catatan data, dimaksudkan untuk dibaca dari file, dimodifikasi, dan dibuang ke file (itu adalah utama gunakan dan, dalam banyak bahasa, Anda bahkan dapat mendefinisikan penyelarasan data dalam catatan, sementara itu tidak selalu berarti untuk Objek yang disebut dengan benar).

Ingin contoh yang bagus? Structs digunakan untuk membaca file dengan mudah. Python memiliki pustaka ini karena, karena berorientasi objek dan tidak memiliki dukungan untuk struct, ia harus mengimplementasikannya dengan cara lain, yang agak jelek. Bahasa yang menerapkan struct memiliki fitur itu ... bawaan. Cobalah membaca header bitmap dengan struct yang sesuai dalam bahasa seperti Pascal atau C. Akan mudah (jika struct dibangun dan disejajarkan dengan benar; di Pascal Anda tidak akan menggunakan akses berbasis catatan tetapi berfungsi untuk membaca data biner acak). Jadi, untuk file dan akses memori (lokal) langsung, struct lebih baik daripada objek. Seperti untuk hari ini, kita terbiasa dengan JSON dan XML, jadi kita lupa penggunaan file biner (dan sebagai efek samping, penggunaan struct). Tapi ya: mereka ada, dan punya tujuan.

Mereka tidak jahat. Cukup gunakan untuk tujuan yang benar.

Jika Anda berpikir tentang palu, Anda akan ingin memperlakukan sekrup sebagai paku, untuk menemukan sekrup lebih sulit untuk dipasang di dinding, dan itu akan menjadi kesalahan sekrup, dan mereka akan menjadi yang jahat.


12

Bayangkan Anda memiliki array 1.000.000 struct. Setiap struct mewakili ekuitas dengan hal-hal seperti bid_price, offer_price (mungkin desimal) dan seterusnya, ini dibuat oleh C # / VB.

Bayangkan bahwa array dibuat dalam blok memori yang dialokasikan dalam heap yang tidak dikelola sehingga beberapa thread kode asli lainnya dapat secara bersamaan mengakses array (mungkin beberapa kode perf-tinggi melakukan matematika).

Bayangkan kode C # / VB sedang mendengarkan umpan pasar dari perubahan harga, kode itu mungkin harus mengakses beberapa elemen array (untuk keamanan apa pun) dan kemudian memodifikasi beberapa bidang harga.

Bayangkan ini dilakukan puluhan atau bahkan ratusan ribu kali per detik.

Yah mari kita hadapi fakta, dalam hal ini kita benar-benar ingin struct ini bisa berubah, mereka harus karena mereka sedang dibagikan oleh beberapa kode asli lain sehingga membuat salinan tidak akan membantu; mereka perlu karena membuat salinan dari beberapa struct 120 byte pada tingkat ini adalah kegilaan, terutama ketika pembaruan sebenarnya dapat berdampak hanya satu atau dua byte.

Hugo


3
Benar, tetapi dalam kasus ini alasan untuk menggunakan struct adalah bahwa hal itu dikenakan pada desain aplikasi oleh kendala luar (yang oleh penggunaan kode asli). Segala sesuatu yang Anda jelaskan tentang objek-objek ini menunjukkan mereka harus jelas kelas dalam C # atau VB.NET.
Jon Hanna

6
Saya tidak yakin mengapa beberapa orang berpikir benda itu harus menjadi objek kelas. Jika semua slot array diisi dengan referensi instance yang berbeda, menggunakan tipe kelas akan menambahkan dua belas atau dua puluh empat byte tambahan ke persyaratan memori, dan akses sekuensial pada array referensi objek kelas cenderung jauh lebih lambat daripada akses sekuensial pada array struct.
supercat

9

Ketika sesuatu dapat bermutasi, ia memperoleh rasa identitas.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Karena Personbisa berubah, lebih alami untuk berpikir tentang mengubah posisi Eric daripada mengkloning Eric, memindahkan klon, dan menghancurkan yang asli . Kedua operasi akan berhasil mengubah konten eric.position, tetapi yang satu lebih intuitif daripada yang lain. Demikian juga, lebih intuitif untuk memberikan Eric sekitar (sebagai referensi) untuk metode untuk memodifikasinya. Memberikan metode klon Eric hampir selalu akan mengejutkan. Siapa pun yang ingin bermutasi Personharus ingat untuk meminta referensi Personatau mereka akan melakukan hal yang salah.

Jika Anda membuat jenisnya tidak dapat diubah, masalahnya hilang; jika saya tidak dapat memodifikasi eric, tidak ada bedanya bagi saya apakah saya menerima ericatau tiruan dari eric. Secara lebih umum, suatu jenis aman untuk dilewati oleh nilai jika semua negara yang dapat diamati dimiliki oleh anggota yang:

  • abadi
  • jenis referensi
  • aman untuk melewati nilai

Jika kondisi tersebut terpenuhi maka tipe nilai yang dapat berubah berperilaku seperti tipe referensi karena salinan yang dangkal masih akan memungkinkan penerima untuk memodifikasi data asli.

Intuisi kekekalan Persontergantung pada apa yang Anda coba lakukan. Jika Personhanya mewakili sekumpulan data tentang seseorang, tidak ada yang tidak intuitif tentangnya; Personvariabel benar-benar mewakili nilai abstrak , bukan objek. (Dalam hal itu, mungkin akan lebih tepat untuk mengubah nama menjadi PersonData.) Jika Personsebenarnya memodelkan seseorang itu sendiri, ide untuk terus-menerus membuat dan memindahkan klon adalah konyol bahkan jika Anda telah menghindari perangkap berpikir Anda memodifikasi asli. Dalam hal itu mungkin akan lebih alami untuk hanya membuat Persontipe referensi (yaitu, kelas.)

Memang, seperti pemrograman fungsional telah mengajarkan kepada kita ada manfaat untuk membuat semuanya berubah (tidak ada yang dapat diam-diam berpegang pada referensi ericdan memutasinya), tetapi karena itu bukan idiomatik dalam OOP itu masih akan menjadi tidak intuitif untuk orang lain yang bekerja dengan Anda kode.


3
Maksud Anda tentang identitas adalah hal yang baik; mungkin perlu dicatat bahwa identitas hanya relevan ketika beberapa referensi ada untuk sesuatu. Jikafoo memegang satu-satunya referensi ke targetnya di mana saja di alam semesta, dan tidak ada yang menangkap nilai identitas-hash objek itu, maka bidang mutasi foo.Xsecara semantik setara dengan membuat footitik ke objek baru yang sama seperti yang sebelumnya disebut, tetapi dengan Xmemegang nilai yang diinginkan. Dengan tipe kelas, umumnya sulit untuk mengetahui apakah banyak referensi ada untuk sesuatu, tetapi dengan struct itu mudah: mereka tidak.
supercat

1
Jika Thingadalah tipe kelas yang dapat diubah, sebuah Thing[] akan merangkum identitas objek - apakah orang menginginkannya atau tidak - kecuali seseorang dapat memastikan bahwa tidak ada Thingdalam array yang ada referensi luar akan pernah dimutasi. Jika seseorang tidak ingin elemen array untuk merangkum identitas, seseorang harus secara umum memastikan bahwa tidak ada item yang dipegang referensi akan pernah dimutasi, atau bahwa tidak ada referensi luar yang akan ada untuk item yang dipegang [pendekatan hybrid juga dapat bekerja ] Tidak ada pendekatan yang sangat nyaman. Jika Thingadalah struktur, a Thing[]merangkum nilai saja.
supercat

Untuk objek, identitas mereka berasal dari lokasi mereka. Contoh tipe referensi memiliki identitasnya berkat lokasinya di memori dan Anda hanya memberikan identitas mereka (referensi), bukan data mereka, sedangkan tipe nilai memiliki identitas mereka di tempat luar tempat mereka disimpan. Identitas tipe nilai Eric Anda hanya berasal dari variabel tempat ia disimpan. Jika Anda melewatinya, dia akan kehilangan identitasnya.
IllidanS4 ingin Monica kembali

6

Itu tidak ada hubungannya dengan struct (dan tidak dengan C #, baik) tetapi di Jawa Anda mungkin mendapatkan masalah dengan objek yang bisa berubah ketika mereka misalnya kunci dalam peta hash. Jika Anda mengubahnya setelah menambahkannya ke peta dan itu mengubah kode hash , hal-hal jahat mungkin terjadi.


3
Itu benar jika Anda menggunakan kelas sebagai kunci dalam peta juga.
Marc Gravell

6

Secara pribadi ketika saya melihat kode berikut ini terlihat cukup kikuk bagi saya:

data.value.set (data.value.get () + 1);

bukan hanya

data.value ++; atau data.value = data.value + 1;

Enkapsulasi data berguna ketika melewati kelas di sekitar dan Anda ingin memastikan nilai dimodifikasi secara terkendali. Namun ketika Anda memiliki set publik dan mendapatkan fungsi yang tidak lebih dari menetapkan nilai pada apa yang pernah diteruskan, bagaimana ini merupakan peningkatan dari sekadar meneruskan struktur data publik?

Ketika saya membuat struktur pribadi di dalam kelas, saya membuat struktur itu untuk mengatur satu set variabel menjadi satu kelompok. Saya ingin dapat memodifikasi struktur itu dalam ruang lingkup kelas, tidak mendapatkan salinan dari struktur itu dan membuat contoh baru.

Bagi saya ini mencegah penggunaan yang valid dari struktur yang digunakan untuk mengatur variabel publik, jika saya ingin kontrol akses saya akan menggunakan kelas.


1
Langsung ke intinya! Struktur adalah unit organisasi tanpa batasan kontrol akses! Sayangnya, C # telah membuat mereka tidak berguna untuk tujuan ini!
ThunderGr

ini benar-benar melenceng karena kedua contoh Anda menunjukkan struct yang bisa berubah.
vidstige

C # membuat mereka tidak berguna untuk tujuan ini karena itu bukan tujuan dari struktur
Luiz Felipe

6

Ada beberapa masalah dengan contoh Mr. Eric Lippert. Ini dibuat untuk menggambarkan titik bahwa struct disalin dan bagaimana itu bisa menjadi masalah jika Anda tidak hati-hati. Melihat contoh saya melihatnya sebagai hasil dari kebiasaan pemrograman yang buruk dan tidak benar-benar masalah dengan struct atau kelas.

  1. Seorang struct seharusnya hanya memiliki anggota publik dan tidak perlu enkapsulasi. Jika ya maka itu benar-benar harus tipe / kelas. Anda benar-benar tidak perlu dua konstruksi untuk mengatakan hal yang sama.

  2. Jika Anda memiliki kelas yang menyertakan struct, Anda akan memanggil metode di kelas untuk memutasi struct member. Inilah yang akan saya lakukan sebagai kebiasaan pemrograman yang baik.

Implementasi yang tepat adalah sebagai berikut.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

Sepertinya itu adalah masalah dengan kebiasaan pemrograman yang bertentangan dengan masalah dengan struct itu sendiri. Structs seharusnya bisa berubah, itulah ide dan maksudnya.

Hasil dari perubahan voila berlaku seperti yang diharapkan:

1 2 3 Tekan tombol apa saja untuk melanjutkan. . .


4
Tidak ada yang salah dengan mendesain struktur buram kecil untuk berperilaku seperti objek kelas abadi; pedoman MSDN masuk akal ketika seseorang mencoba untuk membuat sesuatu yang berperilaku seperti objek . Struktur cocok dalam beberapa kasus di mana seseorang membutuhkan hal-hal ringan yang berperilaku seperti objek, dan dalam kasus di mana seseorang membutuhkan banyak variabel yang menempel bersama dengan lakban. Namun, untuk beberapa alasan, banyak orang gagal menyadari bahwa struktur memiliki dua penggunaan berbeda, dan bahwa pedoman yang sesuai untuk satu tidak sesuai untuk yang lain.
supercat

5

Ada banyak keuntungan dan kerugian dari data yang bisa berubah. Kerugian jutaan dolar adalah aliasing. Jika nilai yang sama digunakan di banyak tempat, dan salah satunya mengubahnya, maka secara ajaib akan berubah menjadi tempat lain yang menggunakannya. Ini terkait dengan, tetapi tidak identik dengan, kondisi lomba.

Keuntungan jutaan dolar adalah modularitas, kadang-kadang. Status yang dapat diubah dapat memungkinkan Anda untuk menyembunyikan informasi yang berubah dari kode yang tidak perlu diketahui tentang hal itu.

Art of the Interpreter membahas trade off ini secara rinci, dan memberikan beberapa contoh.


struct tidak dapat disebut dalam c #. Setiap penugasan struct adalah salinan.
Rekursif

@recursive: Dalam beberapa kasus, itu adalah keuntungan utama dari struct yang bisa berubah, dan yang membuat saya mempertanyakan gagasan bahwa struct seharusnya tidak bisa berubah. Fakta bahwa kompiler terkadang secara tersirat menyalin struct tidak mengurangi kegunaan struct yang bisa berubah.
supercat

1

Saya tidak percaya mereka jahat jika digunakan dengan benar. Saya tidak akan memasukkannya ke dalam kode produksi saya, tetapi saya akan melakukan sesuatu seperti mengolok-olok pengujian unit terstruktur, di mana umur sebuah struct relatif kecil.

Menggunakan contoh Eric, mungkin Anda ingin membuat contoh kedua dari Eric itu, tetapi buat penyesuaian, karena itulah sifat pengujian Anda (yaitu duplikasi, lalu modifikasi). Tidak masalah apa yang terjadi dengan instance pertama Eric jika kita hanya menggunakan Eric2 untuk sisa skrip pengujian, kecuali jika Anda berencana menggunakannya sebagai perbandingan pengujian.

Ini akan sangat berguna untuk menguji atau memodifikasi kode legacy yang dangkal mendefinisikan objek tertentu (titik struct), tetapi dengan memiliki struct yang tidak dapat diubah, ini mencegah penggunaannya secara mengganggu.


Seperti yang saya lihat, sebuah struct pada intinya sekelompok variabel terjebak bersama dengan lakban. Itu mungkin dalam. NET untuk struct berpura-pura menjadi sesuatu selain sekelompok variabel terjebak bersama-sama dengan lakban, dan saya akan menyarankan bahwa ketika praktis jenis yang akan berpura-pura menjadi sesuatu selain sekelompok variabel terjebak bersama-sama dengan lakban harus berperilaku sebagai objek terpadu (yang untuk struct akan menyiratkan kekekalan), tetapi kadang-kadang berguna untuk menempel sekelompok variabel bersama dengan lakban. Bahkan dalam kode produksi, saya akan mempertimbangkan lebih baik untuk memiliki tipe ...
supercat

... yang jelas tidak memiliki semantik di luar "setiap bidang berisi hal terakhir yang ditulis untuknya", mendorong semua semantik ke dalam kode yang menggunakan struktur, daripada mencoba membuat struct melakukan lebih banyak. Diberikan, misalnya, Range<T>tipe dengan anggota Minimumdan Maximumbidang tipe T, dan kode Range<double> myRange = foo.getRange();, jaminan apa pun tentang apa Minimumdan isi Maximumharus berasal foo.GetRange();. Memiliki Rangestruct bidang terbuka akan membuat jelas bahwa itu tidak akan menambah perilaku sendiri.
supercat
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.