Kapan saya harus menggunakan struct daripada kelas di C #?

1391

Kapan Anda harus menggunakan struct dan bukan kelas di C #? Model konseptual saya adalah bahwa struct digunakan pada saat-saat ketika item tersebut hanyalah kumpulan tipe nilai . Suatu cara untuk secara logis mengikat mereka semua menjadi satu kesatuan yang kohesif.

Saya menemukan aturan-aturan ini di sini :

  • Sebuah struct harus mewakili nilai tunggal.
  • Sebuah struct harus memiliki jejak memori kurang dari 16 byte.
  • Sebaiknya struct tidak diubah setelah pembuatan.

Apakah aturan ini berfungsi? Apa arti struct secara semantik?

Alex Baranosky
sumber
248
System.Drawing.Rectanglemelanggar ketiga aturan ini.
ChrisW
4
ada beberapa game komersial yang ditulis dalam C #, intinya adalah bahwa mereka digunakan untuk kode yang dioptimalkan
BlackTigerX
25
Struktur memberikan kinerja yang lebih baik ketika Anda memiliki koleksi kecil tipe nilai yang ingin Anda kelompokkan bersama. Ini terjadi setiap saat dalam pemrograman game, misalnya, sebuah simpul dalam model 3D akan memiliki posisi, tekstur dan koordinat yang normal, itu juga umumnya akan berubah. Sebuah model tunggal mungkin memiliki beberapa ribu simpul, atau mungkin memiliki selusin, tetapi struct menyediakan lebih sedikit overhead secara keseluruhan dalam skenario penggunaan ini. Saya telah memverifikasi ini melalui desain mesin saya sendiri.
Chris D.
4
@ Chris Saya mengerti, tetapi bukankah nilai-nilai itu mewakili sebuah persegi panjang, yang merupakan, nilai "tunggal"? Seperti Vector3D atau Warna, mereka juga beberapa nilai di dalamnya, tapi saya pikir mereka mewakili nilai tunggal?
Marson Mao

Jawaban:

604

Sumber yang dirujuk oleh OP memiliki kredibilitas ... tetapi bagaimana dengan Microsoft - bagaimana pendirian penggunaan struct? Saya mencari beberapa pembelajaran tambahan dari Microsoft , dan inilah yang saya temukan:

Pertimbangkan mendefinisikan struktur alih-alih kelas jika instance tipe kecil dan umumnya berumur pendek atau umumnya tertanam dalam objek lain.

Jangan mendefinisikan struktur kecuali tipe memiliki semua karakteristik berikut:

  1. Secara logis mewakili nilai tunggal, mirip dengan tipe primitif (integer, dobel, dan sebagainya).
  2. Ini memiliki ukuran instance lebih kecil dari 16 byte.
  3. Itu tidak berubah.
  4. Itu tidak harus sering kotak.

Microsoft secara konsisten melanggar aturan itu

Baiklah, # 2 dan # 3. Kamus kesayangan kami memiliki 2 struct internal:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

* Sumber Referensi

Sumber 'JonnyCantCode.com' mendapat 3 dari 4 - cukup dimaafkan karena # 4 mungkin tidak akan menjadi masalah. Jika Anda menemukan tinju struct, pikirkan kembali arsitektur Anda.

Mari kita lihat mengapa Microsoft akan menggunakan struct ini:

  1. Setiap struct, Entrydan Enumerator, mewakili nilai tunggal.
  2. Kecepatan
  3. Entrytidak pernah dilewatkan sebagai parameter di luar kelas Kamus. Penyelidikan lebih lanjut menunjukkan bahwa untuk memenuhi implementasi IEnumerable, Kamus menggunakan Enumeratorstruct yang disalin setiap kali seorang enumerator diminta ... masuk akal.
  4. Internal ke kelas Kamus. Enumeratorbersifat publik karena Kamus enumerable dan harus memiliki aksesibilitas yang sama ke implementasi antarmuka IEnumerator - mis. pengambil IEnumerator.

Pembaruan - Selain itu, sadari bahwa ketika struct mengimplementasikan antarmuka - seperti yang dilakukan Enumerator - dan dilemparkan ke tipe yang diimplementasikan, struct menjadi tipe referensi dan dipindahkan ke heap. Internal untuk kelas Dictionary, Enumerator adalah masih jenis nilai. Namun, begitu metode memanggil GetEnumerator(), tipe referensi IEnumeratordikembalikan.

Apa yang tidak kita lihat di sini adalah upaya atau bukti persyaratan untuk menjaga struct tetap atau mempertahankan ukuran instance hanya 16 byte atau kurang:

  1. Tidak ada dalam struct di atas yang dinyatakan readonly- tidak dapat diubah
  2. Ukuran struct ini bisa lebih dari 16 byte
  3. Entrymemiliki seumur hidup belum ditentukan (dari Add(), untuk Remove(), Clear()atau pengumpulan sampah);

Dan ... 4. Kedua struct menyimpan TKey dan TValue, yang kita semua tahu cukup mampu menjadi tipe referensi (info bonus tambahan)

Terlepas dari kunci-kunci yang dihancurkan, kamus-kamus lebih cepat sebagian karena memunculkan struct lebih cepat daripada tipe referensi. Di sini, saya punya Dictionary<int, int>yang menyimpan 300.000 bilangan bulat acak dengan kunci yang bertambah secara berurutan.

Kapasitas: 312874
MemSize: 2660827 bytes
Selesai Mengubah Ukuran: 5ms
Total waktu untuk mengisi: 889ms

Kapasitas : jumlah elemen yang tersedia sebelum array internal harus diubah ukurannya.

MemSize : ditentukan dengan membuat serial kamus ke dalam MemoryStream dan mendapatkan panjang byte (cukup akurat untuk tujuan kita).

Resize Selesai : waktu yang diperlukan untuk mengubah ukuran larik internal dari 150862 elemen menjadi 312874 elemen. Ketika Anda mengetahui bahwa setiap elemen disalin secara berurutan Array.CopyTo(), itu tidak terlalu buruk.

Total waktu untuk mengisi : diakui miring karena penebangan dan OnResizeacara yang saya tambahkan ke sumber; Namun, masih mengesankan untuk mengisi 300k bilangan bulat sambil mengubah ukuran 15 kali selama operasi. Hanya karena penasaran, berapa total waktu yang harus diisi jika saya sudah tahu kapasitasnya? 13 ms

Jadi, sekarang, bagaimana jika Entrykelas? Apakah waktu atau metrik ini benar-benar berbeda jauh?

Kapasitas: 312874
MemSize: 2660827 bytes
Selesai Mengubah Ukuran: 26ms
Total waktu untuk mengisi: 964ms

Jelas, perbedaan besar adalah dalam mengubah ukuran. Adakah perbedaan jika Kamus diinisialisasi dengan Kapasitas? Tidak cukup untuk peduli dengan ... 12ms .

Yang terjadi adalah, karena Entrymerupakan struct, itu tidak memerlukan inisialisasi seperti tipe referensi. Ini adalah keindahan dan kutukan dari tipe nilai. Untuk menggunakan Entrysebagai tipe referensi, saya harus memasukkan kode berikut:

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  

Alasan saya harus menginisialisasi setiap elemen array Entrysebagai tipe referensi dapat ditemukan di MSDN: Structure Design . Pendeknya:

Jangan berikan konstruktor default untuk struktur.

Jika struktur mendefinisikan konstruktor default, ketika array struktur dibuat, runtime bahasa umum secara otomatis mengeksekusi konstruktor default pada setiap elemen array.

Beberapa kompiler, seperti kompiler C #, tidak mengizinkan struktur memiliki konstruktor default.

Ini sebenarnya cukup sederhana dan kami akan meminjam dari Tiga Hukum Robotika Asimov :

  1. Struct harus aman digunakan
  2. Struct harus menjalankan fungsinya secara efisien, kecuali jika ini akan melanggar aturan # 1
  3. Struct harus tetap utuh selama penggunaannya kecuali kehancurannya diperlukan untuk memenuhi aturan # 1

... apa yang kita ambil dari ini : singkatnya, bertanggung jawab dengan penggunaan tipe nilai. Mereka cepat dan efisien, tetapi memiliki kemampuan untuk menyebabkan banyak perilaku tak terduga jika tidak dikelola dengan baik (yaitu salinan yang tidak disengaja).

Ekstrak
sumber
8
Adapun aturan Microsoft, aturan tentang immutabilitas tampaknya dirancang untuk mencegah penggunaan tipe nilai sedemikian rupa sehingga perilaku mereka akan berbeda dari tipe referensi, meskipun fakta bahwa semantik nilai yang bisa berubah-ubah bisa berguna . Jika memiliki jenis bisa berubah-ubah akan membuatnya lebih mudah untuk dikerjakan, dan jika lokasi penyimpanan jenis tersebut harus terpisah secara logis dari satu sama lain, jenis tersebut haruslah merupakan struktur yang "bisa berubah".
supercat
23
Ingatlah bahwa hanya baca! = Tidak berubah.
Justin Morgan
2
Fakta bahwa banyak tipe Microsoft melanggar aturan-aturan itu tidak mewakili masalah dengan tipe-tipe itu, tetapi lebih menunjukkan bahwa aturan tidak boleh berlaku untuk semua tipe struktur. Jika suatu struktur mewakili satu entitas [seperti dengan Decimalatau DateTime], maka jika tidak mau mematuhi tiga aturan lainnya, itu harus digantikan oleh kelas. Jika suatu struktur menyimpan kumpulan variabel yang tetap, yang masing-masingnya dapat memiliki nilai apa pun yang akan valid untuk jenisnya [misalnya Rectangle], maka ia harus mematuhi aturan yang berbeda , beberapa di antaranya bertentangan dengan yang untuk struktur "nilai tunggal" .
supercat
4
@Abstract: Beberapa orang akan membenarkan Dictionarytipe entri dengan alasan itu hanya tipe internal, kinerja dianggap lebih penting daripada semantik, atau alasan lainnya. Maksud saya adalah bahwa jenis seperti Rectangleharus memiliki kontennya diekspos sebagai bidang yang dapat diedit secara individual bukan "karena" manfaat kinerja lebih besar daripada ketidaksempurnaan semantik yang dihasilkan, tetapi karena jenis tersebut secara semantik mewakili sekumpulan nilai independen yang tetap , dan karenanya struktur yang dapat diubah adalah keduanya lebih berkinerja dan secara semantik unggul .
supercat
2
@supercat: Saya setuju ... dan inti dari jawaban saya adalah bahwa 'pedoman' cukup lemah dan struct harus digunakan dengan pengetahuan penuh dan pemahaman tentang perilaku. Lihat jawaban saya di struct bisa berubah di sini: stackoverflow.com/questions/8108920/…
IAbstract
155

Kapanpun Anda:

  1. tidak perlu polimorfisme,
  2. ingin nilai semantik, dan
  3. ingin menghindari alokasi tumpukan dan overhead pengumpulan sampah yang terkait.

Namun, peringatannya adalah bahwa struct (sewenang-wenang besar) lebih mahal untuk dilewatkan daripada referensi kelas (biasanya satu kata mesin), sehingga kelas bisa berakhir lebih cepat dalam praktik.

dsimcha
sumber
1
Itu hanya satu "peringatan". Sebaiknya pertimbangkan juga "mengangkat" tipe-tipe nilai dan kasus-kasus seperti (Guid)null(tidak apa-apa untuk memberikan null ke tipe referensi), di antara hal-hal lain.
1
lebih mahal daripada di C / C ++? di C ++ cara yang disarankan adalah dengan melewatkan objek berdasarkan nilai
Ion Todirel
@IonTodirel Bukan karena alasan keamanan memori, bukan kinerja? Itu selalu trade-off, tetapi melewati 32 B dengan stack selalu (TM) akan lebih lambat daripada melewati referensi 4 B dengan register. Namun , perhatikan juga bahwa penggunaan "nilai / referensi" sedikit berbeda dalam C # dan C ++ - saat Anda meneruskan referensi ke objek, Anda masih meneruskan dengan nilai, meskipun Anda meneruskan referensi (Anda ' kembali nilai referensi, bukan referensi ke referensi, pada dasarnya). Ini bukan nilai semantik , tetapi secara teknis "nilai demi nilai".
Luaan
@Luaan Menyalin hanya satu aspek dari biaya. Ketidaksadaran ekstra karena penunjuk / referensi juga biaya per akses. Dalam beberapa kasus, struct bahkan dapat dipindahkan dan dengan demikian bahkan tidak perlu disalin.
Onur
@Onur itu menarik. Bagaimana Anda "bergerak" tanpa menyalin? Saya pikir instruksi "mov" asm sebenarnya tidak "bergerak". Itu salinan.
Sayap Sendon
148

Saya tidak setuju dengan aturan yang diberikan di pos asli. Ini aturan saya:

1) Anda menggunakan struct untuk kinerja ketika disimpan dalam array. (lihat juga Kapan struct jawabannya? )

2) Anda membutuhkannya dalam kode yang meneruskan data terstruktur ke / dari C / C ++

3) Jangan menggunakan struct kecuali Anda membutuhkannya:

  • Mereka berperilaku berbeda dari "objek normal" ( tipe referensi ) di bawah penugasan dan ketika lewat sebagai argumen, yang dapat menyebabkan perilaku yang tidak terduga; ini sangat berbahaya jika orang yang melihat kode tidak tahu mereka berurusan dengan struct.
  • Mereka tidak bisa diwarisi.
  • Melewati struct sebagai argumen lebih mahal daripada kelas.
ILoveFortran
sumber
4
+1 Ya, saya setuju sepenuhnya pada # 1 (ini adalah keuntungan besar ketika berhadapan dengan hal-hal seperti gambar, dll) dan untuk menunjukkan bahwa mereka berbeda dari "objek normal" dan ada cara mengetahui hal ini kecuali dengan pengetahuan yang ada atau memeriksa tipe itu sendiri. Juga, Anda tidak dapat memberikan nilai nol ke jenis struct :-) Ini sebenarnya adalah satu kasus di mana saya hampir berharap ada beberapa 'Hungaria' untuk tipe nilai non-Core atau kata kunci 'struct' wajib di situs deklarasi variabel .
@ pst: Memang benar bahwa seseorang harus mengetahui sesuatu adalah structuntuk mengetahui bagaimana ia akan berperilaku, tetapi jika ada sesuatu structdengan bidang yang terbuka, itu saja yang harus diketahui. Jika suatu objek mengekspos properti dari tipe bidang bidang-terbuka, dan jika kode membaca struct itu ke variabel dan memodifikasi, seseorang dapat dengan aman memprediksi bahwa tindakan seperti itu tidak akan mempengaruhi objek yang propertinya dibaca kecuali atau sampai struct ditulis kembali. Sebaliknya, jika properti adalah tipe kelas yang bisa berubah, membacanya dan memodifikasinya mungkin memperbarui objek yang mendasarinya seperti yang diharapkan, tapi ...
supercat
... mungkin juga pada akhirnya tidak mengubah apa pun, atau mungkin mengubah atau merusak objek yang tidak ingin diubah. Memiliki kode yang semantiknya mengatakan "ubah variabel ini sesuka Anda; perubahan tidak akan melakukan apa pun hingga Anda secara eksplisit menyimpannya di suatu tempat" tampaknya lebih jelas daripada memiliki kode yang mengatakan "Anda mendapatkan referensi ke beberapa objek, yang mungkin dibagikan dengan nomor berapa pun referensi lain, atau mungkin tidak dibagikan sama sekali; Anda harus mencari tahu siapa lagi yang mungkin memiliki referensi untuk objek ini untuk mengetahui apa yang akan terjadi jika Anda mengubahnya. "
supercat
Temukan dengan # 1. Daftar yang penuh dengan struct dapat memeras data yang jauh lebih relevan ke dalam cache L1 / L2 daripada daftar yang penuh dengan referensi objek, (untuk struct ukuran yang tepat).
Matt Stephenson
2
Warisan jarang merupakan alat yang tepat untuk pekerjaan itu, dan terlalu banyak beralasan tentang kinerja tanpa membuat profil adalah ide yang buruk. Pertama, struct dapat diteruskan dengan referensi. Kedua, melewati dengan referensi atau berdasarkan nilai jarang merupakan masalah kinerja yang signifikan. Terakhir, Anda tidak memperhitungkan alokasi tumpukan tambahan dan pengumpulan sampah yang perlu dilakukan untuk kelas. Secara pribadi, saya lebih suka menganggap struct sebagai data lama dan kelas sebagai hal yang melakukan hal - hal (objek) meskipun Anda dapat mendefinisikan metode pada struct juga.
weberc2
88

Gunakan struct ketika Anda ingin semantik nilai sebagai lawan dari semantik referensi.

Edit

Tidak yakin mengapa orang downvoting ini, tetapi ini adalah poin yang valid, dan dibuat sebelum op menjelaskan pertanyaannya, dan itu adalah alasan dasar yang paling mendasar untuk sebuah struct.

Jika Anda memerlukan semantik referensi, Anda perlu kelas bukan struct.

JoshBerke
sumber
13
Semua orang tahu itu. Sepertinya dia mencari lebih dari jawaban "struct is a type type".
TheSmurf
21
Ini adalah kasus paling mendasar dan harus dinyatakan untuk siapa saja yang membaca posting ini dan tidak tahu itu.
JoshBerke
3
Bukannya jawaban ini tidak benar; jelas itu. Bukan itu intinya.
TheSmurf
55
@Josh: Bagi siapa pun yang belum mengetahuinya, cukup mengatakan itu adalah jawaban yang tidak cukup, karena kemungkinan besar mereka juga tidak tahu apa artinya.
TheSmurf
1
Saya baru saja menurunkan ini karena saya pikir salah satu jawaban lain harus di atas - setiap jawaban yang mengatakan "Untuk interop dengan kode yang tidak dikelola, jika tidak hindari".
Daniel Earwicker
59

Selain jawaban "itu adalah nilai", satu skenario khusus untuk menggunakan struct adalah ketika Anda tahu bahwa Anda memiliki serangkaian data yang menyebabkan masalah pengumpulan sampah, dan Anda memiliki banyak objek. Misalnya, daftar besar / array instance Person. Metafora alami di sini adalah kelas, tetapi jika Anda memiliki jumlah instance Orang yang berumur panjang, mereka dapat menyumbat GEN-2 dan menyebabkan kios-kios GC. Jika skenario menjaminnya, salah satu pendekatan potensial di sini adalah dengan menggunakan array (bukan daftar) Orang struct , yaitu Person[]. Sekarang, alih-alih memiliki jutaan objek di GEN-2, Anda memiliki sepotong tunggal pada LOH (saya mengasumsikan tidak ada string dll di sini - yaitu nilai murni tanpa referensi). Ini memiliki dampak GC yang sangat kecil.

Bekerja dengan data ini aneh, karena data mungkin terlalu besar untuk sebuah struct, dan Anda tidak ingin menyalin nilai lemak sepanjang waktu. Namun, mengaksesnya secara langsung dalam array tidak menyalin struct - itu di tempat (kontras dengan pengindeks daftar, yang tidak menyalin). Ini berarti banyak pekerjaan dengan indeks:

int index = ...
int id = peopleArray[index].Id;

Perhatikan bahwa menjaga nilai-nilai itu sendiri tidak berubah akan membantu di sini. Untuk logika yang lebih kompleks, gunakan metode dengan parameter by-ref:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

Sekali lagi, ini sudah di tempat - kami belum menyalin nilainya.

Dalam skenario yang sangat spesifik, taktik ini bisa sangat berhasil; Namun, itu adalah scernario yang cukup canggih yang harus dicoba hanya jika Anda tahu apa yang Anda lakukan dan mengapa. Default di sini adalah kelas.

Marc Gravell
sumber
+1 Jawaban yang menarik. Apakah Anda bersedia membagikan anekdot dunia nyata tentang pendekatan seperti itu yang digunakan?
Jordão
@Jordao aktif di ponsel, tetapi cari google untuk: + gravell + "assault by GC"
Marc Gravell
1
Terima kasih banyak. Saya menemukannya di sini .
Jordão
2
@MarcGravell Mengapa Anda menyebutkan: gunakan array (bukan daftar) ? ListSaya percaya, menggunakan Arraylayar belakang. tidak ?
Royi Namir
4
@RoyiNamir Saya ingin tahu tentang ini juga, tapi saya percaya jawabannya ada pada paragraf kedua dari jawaban Marc. "Namun, mengaksesnya secara langsung dalam array tidak menyalin struct - itu di tempat (kontras dengan pengindeks daftar, yang tidak menyalin)."
user1323245
40

Dari spesifikasi Bahasa C # :

1.7 Struktur

Seperti kelas, struct adalah struktur data yang dapat berisi anggota data dan anggota fungsi, tetapi tidak seperti kelas, struct adalah tipe nilai dan tidak memerlukan alokasi tumpukan. Variabel tipe struct langsung menyimpan data struct, sedangkan variabel tipe kelas menyimpan referensi ke objek yang dialokasikan secara dinamis. Tipe struct tidak mendukung pewarisan yang ditentukan pengguna, dan semua tipe struct secara implisit mewarisi dari objek type.

Struct sangat berguna untuk struktur data kecil yang memiliki nilai semantik. Bilangan kompleks, titik dalam sistem koordinat, atau pasangan nilai kunci dalam kamus adalah semua contoh struct yang baik. Penggunaan struct daripada kelas untuk struktur data kecil dapat membuat perbedaan besar dalam jumlah alokasi memori yang dilakukan aplikasi. Misalnya, program berikut ini membuat dan menginisialisasi array 100 poin. Dengan Point diimplementasikan sebagai sebuah kelas, 101 objek terpisah dipakai - satu untuk array dan masing-masing untuk 100 elemen.

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

Alternatif lain adalah menjadikan Point sebagai struct.

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

Sekarang, hanya satu objek yang dipakai - satu untuk array - dan instance Point disimpan in-line dalam array.

Konstruktor struktur dipanggil dengan operator baru, tetapi itu tidak menyiratkan bahwa memori sedang dialokasikan. Alih-alih secara dinamis mengalokasikan objek dan mengembalikan referensi ke sana, konstruktor struct hanya mengembalikan nilai struct itu sendiri (biasanya di lokasi sementara di stack), dan nilai ini kemudian disalin seperlunya.

Dengan kelas, dimungkinkan untuk dua variabel untuk referensi objek yang sama dan dengan demikian mungkin untuk operasi pada satu variabel untuk mempengaruhi objek yang dirujuk oleh variabel lain. Dengan struct, masing-masing variabel memiliki salinan data mereka sendiri, dan tidak mungkin operasi yang satu mempengaruhi yang lain. Sebagai contoh, output yang dihasilkan oleh fragmen kode berikut ini tergantung pada apakah Point adalah kelas atau struct.

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

Jika Point adalah kelas, outputnya adalah 20 karena a dan b mereferensikan objek yang sama. Jika Point adalah struct, outputnya adalah 10 karena penugasan a ke b menciptakan salinan nilai, dan salinan ini tidak terpengaruh oleh penugasan selanjutnya ke ax

Contoh sebelumnya menyoroti dua keterbatasan struct. Pertama, menyalin seluruh struct biasanya kurang efisien daripada menyalin referensi objek, sehingga penugasan dan nilai parameter yang lewat bisa lebih mahal dengan struct daripada dengan tipe referensi. Kedua, kecuali untuk parameter ref dan out, tidak mungkin membuat referensi untuk struct, yang mengesampingkan penggunaannya dalam sejumlah situasi.

Luke Baughan
sumber
4
Sementara fakta bahwa referensi ke struct tidak dapat bertahan kadang-kadang merupakan batasan, itu juga merupakan karakteristik yang sangat berguna. Salah satu kelemahan utama .net adalah bahwa tidak ada cara yang layak untuk melewatkan kode luar referensi ke objek yang bisa berubah tanpa selamanya kehilangan kendali atas objek itu. Sebaliknya, seseorang dapat dengan aman memberikan metode luar refke struktur yang bisa berubah dan tahu bahwa setiap mutasi yang dilakukan metode luar akan dilakukan sebelum kembali. Sayang sekali .net tidak memiliki konsep parameter fana dan nilai fungsi pengembalian, karena ...
supercat
4
... yang akan memungkinkan semantik menguntungkan dari struct yang berlalu refdicapai dengan objek kelas. Pada dasarnya, variabel lokal, parameter, dan nilai-nilai fungsi kembali bisa bertahan (default), dikembalikan, atau sesaat. Kode akan dilarang menyalin hal-hal fana ke apa pun yang akan hidup lebih lama dari ruang lingkup saat ini. Hal-hal yang dapat dikembalikan akan seperti hal-hal yang fana kecuali bahwa mereka dapat dikembalikan dari suatu fungsi. Nilai pengembalian suatu fungsi akan terikat oleh batasan ketat yang berlaku pada parameter "yang dapat dikembalikan".
supercat
34

Structs baik untuk representasi data atom, di mana data tersebut dapat disalin beberapa kali oleh kode. Mengkloning suatu objek secara umum lebih mahal daripada menyalin struct, karena melibatkan mengalokasikan memori, menjalankan konstruktor dan deallocating / pengumpulan sampah ketika dilakukan dengannya.

Franci Penov
sumber
4
Ya, tetapi struct besar bisa lebih mahal daripada referensi kelas (ketika melewati metode).
Alex
27

Ini aturan dasar.

  • Jika semua bidang anggota adalah tipe nilai buat sebuah struct .

  • Jika salah satu bidang anggota adalah tipe referensi, buat kelas . Ini karena bidang tipe referensi memerlukan alokasi heap.

Exmaples

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}
Usman Zafar
sumber
3
Tipe referensi yang tidak dapat diubah seperti stringsecara semantik setara dengan nilai-nilai, dan menyimpan referensi ke objek yang tidak dapat diubah ke dalam suatu bidang tidak memerlukan alokasi heap. Perbedaan antara struct dengan bidang publik terbuka dan objek kelas dengan bidang publik terbuka adalah bahwa diberi urutan kode var q=p; p.X=4; q.X=5;, p.Xakan memiliki nilai 4 jika amerupakan tipe struktur, dan 5 jika itu adalah tipe kelas. Jika seseorang ingin dapat dengan mudah memodifikasi anggota tipe, seseorang harus memilih 'kelas' atau 'struct' berdasarkan apakah seseorang ingin perubahan qmempengaruhi p.
supercat
Ya saya setuju variabel referensi akan ada di stack tetapi objek yang dirujuk akan ada di heap. Meskipun struct dan kelas berperilaku berbeda ketika ditugaskan ke variabel yang berbeda, tetapi saya tidak berpikir itu faktor penentu yang kuat.
Usman Zafar
Struct yang bisa berubah dan kelas yang bisa berubah berperilaku sangat berbeda; jika satu benar, yang lain kemungkinan besar akan salah. Saya tidak yakin bagaimana perilaku tidak akan menjadi faktor penentu dalam menentukan apakah akan menggunakan struct atau kelas.
supercat
Saya mengatakan itu bukan faktor penentu yang kuat karena sering kali ketika Anda membuat kelas atau struct Anda tidak yakin bagaimana itu akan digunakan. Jadi Anda berkonsentrasi pada bagaimana hal-hal lebih masuk akal dari perspektif desain. Lagi pula saya belum pernah melihat di satu tempat di perpustakaan .NET di mana sebuah struct berisi variabel referensi.
Usman Zafar
1
Tipe struktur ArraySegment<T>merangkum a T[], yang selalu merupakan tipe kelas. Tipe struktur KeyValuePair<TKey,TValue>sering digunakan dengan tipe kelas sebagai parameter generik.
supercat
19

Pertama: Skenario interop atau ketika Anda perlu menentukan tata letak memori

Kedua: Ketika data hampir sama ukurannya dengan pointer referensi pula.

SM.
sumber
17

Anda perlu menggunakan "struct" dalam situasi di mana Anda ingin secara eksplisit menentukan tata letak memori menggunakan StructLayoutAttribute - biasanya untuk PInvoke.

Sunting: Komentar menunjukkan bahwa Anda dapat menggunakan kelas atau struct dengan StructLayoutAttribute dan itu memang benar. Dalam praktiknya, Anda biasanya akan menggunakan struct - itu dialokasikan pada tumpukan vs tumpukan yang masuk akal jika Anda hanya meneruskan argumen ke pemanggilan metode yang tidak dikelola.

Maurice Flanagan
sumber
5
StructLayoutAttribute dapat diterapkan ke struct atau kelas jadi ini bukan alasan untuk menggunakan struct.
Stephen Martin
Mengapa masuk akal jika Anda hanya menyampaikan argumen ke panggilan metode yang tidak dikelola?
David Klempfner
16

Saya menggunakan struct untuk mengemas atau membongkar segala bentuk format komunikasi biner. Itu termasuk membaca atau menulis ke disk, daftar vertex DirectX, protokol jaringan, atau berurusan dengan data terenkripsi / terkompresi.

Tiga pedoman yang Anda daftarkan belum berguna bagi saya dalam konteks ini. Ketika saya perlu menulis empat ratus byte barang dalam Urutan Tertentu, saya akan mendefinisikan struct empat ratus byte, dan saya akan mengisinya dengan nilai apa pun yang tidak terkait yang seharusnya dimiliki, dan saya akan untuk mengaturnya dengan cara apa pun yang paling masuk akal saat itu. (Oke, empat ratus byte akan sangat aneh - tetapi ketika saya sedang menulis file Excel untuk mencari nafkah, saya berurusan dengan struct hingga sekitar empat puluh byte di seluruh, karena itulah seberapa besar beberapa catatan BIFF ADALAH.)

mjfgates
sumber
Tidak bisakah Anda dengan mudah menggunakan tipe referensi untuk itu?
David Klempfner
15

Dengan pengecualian pada valuetypes yang digunakan secara langsung oleh runtime dan berbagai lainnya untuk keperluan PInvoke, Anda hanya boleh menggunakan valuetypes dalam 2 skenario.

  1. Ketika Anda perlu menyalin semantik.
  2. Ketika Anda membutuhkan inisialisasi otomatis, biasanya dalam array jenis ini.
leppie
sumber
# 2 tampaknya menjadi bagian dari alasan prevalensi struct di kelas pengumpulan .Net ..
IAbstract
Jika hal pertama yang dilakukan seseorang saat membuat lokasi penyimpanan tipe kelas adalah membuat instance baru dari tipe itu, menyimpan referensi untuknya di lokasi itu, dan tidak pernah menyalin referensi di tempat lain atau menimpanya, maka sebuah struktur dan kelas akan berperilaku identik. Structs memiliki cara standar yang nyaman untuk menyalin semua bidang dari satu contoh ke yang lain, dan umumnya akan menawarkan kinerja yang lebih baik dalam kasus di mana seseorang tidak akan pernah menduplikasi referensi ke kelas (kecuali untuk thisparameter sementara yang digunakan untuk memanggil metode-metodenya); kelas memungkinkan seseorang untuk menggandakan referensi.
supercat
13

.NET mendukung value typesdan reference types(di Jawa, Anda hanya dapat menentukan jenis referensi). Contoh reference typesdialokasikan di tumpukan dikelola dan sampah dikumpulkan ketika tidak ada referensi yang beredar untuk mereka. Contoh value types, di sisi lain, dialokasikan dalam stack, dan karenanya memori yang dialokasikan direklamasi segera setelah ruang lingkup mereka berakhir. Dan tentu saja, value typesdilewati oleh nilai, dan reference typesdengan referensi. Semua tipe data primitif C #, kecuali untuk System.String, adalah tipe nilai.

Kapan harus menggunakan struct di atas kelas,

Di C #, structsadalah value types, kelas adalah reference types. Anda dapat membuat tipe nilai, dalam C #, menggunakan enumkata kunci dan structkata kunci. Menggunakan value typebukan reference typeakan menghasilkan lebih sedikit objek pada heap yang dikelola, yang menghasilkan lebih sedikit beban pada pengumpul sampah, lebih jarang siklus GC, dan akibatnya kinerja yang lebih baik. Namun, value typesmemiliki kelemahan mereka juga. Melewati yang besar structpasti lebih mahal daripada melewati referensi, itu salah satu masalah yang jelas. Masalah lainnya adalah overhead yang terkait dengannya boxing/unboxing. Jika Anda bertanya-tanya apa boxing/unboxingartinya, ikuti tautan ini untuk penjelasan boxingdanunboxing. Terlepas dari kinerja, ada kalanya Anda hanya perlu tipe untuk memiliki semantik nilai, yang akan sangat sulit (atau jelek) untuk diimplementasikan jika reference typeshanya itu yang Anda miliki. Anda value typeshanya harus menggunakan , Ketika Anda perlu menyalin semantik atau membutuhkan inisialisasi otomatis, biasanya dalam arraysjenis ini.

Sujit
sumber
Menyalin struktur kecil atau melewati nilai sama murahnya dengan menyalin atau melewati referensi kelas, atau melewati struktur ref. Melewati setiap struktur ukuran dengan refbiaya sama seperti melewati referensi kelas dengan nilai. Menyalin semua struktur ukuran atau meneruskan dengan nilai lebih murah daripada melakukan salinan defensif dari objek kelas dan menyimpan atau melewati referensi untuk itu. Kelas waktu besar lebih baik daripada struct untuk menyimpan nilai adalah (1) ketika kelas tidak dapat diubah (sehingga untuk menghindari menyalin defensif), dan setiap contoh yang dibuat akan dilewatkan banyak, atau ...
supercat
... (2) ketika karena berbagai alasan struct akan tidak dapat digunakan [misalnya karena seseorang perlu menggunakan referensi bersarang untuk sesuatu seperti pohon, atau karena seseorang membutuhkan polimorfisme]. Perhatikan bahwa ketika menggunakan tipe nilai, seseorang biasanya harus mengekspos bidang yang secara langsung tidak ada alasan tertentu untuk tidak (sedangkan dengan sebagian besar tipe kelas bidang harus dibungkus dalam properti). Banyak yang disebut "kejahatan" dari tipe nilai yang dapat berubah berasal dari pembungkus bidang yang tidak perlu di properti (mis. Sementara beberapa kompiler akan memungkinkan seseorang untuk memanggil properti setter pada struktur read-only karena kadang-kadang akan ...
supercat
... melakukan hal yang benar, semua penyusun akan dengan benar menolak upaya untuk secara langsung mengatur bidang pada struktur tersebut; cara terbaik untuk memastikan kompiler menolak readOnlyStruct.someMember = 5;adalah tidak membuat someMemberproperti hanya baca, tetapi menjadikannya bidang.
supercat
12

Sebuah struct adalah jenis nilai. Jika Anda menetapkan struct ke variabel baru, variabel baru akan berisi salinan aslinya.

public struct IntStruct {
    public int Value {get; set;}
}

Eksekusi hasil berikut dalam 5 contoh struct yang disimpan dalam memori:

var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1;  // A copy is made
var struct3 = struct2;  // A copy is made
var struct4 = struct3;  // A copy is made
var struct5 = struct4;  // A copy is made

// NOTE: A "copy" will occur when you pass a struct into a method parameter.
// To avoid the "copy", use the ref keyword.

// Although structs are designed to use less system resources
// than classes.  If used incorrectly, they could use significantly more.

Sebuah kelas adalah tipe referensi. Saat Anda menetapkan kelas ke variabel baru, variabel tersebut berisi referensi ke objek kelas asli.

public class IntClass {
    public int Value {get; set;}
}

Eksekusi hasil berikut ini hanya dalam satu contoh objek kelas dalam memori.

var class1 = new IntClass() { Value = 0 };
var class2 = class1;  // A reference is made to class1
var class3 = class2;  // A reference is made to class1
var class4 = class3;  // A reference is made to class1
var class5 = class4;  // A reference is made to class1  

Struct s dapat meningkatkan kemungkinan kesalahan kode. Jika objek nilai diperlakukan seperti objek referensi yang bisa berubah, pengembang mungkin akan terkejut ketika perubahan yang dilakukan tiba-tiba hilang.

var struct1 = new IntStruct() { Value = 0 };
var struct2 = struct1;
struct2.Value = 1;
// At this point, a developer may be surprised when 
// struct1.Value is 0 and not 1
Jason Williams
sumber
12

Saya membuat tolok ukur kecil dengan BenchmarkDotNet untuk mendapatkan pemahaman yang lebih baik tentang manfaat "struct" dalam angka. Saya sedang menguji pengulangan melalui array (atau daftar) struct (atau kelas). Membuat susunan atau daftar tersebut di luar jangkauan tolok ukur - jelas bahwa "kelas" yang lebih berat akan menggunakan lebih banyak memori, dan akan melibatkan GC.

Jadi kesimpulannya adalah: berhati-hatilah dengan LINQ dan hidden structs boxing / unboxing dan gunakan structs untuk optimasi mikro secara ketat tetap dengan array.

PS Patokan lain tentang melewati struct / kelas melalui tumpukan panggilan ada https://stackoverflow.com/a/47864451/506147

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Core   : .NET Core 4.6.25211.01, 64bit RyuJIT


          Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|
   TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |
  TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |
  TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |
 TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |
   TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |
  TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |
   TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |
  TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |
  TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |
 TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |
   TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |
  TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

Kode:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
    [ClrJob, CoreJob]
    [HtmlExporter, MarkdownExporter]
    [MemoryDiagnoser]
    public class BenchmarkRef
    {
        public class C1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        public struct S1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        List<C1> testListClass = new List<C1>();
        List<S1> testListStruct = new List<S1>();
        C1[] testArrayClass;
        S1[] testArrayStruct;
        public BenchmarkRef()
        {
            for(int i=0;i<1000;i++)
            {
                testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });
                testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });
            }
            testArrayClass = testListClass.ToArray();
            testArrayStruct = testListStruct.ToArray();
        }

        [Benchmark]
        public int TestListClass()
        {
            var x = 0;
            foreach(var i in testListClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayClass()
        {
            var x = 0;
            foreach (var i in testArrayClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestListStruct()
        {
            var x = 0;
            foreach (var i in testListStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayStruct()
        {
            var x = 0;
            foreach (var i in testArrayStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestLinqClass()
        {
            var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }

        [Benchmark]
        public int TestLinqStruct()
        {
            var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }
    }
Roman Pokrovskij
sumber
Sudahkah Anda mengetahui mengapa struct jauh lebih lambat saat digunakan dalam daftar dan semacamnya? Apakah karena tinju tersembunyi dan unboxing yang Anda sebutkan? Jika demikian mengapa itu terjadi?
Marko Grdinic
Mengakses struct dalam array harus lebih cepat hanya karena tidak diperlukan referensi tambahan. Boxing / Unboxing adalah kasus untuk linq.
Roman Pokrovskij
10

Tipe struktur dalam C # atau bahasa .net lainnya umumnya digunakan untuk menampung hal-hal yang seharusnya berperilaku seperti kelompok nilai berukuran tetap. Aspek yang berguna dari tipe struktur adalah bahwa bidang instance tipe struktur dapat dimodifikasi dengan mengubah lokasi penyimpanan di mana ia disimpan, dan tidak dengan cara lain. Dimungkinkan untuk mengkodekan struktur sedemikian rupa sehingga satu-satunya cara untuk bermutasi bidang apa pun adalah dengan membangun contoh baru dan kemudian menggunakan penugasan struct untuk memutasi semua bidang target dengan menimpa mereka dengan nilai-nilai dari instance baru, tetapi kecuali sebuah struct tidak menyediakan sarana untuk membuat instance di mana bidangnya memiliki nilai-nilai non-default, semua bidangnya akan bisa berubah jika dan jika struct itu sendiri disimpan di lokasi yang bisa diubah.

Perhatikan bahwa mungkin untuk merancang tipe struktur sehingga pada dasarnya akan berperilaku seperti tipe kelas, jika struktur berisi bidang tipe kelas pribadi, dan mengarahkan anggotanya sendiri ke objek kelas yang dibungkus. Sebagai contoh, sebuah PersonCollectionmungkin menawarkan properti SortedByNamedan SortedById, keduanya memiliki referensi "tidak berubah" ke PersonCollection(diatur dalam konstruktor mereka) dan mengimplementasikannya GetEnumeratordengan memanggil salah satu creator.GetNameSortedEnumeratoratau creator.GetIdSortedEnumerator. Struct tersebut akan berperilaku seperti referensi ke a PersonCollection, kecuali bahwa GetEnumeratormetode mereka akan terikat pada metode yang berbeda di PersonCollection. Seseorang juga dapat memiliki struktur yang membungkus suatu bagian dari array (misalnya seseorang dapat mendefinisikan suatu ArrayRange<T>struktur yang akan menahan suatu yang T[]disebut Arr, suatu int Offset, dan suatu intLength, dengan properti yang diindeks yang, untuk indeks idxdalam kisaran 0 hingga Length-1, akan mengakses Arr[idx+Offset]). Sayangnya, jika fooini adalah contoh read-only dari struktur seperti itu, versi kompiler saat ini tidak akan memungkinkan operasi seperti foo[3]+=4;karena mereka tidak memiliki cara untuk menentukan apakah operasi tersebut akan mencoba menulis ke bidang foo.

Dimungkinkan juga untuk mendesain struktur untuk berperilaku seperti tipe nilai yang menyimpan koleksi berukuran variabel (yang akan tampak disalin setiap kali struct) tetapi satu-satunya cara untuk membuat pekerjaan itu adalah untuk memastikan bahwa tidak ada objek yang mana struct memegang referensi yang akan pernah terpapar pada apa pun yang mungkin bermutasi. Sebagai contoh, seseorang dapat memiliki struct mirip array yang menampung array privat, dan metode "put" yang diindeks membuat array baru yang isinya seperti aslinya kecuali untuk satu elemen yang diubah. Sayangnya, bisa agak sulit untuk membuat struct seperti itu bekerja secara efisien. Meskipun ada saat-saat semantik struct dapat menjadi nyaman (misalnya bisa meneruskan koleksi seperti array ke rutin, dengan penelepon dan callee keduanya mengetahui bahwa kode luar tidak akan mengubah koleksi,

supercat
sumber
10

Nah - Saya tidak sepenuhnya setuju dengan aturan. Mereka adalah pedoman yang baik untuk dipertimbangkan dengan kinerja dan standardisasi, tetapi tidak mengingat kemungkinan.

Seperti yang dapat Anda lihat di respons, ada banyak cara kreatif untuk menggunakannya. Jadi, pedoman ini perlu seperti itu, selalu demi kinerja dan efisiensi.

Dalam hal ini, saya menggunakan kelas untuk mewakili objek dunia nyata dalam bentuk yang lebih besar, saya menggunakan struct untuk mewakili objek yang lebih kecil yang memiliki penggunaan yang lebih tepat. Cara Anda mengatakannya, "keseluruhan yang lebih kohesif." Kata kunci menjadi kohesif. Kelas akan lebih banyak elemen berorientasi objek, sementara struct dapat memiliki beberapa karakteristik tersebut, meskipun pada skala yang lebih kecil. IMO.

Saya sering menggunakannya di tag Treeview dan Listview di mana atribut statis umum dapat diakses dengan sangat cepat. Saya selalu berjuang untuk mendapatkan info ini dengan cara lain. Sebagai contoh, dalam aplikasi database saya, saya menggunakan Treeview di mana saya memiliki Tabel, SP, Fungsi, atau objek lainnya. Saya membuat dan mengisi struct saya, memasukkannya ke dalam tag, menariknya keluar, mendapatkan data pilihan dan sebagainya. Saya tidak akan melakukan ini dengan kelas!

Saya mencoba dan menjaga mereka tetap kecil, menggunakannya dalam situasi instan, dan menjaga mereka agar tidak berubah. Adalah bijaksana untuk menyadari ingatan, alokasi, dan kinerja. Dan pengujian sangat diperlukan.

SnapJag
sumber
Struktur dapat digunakan secara bijaksana untuk merepresentasikan objek yang tidak dapat diubah yang ringan, atau mereka dapat digunakan secara masuk akal untuk merepresentasikan set tetap variabel terkait tetapi independen (misalnya koordinat suatu titik). Saran pada halaman itu baik untuk struct yang dirancang untuk melayani tujuan sebelumnya, tetapi salah untuk struct yang dirancang untuk melayani tujuan yang terakhir. Pemikiran saya saat ini adalah bahwa struct yang memiliki bidang pribadi umumnya harus memenuhi deskripsi yang ditunjukkan, tetapi banyak struct harus mengekspos seluruh negara mereka melalui bidang publik.
supercat
Jika spesifikasi untuk tipe "titik 3d" menunjukkan bahwa seluruh keadaannya diekspos melalui anggota yang dapat dibaca x, y, dan z, dan dimungkinkan untuk membuat instance dengan kombinasi doublenilai untuk koordinat tersebut, spesifikasi semacam itu akan memaksa untuk berperilaku semantik identik dengan struct bidang-terbuka kecuali untuk beberapa detail perilaku multi-threaded (kelas abadi akan lebih baik dalam beberapa kasus, sementara struct bidang-terbuka akan lebih baik dalam kasus lain; yang disebut "immutable" struct akan lebih buruk dalam setiap kasus).
supercat
8

Aturan saya adalah

1, Selalu gunakan kelas;

2, Jika ada masalah kinerja, saya mencoba untuk mengubah beberapa kelas ke struct tergantung pada aturan yang disebutkan @ Ekstrak, dan kemudian melakukan tes untuk melihat apakah perubahan ini dapat meningkatkan kinerja.

rockXrock
sumber
Kasus penggunaan substansial yang diabaikan oleh Microsoft adalah ketika seseorang menginginkan variabel tipe Foountuk merangkum koleksi tetap dari nilai-nilai independen (misalnya koordinat suatu titik) yang terkadang ingin diedarkan sebagai grup dan terkadang ingin diubah secara independen. Saya tidak menemukan pola apa pun untuk menggunakan kelas yang menggabungkan kedua tujuan hampir sama baiknya dengan struct bidang terbuka sederhana (yang, sebagai koleksi tetap variabel independen, sangat cocok dengan tagihan).
supercat
1
@ supercat: Saya pikir itu tidak sepenuhnya adil untuk menyalahkan Microsoft untuk itu. Masalah sebenarnya di sini adalah bahwa C # sebagai bahasa berorientasi objek hanya tidak fokus pada tipe rekaman biasa yang hanya mengekspos data tanpa banyak perilaku. C # bukan bahasa multi-paradigma pada tingkat yang sama seperti misalnya C ++. Yang sedang berkata, saya juga percaya sangat sedikit orang memprogram OOP murni, jadi mungkin C # adalah bahasa yang terlalu idealis. (Saya sendiri baru-baru ini mulai mengekspos public readonlybidang dalam tipe saya juga, karena membuat properti hanya-baca terlalu banyak bekerja untuk praktis tidak ada manfaatnya.)
stakx - tidak lagi berkontribusi
1
@stakx: Tidak perlu untuk "fokus" pada tipe seperti itu; mengenali mereka apa adanya mereka sudah cukup. Kelemahan terbesar dengan C # berkaitan dengan struct adalah masalah terbesar di banyak bidang lain juga: bahasa menyediakan fasilitas yang tidak memadai untuk menunjukkan kapan transformasi tertentu sesuai atau tidak sesuai, dan kurangnya fasilitas tersebut mendorong keputusan desain yang kurang menguntungkan. Sebagai contoh, 99% dari "struct yang bisa berubah-ubah adalah jahat" berasal dari pergantian kompiler MyListOfPoint[3].Offset(2,3);menjadi var temp=MyListOfPoint[3]; temp.Offset(2,3);, sebuah transformasi yang palsu ketika diterapkan ...
supercat
... ke Offsetmetode. Cara yang tepat untuk mencegah kode palsu seperti itu tidak boleh membuat struct tidak dapat diubah selamanya, tetapi sebagai gantinya membiarkan metode suka Offsetditandai dengan atribut yang melarang transformasi yang disebutkan di atas. Konversi numerik implisit juga bisa jauh lebih baik jika mereka dapat ditandai sehingga hanya berlaku dalam kasus-kasus di mana doa mereka akan jelas. Jika ada kelebihan untuk foo(float,float)dan foo(double,double), saya berpendapat bahwa mencoba menggunakan floatdan doublesering tidak boleh menerapkan konversi implisit, tetapi seharusnya menjadi kesalahan.
supercat
Penugasan langsung doublenilai ke float, atau meneruskannya ke metode yang dapat mengambil floatargumen tetapi tidak double, hampir selalu melakukan apa yang diinginkan oleh pemrogram. Sebaliknya, menetapkan floatekspresi doubletanpa typecast eksplisit seringkali merupakan kesalahan. Satu-satunya waktu yang memungkinkan double->floatkonversi implisit akan menyebabkan masalah adalah ketika itu akan menyebabkan kelebihan yang kurang ideal untuk dipilih. Saya berpendapat bahwa cara yang tepat untuk mencegah hal itu seharusnya tidak melarang float implisit ganda>, tetapi menandai kelebihan beban dengan atribut untuk melarang konversi.
supercat
8

Kelas adalah tipe referensi. Ketika objek kelas dibuat, variabel yang objek ditugaskan hanya memiliki referensi ke memori itu. Ketika referensi objek ditugaskan ke variabel baru, variabel baru merujuk ke objek asli. Perubahan yang dilakukan melalui satu variabel tercermin dalam variabel lainnya karena keduanya merujuk pada data yang sama. Str adalah tipe nilai. Ketika sebuah struct dibuat, variabel yang diberikan struct menyimpan data aktual struct. Ketika struct ditugaskan ke variabel baru, itu akan disalin. Variabel baru dan variabel asli karenanya mengandung dua salinan terpisah dari data yang sama. Perubahan yang dilakukan pada satu salinan tidak memengaruhi salinan lainnya. Secara umum, kelas digunakan untuk memodelkan perilaku yang lebih kompleks, atau data yang dimaksudkan untuk dimodifikasi setelah objek kelas dibuat.

Kelas dan Struktur (Panduan Pemrograman C #)

J_hajian_nzd
sumber
Struktur juga sangat baik dalam kasus-kasus di mana perlu untuk mengikat beberapa variabel terkait-tetapi-independen bersama-sama dengan lakban (misalnya koordinat titik). Pedoman MSDN masuk akal jika seseorang mencoba untuk menghasilkan struktur yang berperilaku seperti objek, tetapi jauh kurang tepat ketika merancang agregat; beberapa dari mereka hampir persis salah dalam situasi terakhir. Sebagai contoh, semakin besar tingkat kemandirian variabel yang dienkapsulasi oleh suatu tipe, semakin besar keuntungan menggunakan struktur bidang terbuka daripada kelas yang tidak dapat diubah.
supercat
6

MITOS # 1: STRUCTS ADALAH KELAS LIGHTWEIGHT

Mitos ini muncul dalam berbagai bentuk. Beberapa orang percaya bahwa tipe nilai tidak dapat atau tidak seharusnya memiliki metode atau perilaku signifikan lainnya — mereka harus digunakan sebagai tipe transfer data sederhana, hanya dengan bidang publik atau properti sederhana. Tipe DateTime adalah contoh tandingan yang baik untuk ini: masuk akal jika itu menjadi tipe nilai, dalam hal menjadi unit mendasar seperti angka atau karakter, dan juga masuk akal untuk dapat melakukan perhitungan berdasarkan nilainya. Melihat hal-hal dari arah lain, tipe transfer data harus sering menjadi tipe referensi — keputusan harus didasarkan pada nilai yang diinginkan atau semantik tipe referensi, bukan kesederhanaan tipe. Orang lain percaya bahwa tipe nilai "lebih ringan" daripada tipe referensi dalam hal kinerja. Yang benar adalah bahwa dalam beberapa kasus nilai jenis lebih berkinerja - mereka tidak memerlukan pengumpulan sampah kecuali mereka kotak, tidak memiliki jenis identifikasi overhead, dan tidak memerlukan dereferencing, misalnya. Tetapi dengan cara lain, tipe referensi lebih berkinerja baik - melewati parameter, menetapkan nilai ke variabel, mengembalikan nilai, dan operasi yang serupa hanya memerlukan 4 atau 8 byte untuk dioptasi (tergantung pada apakah Anda menjalankan CLR 32-bit atau 64-bit) ) daripada menyalin semua data. Bayangkan jika ArrayList entah bagaimana merupakan tipe nilai "murni", dan meneruskan ekspresi ArrayList ke metode yang terlibat menyalin semua datanya! Dalam hampir semua kasus, kinerja sebenarnya tidak ditentukan oleh keputusan semacam ini. Kemacetan hampir tidak pernah Anda pikirkan, dan sebelum Anda membuat keputusan desain berdasarkan kinerja, Anda harus mengukur opsi yang berbeda. Perlu dicatat bahwa kombinasi dari kedua kepercayaan itu juga tidak berhasil. Tidak masalah berapa banyak metode yang dimiliki suatu tipe (apakah itu kelas atau struct) - memori yang diambil per instance tidak terpengaruh. (Ada biaya dalam hal memori yang digunakan untuk kode itu sendiri, tetapi itu terjadi sekali daripada untuk setiap contoh.)

MITOS # 2: JENIS-JENIS REFERENSI HIDUP DI HEAP; JENIS-JENIS NILAI LANGSUNG DI STACK

Yang ini sering disebabkan oleh kemalasan pada bagian orang yang mengulanginya. Bagian pertama sudah benar — sebuah instance dari tipe referensi selalu dibuat di heap. Itu bagian kedua yang menyebabkan masalah. Seperti yang sudah saya catat, nilai variabel tinggal di mana pun ia dideklarasikan, jadi jika Anda memiliki kelas dengan variabel instan dari tipe int, nilai variabel untuk objek tertentu akan selalu berada di tempat sisa data untuk objek tersebut adalah— di atas tumpukan. Hanya variabel lokal (variabel yang dideklarasikan dalam metode) dan parameter metode yang hidup di stack. Dalam C # 2 dan yang lebih baru, bahkan beberapa variabel lokal tidak benar-benar hidup di stack, seperti yang akan Anda lihat ketika kita melihat metode anonim di bab 5. APAKAH KONSEP INI RELEVAN SEKARANG? Dapat diperdebatkan bahwa jika Anda menulis kode terkelola, Anda harus membiarkan runtime khawatir tentang bagaimana memori digunakan. Memang, spesifikasi bahasa tidak memberikan jaminan tentang apa yang tinggal di mana; runtime di masa depan mungkin dapat membuat beberapa objek pada stack jika ia tahu ia dapat lolos, atau kompiler C # dapat menghasilkan kode yang hampir tidak menggunakan stack sama sekali. Mitos selanjutnya biasanya hanya masalah terminologi.

MITOS # 3: OBYEK BERLALU OLEH REFERENSI DALAM C # OLEH DEFAULT

Ini mungkin mitos yang paling banyak diperbanyak. Sekali lagi, orang-orang yang membuat klaim ini sering (walaupun tidak selalu) tahu bagaimana sebenarnya C # berperilaku, tetapi mereka tidak tahu apa arti sebenarnya "lewat referensi". Sayangnya, ini membingungkan bagi orang yang tahu apa artinya. Definisi formal pass by reference relatif rumit, melibatkan nilai-l dan terminologi ilmu komputer yang serupa, tetapi yang penting adalah bahwa jika Anda melewatkan variabel dengan referensi, metode yang Anda panggil dapat mengubah nilai variabel pemanggil dengan mengubah nilai parameternya. Sekarang, ingat bahwa nilai variabel tipe referensi adalah referensi, bukan objek itu sendiri. Anda dapat mengubah konten objek yang dirujuk oleh parameter tanpa parameter itu sendiri dilewatkan oleh referensi. Contohnya,

void AppendHello(StringBuilder builder)
{
    builder.Append("hello");
}

Ketika metode ini dipanggil, nilai parameter (referensi ke StringBuilder) diteruskan oleh nilai. Jika Anda mengubah nilai variabel builder dalam metode — misalnya, dengan statement builder = null; —bahwa perubahan tidak akan terlihat oleh penelepon, bertentangan dengan mitos. Sangat menarik untuk dicatat bahwa tidak hanya bit "dengan referensi" mitos yang tidak akurat, tetapi juga bit "objek yang dilewati". Objek itu sendiri tidak pernah dilewatkan, baik dengan referensi atau dengan nilai. Ketika jenis referensi terlibat, variabel dilewatkan oleh referensi atau nilai argumen (referensi) dilewatkan oleh nilai. Selain dari hal lain, ini menjawab pertanyaan tentang apa yang terjadi ketika null digunakan sebagai argumen nilai-jika-objek dilewatkan, itu akan menyebabkan masalah, karena tidak akan ada objek untuk dilewati! Sebagai gantinya, referensi nol dilewatkan oleh nilai dengan cara yang sama seperti referensi lainnya. Jika penjelasan singkat ini membuat Anda bingung, Anda mungkin ingin melihat artikel saya, "Parameter meneruskan C #," (http://mng.bz/otVt ), yang lebih detail. Mitos-mitos ini bukan satu-satunya di sekitar. Boxing dan unboxing masuk untuk bagian kesalahpahaman mereka, yang akan saya coba jelaskan selanjutnya.

Referensi: C # in Depth 3rd Edition oleh Jon Skeet

habib
sumber
1
Sangat bagus dengan asumsi Anda benar. Juga sangat bagus untuk menambahkan referensi.
NoChance
5

Saya pikir pendekatan pertama yang baik adalah "tidak pernah".

Saya pikir perkiraan kedua yang baik adalah "tidak pernah".

Jika Anda putus asa untuk perf, pertimbangkan mereka, tetapi kemudian selalu mengukur.

Brian
sumber
24
Saya tidak setuju dengan jawaban itu. Struct memiliki penggunaan yang sah dalam banyak skenario. Berikut ini sebuah contoh - mengatur proses lintas data dengan cara atom.
Franci Penov
25
Anda harus mengedit posting Anda dan menguraikan poin Anda - Anda telah memberikan pendapat Anda, tetapi Anda harus mendukungnya dengan alasan Anda menerima pendapat ini.
Erik Forbes
4
Saya pikir mereka membutuhkan setara dengan kartu Chip Totin '( en.wikipedia.org/wiki/Totin%27_Chip ) untuk menggunakan struct. Serius.
Greg
4
Bagaimana orang 87.5K mengirim jawaban seperti ini? Apakah dia melakukannya saat dia masih kecil?
Rohit Vipin Mathews
3
@Rohit - enam tahun yang lalu; standar situs sangat berbeda saat itu. ini masih merupakan jawaban yang buruk, kamu benar.
Andrew Arnold
5

Saya hanya berurusan dengan Windows Communication Foundation [WCF] Named Pipe dan saya perhatikan bahwa memang masuk akal untuk menggunakan Structs untuk memastikan bahwa pertukaran data adalah tipe nilai dan bukan tipe referensi .

N_E
sumber
1
Ini adalah petunjuk terbaik, IMHO.
Ivan
4

C # struct adalah alternatif ringan untuk sebuah kelas. Ini bisa melakukan hampir sama dengan kelas, tetapi lebih murah untuk menggunakan struct daripada kelas. Alasan untuk ini sedikit teknis, tetapi untuk menyimpulkan, contoh baru dari kelas ditempatkan di heap, di mana struct yang baru dipakai ditempatkan pada stack. Selain itu, Anda tidak berurusan dengan referensi untuk struct, seperti dengan kelas, tetapi Anda bekerja secara langsung dengan instance struct. Ini juga berarti bahwa ketika Anda meneruskan struct ke suatu fungsi, itu dengan nilai, bukan sebagai referensi. Ada lebih banyak tentang ini di bab tentang parameter fungsi.

Jadi, Anda harus menggunakan struct ketika Anda ingin mewakili struktur data yang lebih sederhana, dan terutama jika Anda tahu bahwa Anda akan membuat banyak dari mereka. Ada banyak contoh dalam kerangka .NET, di mana Microsoft telah menggunakan struct bukan kelas, misalnya Point, Rectangle dan Color struct.

Saeed Dini
sumber
3

Struct dapat digunakan untuk meningkatkan kinerja pengumpulan sampah. Meskipun Anda biasanya tidak perlu khawatir tentang kinerja GC, ada skenario di mana itu bisa menjadi pembunuh. Seperti cache besar dalam aplikasi latensi rendah. Lihat posting ini sebagai contoh:

http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/

kelinci
sumber
3

Jenis struktur atau nilai dapat digunakan dalam skenario berikut -

  1. Jika Anda ingin mencegah objek dikumpulkan oleh pengumpulan sampah.
  2. Jika ini adalah tipe sederhana dan tidak ada fungsi anggota memodifikasi bidang instansnya
  3. Jika tidak perlu berasal dari jenis lain atau diturunkan ke jenis lain.

Anda bisa tahu lebih banyak tentang tipe nilai dan tipe nilai di sini di tautan ini

Vikram
sumber
3

Secara singkat, gunakan struct jika:

1- properti / bidang objek Anda tidak perlu diubah. Maksud saya Anda hanya ingin memberi mereka nilai awal dan kemudian membacanya.

2- properti dan bidang dalam objek Anda adalah tipe nilai dan tidak terlalu besar.

Jika demikian, Anda dapat memanfaatkan struct untuk kinerja yang lebih baik dan alokasi memori yang dioptimalkan karena mereka hanya menggunakan tumpukan daripada tumpukan dan tumpukan (di kelas)

akazemis
sumber
2

Saya jarang menggunakan struct untuk sesuatu. Tapi itu hanya aku. Itu tergantung apakah saya perlu objek menjadi nullable atau tidak.

Seperti yang dinyatakan dalam jawaban lain, saya menggunakan kelas untuk objek dunia nyata. Saya juga memiliki pola pikir struct yang digunakan untuk menyimpan sejumlah kecil data.

Flexo
sumber
-11

Struktur dalam banyak hal seperti kelas / objek. Struktur dapat berisi fungsi, anggota dan dapat diwarisi. Tetapi struktur dalam C # digunakan hanya untuk memegang data . Struktur memang membutuhkan RAM lebih sedikit daripada kelas dan lebih mudah untuk mengumpulkan sampah . Tetapi ketika Anda menggunakan fungsi dalam struktur Anda, maka kompiler benar-benar mengambil struktur yang sangat mirip dengan kelas / objek, jadi jika Anda menginginkan sesuatu dengan fungsi, maka gunakan kelas / objek .

Cryo Erger
sumber
2
Struktur TIDAK dapat diwarisi, lihat msdn.microsoft.com/en-us/library/0taef578.aspx
HimBromBeere