Kapan saya harus menggunakan struct bukan kelas?

302

MSDN mengatakan bahwa Anda harus menggunakan struct ketika Anda membutuhkan benda ringan. Apakah ada skenario lain ketika struct lebih disukai daripada kelas?

Beberapa orang mungkin lupa bahwa:

  1. struct dapat memiliki metode.
  2. struct tidak dapat diwarisi.

Saya memahami perbedaan teknis antara struct dan kelas, saya hanya tidak memiliki perasaan yang baik tentang kapan harus menggunakan struct.

Esteban Araya
sumber
Hanya pengingat - apa yang cenderung dilupakan kebanyakan orang dalam konteks ini adalah bahwa dalam C # struct dapat memiliki metode juga.
petr k.

Jawaban:

295

MSDN memiliki jawabannya: Memilih Antara Kelas dan Struktur .

Pada dasarnya, halaman itu memberi Anda daftar periksa 4-item dan mengatakan untuk menggunakan kelas kecuali tipe Anda memenuhi semua kriteria.

Jangan mendefinisikan struktur kecuali tipe memiliki semua karakteristik berikut:

  • Secara logis mewakili nilai tunggal, mirip dengan tipe primitif (integer, dobel, dan sebagainya).
  • Ini memiliki ukuran instance lebih kecil dari 16 byte.
  • Itu tidak berubah.
  • Itu tidak harus sering kotak.
OwenP
sumber
1
Mungkin saya kehilangan sesuatu yang jelas tetapi saya tidak mendapatkan alasan di balik bagian "abadi". Mengapa ini perlu? Bisakah seseorang menjelaskannya?
Tamas Czinege
3
Mereka mungkin merekomendasikan ini karena jika struct tidak dapat diubah, maka tidak masalah bahwa itu memiliki nilai semantik daripada referensi semantik. Perbedaannya hanya penting jika Anda bermutasi objek / struct setelah membuat salinan.
Stephen C. Steel
@DrJokepu: Dalam beberapa situasi, sistem akan membuat salinan sementara dari sebuah struct dan kemudian mengizinkan salinan itu untuk dilewatkan dengan referensi ke kode yang mengubahnya; karena salinan sementara akan dibuang, perubahannya akan hilang. Masalah ini sangat parah jika struct memiliki metode yang mengubahnya. Saya dengan tegas tidak setuju dengan gagasan bahwa mutabilitas adalah alasan untuk membuat sesuatu menjadi kelas, karena - terlepas dari beberapa kekurangan dalam c # dan vb.net, struct yang bisa berubah memberikan semantik yang berguna yang tidak dapat dicapai dengan cara lain; tidak ada alasan semantik untuk lebih memilih struct yang tidak dapat diubah daripada sebuah kelas.
supercat
4
@Huhu: Dalam mendesain kompiler JIT, Microsoft memutuskan untuk mengoptimalkan kode untuk menyalin struct yang 16 byte atau lebih kecil; ini berarti bahwa menyalin struct 17-byte mungkin secara signifikan lebih lambat daripada menyalin struct 16-byte. Saya tidak melihat alasan khusus untuk mengharapkan Microsoft memperluas optimisasi tersebut ke struct yang lebih besar, tetapi penting untuk dicatat bahwa sementara 17-byte structurs mungkin lebih lambat untuk menyalin daripada struct 16-byte, ada banyak kasus di mana struct besar mungkin lebih efisien daripada objek kelas besar, dan di mana keuntungan relatif dari struct tumbuh dengan ukuran struct.
supercat
3
@ Chuu: Menerapkan pola penggunaan yang sama ke struktur besar seperti yang akan dilakukan kelas cenderung menghasilkan kode yang tidak efisien, tetapi solusi yang tepat sering tidak mengganti struct dengan kelas, tetapi sebaliknya menggunakan struct lebih efisien; terutama, seseorang harus menghindari melewati atau mengembalikan struct dengan nilai. Berikan mereka sebagai refparameter setiap kali masuk akal untuk melakukannya. Mengirimkan struct dengan 4.000 bidang sebagai parameter ref ke metode yang mengubah satu akan lebih murah daripada melewati struct dengan 4 bidang dengan nilai ke metode yang mengembalikan versi yang dimodifikasi.
supercat
53

Saya terkejut saya belum membaca jawaban sebelumnya yang mana, yang saya anggap sebagai aspek paling penting:

Saya menggunakan struct ketika saya ingin jenis tanpa identitas. Misalnya titik 3D:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Jika Anda memiliki dua contoh dari struct ini Anda tidak peduli apakah mereka adalah satu bagian data dalam memori atau dua. Anda hanya peduli dengan nilai yang mereka miliki.

Andrei Rînea
sumber
4
Mengagetkan topik, tetapi mengapa Anda melempar ArgumentException ketika obj bukan ThreeDimensionalPoint? Bukankah Anda seharusnya mengembalikan false dalam kasus itu?
Svish
4
Itu benar, saya terlalu bersemangat. return falseadalah apa yang seharusnya ada di sana, mengoreksi sekarang.
Andrei Rînea
Alasan yang menarik untuk menggunakan struct. Saya telah membuat kelas dengan GetHashCode dan Equals didefinisikan mirip dengan apa yang Anda perlihatkan di sini, tapi kemudian saya selalu harus berhati-hati untuk tidak mengubah contoh-contoh itu jika saya menggunakannya sebagai kunci kamus. Mungkin akan lebih aman jika saya mendefinisikannya sebagai struct. (Karena dengan demikian kuncinya akan menjadi salinan bidang pada saat struct menjadi kunci kamus , jadi kuncinya akan tetap tidak berubah jika saya kemudian mengubah yang asli.)
ToolmakerSteve
Dalam contoh Anda tidak apa-apa karena Anda hanya memiliki 12 Bytes tetapi ingatlah bahwa jika Anda memiliki banyak bidang dalam struct yang melebihi 16 Bytes, Anda harus mempertimbangkan untuk menggunakan kelas dan mengganti metode GetHashCode dan Equals.
Daniel Botero Correa
Jenis nilai dalam DDD tidak berarti Anda harus menggunakan tipe nilai dalam C #
Darragh
26

Bill Wagner memiliki bab tentang ini dalam bukunya "efektif c #" ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Dia menyimpulkan dengan menggunakan prinsip berikut:

  1. Apakah responsabilitas utama tipe penyimpanan data?
  2. Apakah antarmuka publiknya sepenuhnya ditentukan oleh properti yang mengakses atau memodifikasi anggota datanya?
  3. Apakah Anda yakin tipe Anda tidak akan pernah memiliki subclass?
  4. Apakah Anda yakin jenis Anda tidak akan pernah dirawat secara polimorfik?

Jika Anda menjawab 'ya' untuk semua 4 pertanyaan: gunakan struct. Kalau tidak, gunakan kelas.

Bart Gijssens
sumber
1
jadi ... objek transfer data (DTO) harus berupa struct?
Cruizer
1
Saya akan mengatakan ya, jika memenuhi 4 kriteria yang dijelaskan di atas. Mengapa objek transfer data perlu diperlakukan dengan cara tertentu?
Bart Gijssens
2
@cruizer tergantung pada situasi Anda. Pada satu proyek, kami memiliki bidang audit umum di DTO kami dan karenanya menulis DTO dasar yang diwarisi orang lain.
Andrew Grothe
1
Semua kecuali (2) tampak seperti prinsip yang sangat baik. Perlu melihat alasannya untuk mengetahui apa sebenarnya yang dia maksud dengan (2), dan mengapa.
ToolmakerSteve
1
@ToolmakerSteve: Anda harus membaca buku untuk itu. Jangan pikir itu adil untuk menyalin / menempelkan sebagian besar buku.
Bart Gijssens
15

Gunakan struct ketika Anda ingin semantik tipe nilai bukan tipe referensi. Struct adalah copy-by-value jadi hati-hati!

Lihat juga pertanyaan sebelumnya, mis

Apa perbedaan antara struct dan kelas di .NET?

Simon Steele
sumber
12

Saya akan menggunakan struct ketika:

  1. suatu objek seharusnya hanya dibaca (setiap kali Anda lulus / menetapkan struct itu akan disalin). Hanya membaca objek yang bagus ketika datang ke pemrosesan multithreaded karena mereka tidak cukup mengunci dalam banyak kasus.

  2. sebuah benda kecil dan berumur pendek. Dalam kasus seperti itu ada kemungkinan besar bahwa objek akan dialokasikan pada tumpukan yang jauh lebih efisien daripada meletakkannya di tumpukan yang dikelola. Terlebih lagi memori yang dialokasikan oleh objek akan dibebaskan segera setelah melampaui ruang lingkupnya. Dengan kata lain itu kurang berfungsi untuk Pengumpul Sampah dan memori yang digunakan lebih efisien.

Pawel Pabich
sumber
10

Gunakan kelas jika:

  • Identitasnya penting. Struktur disalin secara implisit ketika diteruskan oleh nilai ke dalam metode.
  • Ini akan memiliki jejak memori yang besar.
  • Bidangnya membutuhkan inisialisasi.
  • Anda harus mewarisi dari kelas dasar.
  • Anda membutuhkan perilaku polimorfik;

Gunakan struktur jika:

  • Ini akan bertindak seperti tipe primitif (int, panjang, byte, dll.).
  • Itu harus memiliki jejak memori kecil.
  • Anda memanggil metode P / Invoke yang membutuhkan struktur untuk diteruskan oleh nilai.
  • Anda perlu mengurangi dampak pengumpulan sampah pada kinerja aplikasi.
  • Bidangnya harus diinisialisasi hanya ke nilai defaultnya. Nilai ini akan menjadi nol untuk tipe numerik, false untuk tipe Boolean, dan null untuk tipe referensi.
    • Perhatikan bahwa dalam C # 6.0 struct dapat memiliki konstruktor default yang dapat digunakan untuk menginisialisasi bidang struct ke nilai non-default.
  • Anda tidak perlu mewarisi dari kelas dasar (selain ValueType, dari mana semua struct mewarisi).
  • Anda tidak perlu perilaku polimorfik.
Yashwanth Chowdary Kata
sumber
5

Saya selalu menggunakan struct ketika saya ingin mengelompokkan beberapa nilai untuk melewati sesuatu dari pemanggilan metode, tapi saya tidak perlu menggunakannya untuk apa pun setelah saya membaca nilai-nilai itu. Sama seperti cara untuk menjaga kebersihan. Saya cenderung melihat hal-hal dalam struct sebagai "membuang" dan hal-hal di kelas sebagai lebih berguna dan "fungsional"

Ryan Skarin
sumber
4

Jika suatu entitas akan berubah, pertanyaan apakah akan menggunakan struct atau kelas umumnya akan menjadi salah satu kinerja daripada semantik. Pada sistem 32/64-bit, referensi kelas membutuhkan 4/8 byte untuk disimpan, terlepas dari jumlah informasi di kelas; menyalin referensi kelas akan membutuhkan menyalin 4/8 byte. Di sisi lain, setiap berbedainstance kelas akan memiliki 8/16 byte overhead di samping informasi yang dimilikinya dan biaya memori referensi untuk itu. Misalkan seseorang menginginkan array 500 entitas, masing-masing memegang empat bilangan bulat 32-bit. Jika entitas adalah tipe struktur, array akan membutuhkan 8.000 byte terlepas dari apakah semua 500 entitas semuanya identik, semua berbeda, atau di antara keduanya. Jika entitas adalah tipe kelas, array 500 referensi akan mengambil 4.000 byte. Jika referensi semua menunjuk ke objek yang berbeda, objek akan memerlukan tambahan 24 byte masing-masing (12.000 byte untuk semua 500), total 16.000 byte - dua kali biaya penyimpanan dari jenis struct. Di sisi lain, dari kode yang dibuat satu objek instance dan kemudian disalin referensi ke semua 500 array slot, total biaya akan menjadi 24 byte untuk instance itu dan 4, 000 untuk array - total 4.024 byte. Penghematan besar. Beberapa situasi akan berhasil serta yang terakhir, tetapi dalam beberapa kasus mungkin untuk menyalin beberapa referensi ke slot array yang cukup untuk membuat berbagi seperti itu bermanfaat.

Jika entitas seharusnya bisa berubah, pertanyaan apakah menggunakan kelas atau struct dalam beberapa hal lebih mudah. Asumsikan "Benda" adalah suatu struct atau kelas yang memiliki bidang bilangan bulat yang disebut x, dan seseorang melakukan kode berikut:

  Hal t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Apakah orang ingin pernyataan yang terakhir mempengaruhi t1.x?

Jika Benda adalah tipe kelas, t1 dan t2 akan sama, artinya t1.x dan t2.x juga akan setara. Dengan demikian, pernyataan kedua akan mempengaruhi t1.x. Jika Benda adalah tipe struktur, t1 dan t2 akan menjadi instance yang berbeda, artinya t1.x dan t2.x akan merujuk ke bilangan bulat yang berbeda. Dengan demikian, pernyataan kedua tidak akan mempengaruhi t1.x.

Struktur yang dapat berubah dan kelas yang dapat berubah memiliki perilaku yang berbeda secara mendasar, meskipun .net memiliki beberapa kebiasaan dalam penanganan mutasi struktural. Jika seseorang menginginkan perilaku tipe-nilai (artinya "t2 = t1" akan menyalin data dari t1 ke t2 sambil meninggalkan t1 dan t2 sebagai contoh berbeda), dan jika seseorang dapat hidup dengan quirks dalam penanganan jenis nilai .net menggunakan, sebuah struktur. Jika seseorang menginginkan semantik tipe nilai tetapi quirks .net akan menyebabkan rusaknya semantik tipe nilai dalam aplikasi seseorang, gunakan kelas dan bergumam.

supercat
sumber
3

Selain itu jawaban yang sangat baik di atas:

Struktur adalah tipe nilai.

Mereka tidak pernah dapat diatur ke Nothing .

Mengatur struktur = Tidak ada, akan mengatur semua jenis nilainya ke nilai standarnya.

George Filippakos
sumber
2

ketika Anda tidak benar-benar membutuhkan perilaku, tetapi Anda membutuhkan lebih banyak struktur daripada array atau kamus sederhana.

Tindak lanjut Ini adalah bagaimana saya memikirkan struct secara umum. Saya tahu mereka dapat memiliki metode, tetapi saya suka menjaga perbedaan mental secara keseluruhan.

Jim Deville
sumber
Mengapa kamu mengatakan itu? Struct dapat memiliki metode.
Esteban Araya
2

Seperti yang dikatakan @Simon, struct menyediakan semantik "nilai-tipe" jadi jika Anda memerlukan perilaku yang mirip dengan tipe data bawaan, gunakan struct. Karena struct dilewatkan melalui salinan, Anda ingin memastikan ukurannya kecil, sekitar 16 byte.

Scott Dorman
sumber
2

Ini adalah topik lama, tetapi ingin memberikan tes benchmark sederhana.

Saya telah membuat dua file .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

dan

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Jalankan benchmark:

  • Buat 1 TestClass
  • Buat 1 TestStruct
  • Buat 100 TestClass
  • Buat 100 TestStruct
  • Buat 10.000 TestClass
  • Buat 10.000 TestStruct

Hasil:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |
Eduardas Šlutas
sumber
1

Hmm ...

Saya tidak akan menggunakan pengumpulan sampah sebagai argumen untuk / menentang penggunaan struct vs kelas. Tumpukan terkelola bekerja sangat mirip tumpukan - membuat objek hanya menempatkannya di atas tumpukan, yang hampir secepat mengalokasikan pada tumpukan. Selain itu, jika suatu objek berumur pendek dan tidak bertahan dari siklus GC, deallokasi bebas karena GC hanya bekerja dengan memori yang masih dapat diakses. (Cari MSDN, ada serangkaian artikel tentang manajemen memori .NET, saya terlalu malas untuk menggali untuk mereka).

Sebagian besar waktu saya menggunakan struct, saya akhirnya menendang diri saya untuk melakukannya, karena saya kemudian menemukan bahwa memiliki referensi semantik akan membuat segalanya lebih sederhana.

Bagaimanapun, keempat poin dalam artikel MSDN yang diposting di atas tampaknya merupakan pedoman yang baik.


sumber
1
Jika Anda kadang-kadang membutuhkan referensi semantik dengan sebuah struct, cukup deklarasikan class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, dan kemudian sebuah MutableHolder<T>akan menjadi objek dengan semantik kelas yang bisa berubah-ubah (ini berfungsi juga jika itu Tadalah struct atau tipe kelas yang tidak dapat diubah).
supercat
1

Struct berada di Stack bukan Heap jadi oleh karena itu mereka thread aman, dan harus digunakan ketika menerapkan pola objek transfer, Anda tidak pernah ingin menggunakan objek di Heap mereka volatile, Anda ingin dalam hal ini menggunakan Call Stack, ini adalah kasus dasar untuk menggunakan struct saya terkejut dengan semua jalan keluar jawabannya di sini,

Mendongkrak
sumber
-3

Saya pikir jawaban terbaik adalah dengan menggunakan struct ketika yang Anda butuhkan adalah kumpulan properti, kelas ketika itu adalah kumpulan properti DAN perilaku.

Lucian Gabriel Popescu
sumber
struct dapat memiliki metode juga
Nithin Chandran
tentu saja, tetapi jika Anda membutuhkan metode kemungkinan 99% Anda tidak benar menggunakan struct, bukan kelas. Satu-satunya pengecualian yang saya temukan ketika boleh menggunakan metode struct adalah callback
Lucian Gabriel Popescu