Kapan Anda menggunakan struct, bukan kelas? [Tutup]

174

Apa aturan praktis Anda saat menggunakan struct vs kelas? Saya sedang memikirkan definisi C # dari istilah-istilah itu tetapi jika bahasa Anda memiliki konsep yang sama, saya ingin mendengar pendapat Anda juga.

Saya cenderung menggunakan kelas untuk hampir semua hal, dan menggunakan struct hanya ketika ada sesuatu yang sangat sederhana dan harus menjadi tipe nilai, seperti PhoneNumber atau sesuatu seperti itu. Tapi ini sepertinya penggunaan yang relatif kecil dan saya harap ada kasus penggunaan yang lebih menarik.

RasionalGeek
sumber
4
Kemungkinan duplikat: stackoverflow.com/questions/6267957/struct-vs-class
FrustratedWithFormsDesigner
7
@Frustrated Tidak dapat menandai pertanyaan sebagai duplikat sesuatu di situs lain, meskipun pasti terkait.
Adam Lear
@Anna Lear: Saya tahu, saya memilih untuk bermigrasi agar tidak sedekat duplikat. Setelah dimigrasikan, itu dapat ditutup sebagai duplikat dengan benar .
FrustratedWithFormsDesigner
5
@ Frustrasi Saya tidak berpikir itu perlu dimigrasi.
Adam Lear
15
Ini sepertinya pertanyaan yang jauh lebih cocok untuk P.SE vs. SO. Itulah sebabnya saya bertanya di sini.
RationalGeek

Jawaban:

169

Aturan umum yang harus diikuti adalah bahwa struct harus berupa kumpulan properti terkait yang kecil, sederhana (satu tingkat), yang tidak dapat diubah begitu dibuat; untuk hal lain, gunakan kelas.

C # bagus karena struct dan kelas tidak memiliki perbedaan eksplisit dalam deklarasi selain kata kunci yang menentukan; jadi, jika Anda merasa perlu "meningkatkan" sebuah struct ke suatu kelas, atau sebaliknya "menurunkan" sebuah kelas ke sebuah struct, itu sebagian besar masalah sederhana mengubah kata kunci (ada beberapa gotchas lainnya; struct tidak dapat memperoleh dari kelas atau tipe struct lainnya, dan mereka tidak dapat secara eksplisit mendefinisikan konstruktor tanpa parameter default).

Saya mengatakan "sebagian besar", karena hal yang lebih penting untuk diketahui tentang struct adalah bahwa, karena mereka adalah tipe nilai, memperlakukan mereka seperti kelas (tipe referensi) dapat berakhir dengan rasa sakit setengah. Khususnya, membuat sifat-sifat struktur bisa berubah bisa menyebabkan perilaku yang tidak terduga.

Misalnya, Anda memiliki kelas SimpleClass dengan dua properti, A dan B. Anda instantiate salinan kelas ini, menginisialisasi A dan B, dan kemudian meneruskan contoh ke metode lain. Metode itu selanjutnya memodifikasi A dan B. Kembali ke fungsi pemanggilan (yang membuat instance), A dan B instance Anda akan memiliki nilai yang diberikan kepada mereka oleh metode yang dipanggil.

Sekarang, Anda membuatnya menjadi sebuah struct. Properti masih bisa berubah. Anda melakukan operasi yang sama dengan sintaks yang sama seperti sebelumnya, tetapi sekarang, nilai-nilai baru A dan B tidak dalam contoh setelah memanggil metode. Apa yang terjadi? Nah, kelas Anda sekarang adalah sebuah struct, yang berarti itu adalah tipe nilai. Jika Anda meneruskan jenis nilai ke suatu metode, default (tanpa kata kunci keluar atau ref) adalah meneruskan "menurut nilai"; salinan dangkal instance dibuat untuk digunakan oleh metode, dan kemudian dihancurkan ketika metode dilakukan meninggalkan instance awal utuh.

Ini menjadi lebih membingungkan jika Anda memiliki tipe referensi sebagai anggota struct Anda (tidak dianulir, tetapi praktik yang sangat buruk dalam hampir semua kasus); kelas tidak akan dikloning (hanya referensi struct untuknya), jadi perubahan pada struct tidak akan memengaruhi objek asli, tetapi perubahan pada subkelas struct AKAN memengaruhi instance dari kode panggilan. Ini dapat dengan mudah menempatkan struct yang bisa berubah dalam kondisi yang sangat tidak konsisten yang dapat menyebabkan kesalahan jauh dari tempat masalah sebenarnya.

Untuk alasan ini, hampir setiap otoritas di C # mengatakan untuk selalu membuat struktur Anda tidak berubah; memungkinkan konsumen untuk menentukan nilai properti hanya pada konstruksi objek, dan tidak pernah menyediakan sarana apa pun untuk mengubah nilai instance itu. Bidang hanya baca, atau properti hanya dapatkan, adalah aturannya. Jika konsumen ingin mengubah nilainya, mereka dapat membuat objek baru berdasarkan nilai-nilai yang lama, dengan perubahan yang mereka inginkan, atau mereka dapat memanggil metode yang akan melakukan hal yang sama. Ini memaksa mereka untuk memperlakukan satu instance dari struct Anda sebagai satu "nilai" konseptual, tidak dapat dibagi dan berbeda dari (tetapi mungkin setara dengan) semua yang lain. Jika mereka melakukan operasi pada "nilai" yang disimpan oleh tipe Anda, mereka mendapatkan "nilai" baru yang berbeda dari nilai awal mereka,

Untuk contoh yang baik, lihat tipe DateTime. Anda tidak dapat menetapkan salah satu bidang instance DateTime secara langsung; Anda harus membuat yang baru, atau memanggil metode yang sudah ada yang akan menghasilkan contoh baru. Ini karena tanggal dan waktu adalah "nilai", seperti angka 5, dan perubahan ke angka 5 menghasilkan nilai baru yang bukan 5. Hanya karena 5 + 1 = 6 tidak berarti 5 sekarang 6 karena Anda menambahkan 1 ke dalamnya. DateTimes bekerja dengan cara yang sama; 12:00 tidak "menjadi" 12:01 jika Anda menambahkan satu menit, Anda malah mendapatkan nilai baru 12:01 yang berbeda dari 12:00. Jika ini adalah keadaan logis untuk jenis Anda (contoh konseptual yang bagus yang tidak ada di dalam. NET adalah Uang, Jarak, Berat, dan jumlah lain dari UOM di mana operasi harus memperhitungkan semua bagian dari nilai tersebut), kemudian gunakan struct dan desain yang sesuai. Dalam kebanyakan kasus lain di mana sub-item suatu objek harus bisa berubah secara independen, gunakan kelas.

KeithS
sumber
1
Jawaban yang bagus! Saya akan menunjuk satu membacanya untuk metodologi dan buku 'Pengembangan Domain Didorong', yang tampaknya agak terkait dengan pendapat Anda tentang mengapa seseorang harus menggunakan kelas atau struktur berdasarkan pada konsep mana Anda benar-benar mencoba untuk menerapkan
Maxim Popravko
1
Hanya sedikit detail yang layak disebutkan: Anda tidak dapat menentukan konstruktor default Anda sendiri pada struct. Anda harus puas dengan yang menginisialisasi semuanya ke nilai standarnya. Biasanya, itu sangat kecil, terutama pada kelas yang merupakan kandidat yang baik untuk menjadi struct. Tetapi itu berarti bahwa mengubah kelas menjadi struct mungkin menyiratkan lebih dari sekedar mengubah kata kunci.
Laurent Bourgault-Roy
1
@ LaurentBourgault-Roy - Benar. Ada gotcha lainnya juga; struct tidak dapat memiliki hierarki warisan, misalnya. Mereka dapat mengimplementasikan antarmuka, tetapi tidak dapat berasal dari apa pun selain System.ValueType (tersirat oleh mereka sedang struct).
KeithS
Sebagai pengembang JavaScript, saya harus bertanya dua hal. Bagaimana literal objek berbeda dari penggunaan struct secara tipikal dan mengapa penting bahwa struct tidak dapat diubah?
Erik Reppen
1
@ErikReppen: Sebagai jawaban atas kedua pertanyaan Anda: Ketika sebuah struct diteruskan ke suatu fungsi, ia diteruskan oleh nilai. Dengan kelas, Anda memberikan referensi ke kelas (masih berdasarkan nilai). Eric Lippert membahas mengapa ini menjadi masalah: blogs.msdn.com/b/ericlippert/archive/2008/05/14/… . Paragraf terakhir KeithS juga mencakup pertanyaan-pertanyaan ini.
Brian
14

Jawabannya: "Gunakan struct untuk konstruksi data murni, dan kelas untuk objek dengan operasi" jelas salah IMO. Jika struct memegang sejumlah besar properti maka kelas hampir selalu lebih tepat. Microsoft sering mengatakan, dari sudut pandang efisiensi, jika tipe Anda lebih besar dari 16 byte itu harus kelas.

https://stackoverflow.com/questions/1082311/why-should-a-net-struct-be-less-than-16-bytes

bytedev
sumber
1
Benar. Saya akan memilih jika saya bisa. Saya tidak mendapatkan dari mana ide itu berasal bahwa struct adalah untuk data dan objek tidak. Sebuah struct di C # hanyalah sebuah mekanisme untuk mengalokasikan pada stack. Salinan seluruh struct dilewatkan bukan hanya salinan dari pointer objek.
mike30
2
@mike - C # struct tidak harus dialokasikan pada stack.
Lee
1
@mike Gagasan "struct is for data" mungkin berasal dari kebiasaan yang diperoleh selama bertahun-tahun pemrograman C. C tidak memiliki kelas dan struct-nya adalah wadah data saja.
Konamiman
Jelas ada setidaknya 3 programmer C (terangkat) setelah membaca jawaban Michael K ;-)
bytedev
10

Perbedaan paling penting antara a classdan a structadalah apa yang terjadi dalam situasi berikut:

  Thing thing1 = new Thing ();
  thing1.somePropertyOrField = 5;
  Benda2 = benda1;
  thing2.somePropertyOrField = 9;

Apa yang seharusnya menjadi efek dari pernyataan terakhir thing1.somePropertyOrField? Jika Thingsebuah struct, dan somePropertyOrFieldmerupakan bidang publik yang terbuka, objek thing1dan thing2akan "terlepas" dari satu sama lain, sehingga pernyataan terakhir tidak akan mempengaruhi thing1. Jika Thingkelas, maka thing1dan thing2akan dilampirkan satu sama lain, sehingga pernyataan terakhir akan menulis thing1.somePropertyOrField. Seseorang harus menggunakan struct dalam kasus-kasus di mana semantik sebelumnya lebih masuk akal, dan harus menggunakan kelas dalam kasus-kasus di mana semantik terakhir akan lebih masuk akal.

Perhatikan bahwa sementara beberapa orang menyarankan bahwa keinginan untuk membuat sesuatu yang bisa berubah adalah argumen yang mendukungnya menjadi kelas, saya akan menyarankan sebaliknya adalah benar: jika sesuatu yang ada untuk tujuan menyimpan beberapa data akan bisa berubah, dan jika tidak akan jelas apakah instance terpasang pada sesuatu, benda tersebut harus berupa struct (kemungkinan dengan bidang yang terbuka) sehingga memperjelas bahwa instance tidak melekat pada hal lain.

Misalnya, perhatikan pernyataan:

  Person somePerson = myPeople.GetPerson ("123-45-6789");
  somePerson.Name = "Mary Johnson"; // Sudah menjadi "Mary Smith"

Akankah pernyataan kedua mengubah informasi yang disimpan myPeople? Jika Personadalah struct bidang-terbuka, itu tidak akan, dan fakta bahwa itu tidak akan menjadi konsekuensi yang jelas sebagai struktur-bidang terbuka ; jika Personadalah struct dan seseorang ingin memperbarui myPeople, seseorang jelas harus melakukan sesuatu seperti myPeople.UpdatePerson("123-45-6789", somePerson). Jika Personkelas, namun, mungkin jauh lebih sulit untuk menentukan apakah kode di atas tidak akan memperbarui isi MyPeople, selalu memperbaruinya, atau kadang-kadang memperbaruinya.

Berkenaan dengan gagasan bahwa struct harus "abadi", saya tidak setuju secara umum. Ada kasus penggunaan yang valid untuk struct "tidak dapat diubah" (di mana invarian diterapkan dalam sebuah konstruktor) tetapi mengharuskan seluruh struktur harus ditulis ulang kapan saja bagian mana pun dari perubahan itu adalah canggung, boros, dan lebih cenderung menyebabkan bug daripada sekadar mengekspos bug. bidang secara langsung. Misalnya, pertimbangkan PhoneNumberstruct yang bidangnya, sertakan antara lain, AreaCodedan Exchange, dan anggaplah seseorang memiliki a List<PhoneNumber>. Efek dari berikut ini harus cukup jelas:

  untuk (int i = 0; i <myList.Count; i ++)
  {
    PhoneNumber theNumber = myList [i];
    if (theNumber.AreaCode == "312")
    {
      string newExchange = "";
      if (new312to708Exchanges.TryGetValue (theNumber.Exchange), keluar newExchange)
      {
        theNumber.AreaCode = "708";
        theNumber.Exchange = newExchange;
        myList [i] = theNumber;
      }
    }
  }

Perhatikan bahwa tidak ada dalam kode di atas yang tahu atau peduli tentang bidang apa pun PhoneNumberselain AreaCodedan Exchange. Jika PhoneNumberapa yang disebut sebagai struct "tidak dapat diubah", akan diperlukan bahwa ia menyediakan withXXmetode untuk setiap bidang, yang akan mengembalikan instance struct baru yang memegang nilai pass-in di bidang yang ditunjukkan, atau kalau tidak akan diperlukan untuk kode seperti di atas untuk mengetahui tentang setiap bidang dalam struct. Tidak persis menarik.

BTW, setidaknya ada dua kasus di mana struct dapat memegang referensi untuk jenis yang bisa berubah.

  1. Semantik dari struct menunjukkan bahwa itu menyimpan identitas objek yang bersangkutan, bukan sebagai jalan pintas untuk memegang properti objek. Misalnya, `KeyValuePair` akan menampung identitas tombol-tombol tertentu, tetapi tidak akan diharapkan untuk menyimpan informasi terus-menerus tentang posisi tombol itu, keadaan sorot, dll.
  2. Struct tahu bahwa ia memegang referensi hanya untuk objek itu, tidak ada orang lain yang akan mendapatkan referensi, dan setiap mutasi yang akan dilakukan untuk objek itu akan dilakukan sebelum referensi ke itu disimpan di mana saja.

Dalam skenario sebelumnya, IDENTITAS objek tidak akan berubah; di yang kedua, kelas objek bersarang mungkin tidak menegakkan imutabilitas, tetapi struct yang memegang referensi akan.

supercat
sumber
7

Saya cenderung menggunakan struct hanya ketika ada satu metode yang diperlukan sehingga membuat kelas tampak terlalu "berat". Jadi terkadang saya memperlakukan struct sebagai objek yang ringan. WASPADALAH: ini tidak selalu berhasil dan tidak selalu merupakan hal terbaik untuk dilakukan, jadi itu benar-benar tergantung pada situasinya!

SUMBER DAYA EKSTRA

Untuk penjelasan yang lebih formal, MSDN mengatakan: http://msdn.microsoft.com/en-us/library/ms229017.aspx Tautan ini memverifikasi apa yang saya sebutkan di atas, struct hanya boleh digunakan untuk menampung sejumlah kecil data.

gila
sumber
5
Pertanyaan di situs lain (bahkan jika situs itu Stack Overflow ) tidak dihitung sebagai duplikat. Harap rentangkan jawaban Anda untuk memasukkan jawaban yang sebenarnya dan bukan hanya tautan ke tempat lain. Jika tautannya pernah berubah, jawaban Anda seperti yang tertulis sekarang tidak akan berguna dan kami ingin menjaga informasi dan membuat Programer semaksimal mungkin. Terima kasih!
Adam Lear
2
@Anna Lear Terima kasih telah memberi tahu saya, akan mengingatnya mulai sekarang!
rrazd
2
-1 "Saya cenderung menggunakan struct hanya ketika ada satu metode yang diperlukan sehingga membuat kelas tampak terlalu" berat "." ... berat? apa maksudnya itu? ... dan apakah Anda benar-benar mengatakan bahwa hanya karena ada satu metode maka mungkin harus menjadi struct?
bytedev
2

Gunakan struct untuk konstruk data murni, dan kelas untuk objek dengan operasi.

Jika struktur data Anda tidak memerlukan kontrol akses dan tidak memiliki operasi khusus selain get / set, gunakan struct. Ini membuatnya jelas bahwa semua struktur itu adalah wadah untuk data.

Jika struktur Anda memiliki operasi yang memodifikasi struktur dengan cara yang lebih kompleks, gunakan kelas. Kelas juga dapat berinteraksi dengan kelas lain melalui argumen ke metode.

Anggap saja sebagai perbedaan antara batu bata dan pesawat terbang. Bata adalah sebuah struct; memiliki panjang, lebar dan tinggi. Itu tidak bisa berbuat banyak. Sebuah pesawat bisa memiliki beberapa operasi yang bermutasi, entah bagaimana, seperti memindahkan permukaan kontrol dan mengubah dorongan mesin. Akan lebih baik sebagai kelas.

Michael K.
sumber
2
"Gunakan struct untuk konstruk data murni, dan kelas untuk objek dengan operasi" di C # ini omong kosong. Lihat jawaban saya di bawah ini.
bytedev
1
"Gunakan struct untuk konstruk data murni, dan kelas untuk objek dengan operasi." Ini berlaku untuk C / C ++, bukan C #
taktak004