Pertanyaan Kapan menggunakan struct?


Kapan sebaiknya Anda menggunakan struct dan bukan kelas dalam C #? Model konseptual saya adalah bahwa struct digunakan pada saat item itu hanya koleksi tipe nilai. Cara untuk secara logis menahan mereka semua menjadi satu kesatuan yang kohesif.

Saya menemukan aturan-aturan ini sini:

  • Sebuah struct harus mewakili satu nilai.
  • Sebuah struct harus memiliki memori tapak kurang dari 16 byte.
  • Sebuah struct tidak boleh diubah setelahnya penciptaan.

Apakah aturan-aturan ini berfungsi? Apa artinya struct secara semantik?


1201
2018-02-06 17:37


asal


Jawaban:


Sumber yang direferensikan oleh OP memiliki kredibilitas ... tapi bagaimana dengan Microsoft - apa posisi penggunaan struct? Saya mencari beberapa tambahan belajar dari Microsoft, dan inilah yang saya temukan:

Pertimbangkan mendefinisikan struktur daripada kelas jika instance dari   jenisnya kecil dan umumnya berumur pendek atau umumnya tertanam di dalamnya   benda lain.

Jangan mendefinisikan struktur kecuali tipe memiliki semua karakteristik berikut: 

  1. Ini secara logis mewakili nilai tunggal, mirip dengan tipe primitif (integer, double, dan seterusnya).
  2. Ini memiliki ukuran instance yang lebih kecil dari 16 byte.
  3. Itu tidak dapat diubah.
  4. Ini tidak harus sering dikemas.

Microsoft secara konsisten melanggar aturan-aturan itu

Oke, # 2 dan # 3. Kamus kami yang tercinta 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 sejak # 4 mungkin tidak akan menjadi masalah. Jika Anda menemukan diri Anda tinju sebuah struct, pikirkan kembali arsitektur Anda.

Mari kita lihat mengapa Microsoft akan menggunakan struct ini:

  1. Setiap struct, Entry dan Enumerator, mewakili nilai-nilai tunggal.
  2. Kecepatan
  3. Entry tidak pernah dilewatkan sebagai parameter di luar kelas Kamus. Investigasi lebih lanjut menunjukkan bahwa untuk memenuhi implementasi IEnumerable, Kamus menggunakan Enumerator struct yang menyalin setiap kali pencacah diminta ... masuk akal.
  4. Internal ke kelas Kamus. Enumerator bersifat publik karena Kamus adalah enumerable dan harus memiliki aksesibilitas yang sama ke implementasi antarmuka IEnumerator - mis. IEnumerator getter.

Memperbarui - Selain itu, sadari bahwa ketika struct mengimplementasikan antarmuka - seperti yang dilakukan Enumerator - dan dilemparkan ke jenis yang diimplementasikan, struct menjadi tipe referensi dan dipindahkan ke heap. Internal ke kelas Kamus, Enumerator aku s masih tipe nilai. Namun, begitu panggilan metode GetEnumerator(), tipe referensi IEnumerator dikembalikan.

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

  1. Tidak ada dalam struct di atas yang dideklarasikan readonly - tidak kekal
  2. Ukuran struct ini bisa lebih dari 16 byte
  3. Entry memiliki seumur hidup yang 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)

Namun, disembunyikan kunci, kamus cepat sebagian karena instancing struct lebih cepat daripada tipe referensi. Di sini, saya punya Dictionary<int, int> yang menyimpan 300.000 bilangan bulat acak dengan kunci yang berurutan.

Kapasitas: 312874
  MemSize: 2660827 byte
  Selesai Resize: 5ms
  Total waktu pengisian: 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).

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

Total waktu untuk diisi: diakui miring karena penebangan dan OnResize acara yang saya tambahkan ke sumbernya; Namun, masih mengesankan untuk mengisi bilangan bulat 300k sementara mengubah ukuran 15 kali selama operasi. Hanya ingin tahu, apa yang akan total waktu untuk mengisi jika saya sudah tahu kapasitas? 13ms 

Jadi, sekarang, bagaimana jika Entry apakah itu kelas? Akankah waktu atau metrik ini benar-benar berbeda?

Kapasitas: 312874
  MemSize: 2660827 byte
  Selesai Resize: 26ms
  Total waktu pengisian: 964ms

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

Apa yang terjadi adalah, karena Entry adalah struct, tidak memerlukan inisialisasi seperti tipe referensi. Ini adalah keindahan dan kutukan dari jenis nilai. Untuk digunakan Entry sebagai 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 Entry sebagai tipe referensi dapat ditemukan di MSDN: Desain Struktur. Pendeknya:

Jangan memberikan konstruktor default untuk struktur.

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

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

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

  1. Struct harus aman digunakan
  2. Struct harus menjalankan fungsinya secara efisien, kecuali 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 jenis nilai. Mereka cepat dan efisien, tetapi memiliki kemampuan untuk menyebabkan banyak perilaku tak terduga jika tidak dipelihara dengan baik (yaitu salinan yang tidak disengaja).


541
2017-08-07 13:44



Setiap kali Anda tidak perlu polimorfisme, ingin semantik nilai, dan ingin menghindari alokasi tumpukan dan pengumpulan sampah terkait di atas. Peringatannya, bagaimanapun, adalah bahwa struct (sewenang-wenang besar) lebih mahal untuk dilewatkan daripada referensi kelas (biasanya satu kata mesin), sehingga kelas bisa berakhir menjadi lebih cepat dalam prakteknya.


142
2018-02-06 17:40



Saya tidak setuju dengan aturan yang diberikan di pos asli. Inilah peraturan 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 gunakan struct kecuali Anda membutuhkannya:

  • Mereka berperilaku berbeda dari "objek normal" (tipe referensi) di bawah penugasan dan ketika melewati sebagai argumen, yang dapat menyebabkan perilaku tak terduga; ini sangat berbahaya jika orang yang melihat kode tidak tidak tahu mereka berurusan dengan sebuah struct.
  • Mereka tidak bisa diwariskan.
  • Melewati struct sebagai argumen lebih mahal daripada kelas.

133
2018-02-28 16:33



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

Edit

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

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


82
2018-02-06 17:40



Selain jawaban "itu adalah nilai", satu skenario khusus untuk menggunakan struct adalah ketika Anda tahu bahwa Anda memiliki satu set data yang menyebabkan masalah pengumpulan sampah, dan Anda memiliki banyak objek. Misalnya, daftar / larik besar instance Orang. Metafora alami di sini adalah kelas, tetapi jika Anda memiliki sejumlah besar contoh Personel berumur panjang, mereka dapat menyumbat GEN-2 dan menyebabkan warung GC. Jika skenario menjaminnya, salah satu pendekatan potensial di sini adalah menggunakan array (bukan daftar) Person structs, i.e. Person[]. Sekarang, alih-alih memiliki jutaan objek dalam GEN-2, Anda memiliki satu bongkahan di LOH (saya mengasumsikan tidak ada dawai 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-nilai lemak sepanjang waktu. Namun, mengaksesnya langsung dalam array tidak menyalin struct - itu di tempat (berbeda 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 di tempat - kami belum menyalin nilai.

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


54
2017-10-22 12:14



Dari Spesifikasi Bahasa C #:

1,7 Structs 

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

Struct sangat berguna untuk struktur data kecil yang memiliki   semantik nilai. Bilangan kompleks, titik dalam sistem koordinat, atau   pasangan kunci-nilai dalam kamus adalah semua contoh struct yang bagus. Itu   penggunaan struct daripada kelas untuk struktur data kecil dapat dibuat   perbedaan besar dalam jumlah alokasi memori aplikasi   melakukan. Misalnya, program berikut membuat dan menginisialisasi   sebuah array dari 100 poin. Dengan Point diimplementasikan sebagai kelas, 101   benda-benda terpisah yang 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);
   }
}

Alternatifnya adalah dengan membuat Point menjadi 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 — yang untuk array — dan instance Point disimpan secara in-line dalam array.

Konstruktor struct dipanggil dengan operator baru, tetapi itu tidak berarti bahwa memori sedang dialokasikan. Alih-alih mengalokasikan objek secara dinamis dan mengembalikan referensi ke itu, 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 direferensikan oleh variabel lain. Dengan struct, masing-masing variabel memiliki salinan data mereka sendiri, dan tidak mungkin operasi pada satu untuk mempengaruhi yang lain. Sebagai contoh, output yang dihasilkan oleh fragmen kode berikut 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, hasilnya adalah 20 karena a dan b mereferensikan objek yang sama. Jika Point adalah struct, hasilnya adalah 10 karena penugasan a ke b menciptakan salinan dari nilai, dan salinan ini tidak terpengaruh oleh penugasan berikutnya ke a.x.

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


36
2017-09-17 15:42



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


31
2018-02-06 17:58



Ini adalah aturan dasar.

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

  • Jika salah satu bidang anggota adalah tipe referensi, buat a kelas. Ini karena field tipe referensi akan membutuhkan 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
}

24
2018-01-22 10:17