Menu

Struct di C#: Value Type, Struct vs Class dan Kapan Memakainya

Cara kerja struct di C#: semantik nilai dan penyalinan saat assignment, error saat mengubah elemen List<T>, aturan constructor, kesamaan, boxing, readonly struct dan record struct, serta perbandingan struct vs class beserta panduan kapan struct menjadi pilihan yang tepat.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Struct adalah value type yang kamu definisikan sendiri. Bentuknya mirip class, dengan field, property, method, dan constructor, tetapi variabel bertipe struct berisi datanya secara langsung, bukan referensi ke sebuah object. Satu perbedaan itu mengubah perilaku assignment, pemanggilan method, kesamaan, dan null.

Semantik nilai: assignment menyalin

Tipe numerik bawaan, bool, char, DateTime, TimeSpan, dan Guid semuanya adalah struct. Struct buatanmu sendiri bekerja dengan cara yang sama:

Output:

struct: s1.X = 1, s2.X = 50
class:  c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150

Dengan struct, s2 adalah salinan yang berdiri sendiri, dan Move juga bekerja pada salinannya sendiri. Agar sebuah method bisa mengubah struct milik pemanggil, teruskan by reference dengan ref (lihat ref dan out). Untuk menghindari penyalinan struct besar tanpa mengizinkan perubahan, C# 7.2 menambahkan parameter in.

Konsekuensi lain dari value type:

  • Variabel struct tidak pernah bisa null. PointS p = null; tidak bisa dikompilasi. Pakai PointS? (nullable value type) jika "tidak ada nilai" punya arti.
  • Struct yang belum diinisialisasi berisi nol semua: field numerik 0, field bool bernilai false, field reference bernilai null.
  • Struct tidak bisa mewarisi struct atau class lain, dan tidak ada yang bisa mewarisi struct. Struct bisa mengimplementasikan interface.
  • Variabel struct lokal biasanya berada di stack atau di dalam object yang memuatnya, jadi membuatnya tidak mengalokasikan memori di heap.

Jebakan List berisi struct: CS1612

Karena membaca struct menghasilkan salinan, baris yang terlihat polos ini tidak bisa dikompilasi:

var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable

Indexer List<T> adalah method yang mengembalikan salinan elemennya. Mengisi X pada salinan sementara itu tidak akan mengubah apa pun, jadi compiler menghentikanmu. Error yang sama muncul saat sebuah property mengembalikan struct: order.Location.X = 10. Salin, ubah, lalu tulis kembali:

Output:

35
7

Array adalah pengecualiannya: array[0] adalah elemen itu sendiri, bukan salinan. Masalah yang terus berulang ini adalah alasan di balik saran standar untuk membuat struct immutable: jika struct tidak bisa diubah, tidak ada yang hilang saat mengubah sebuah salinan.

Constructor dan nilai default

Di C# 7 sampai 9, aturan untuk constructor struct cukup ketat:

  • Kamu tidak bisa mendeklarasikan constructor tanpa parameter. new PointS() selalu ada dan mengisi setiap field dengan nol.
  • Constructor yang kamu deklarasikan wajib mengisi setiap field (dan auto-property) sebelum selesai.
  • Field initializer (public int X = 1;) tidak diizinkan pada field instance.

Output:

19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)

Money juga menunjukkan kenapa nilai default itu penting: Money yang bernilai nol punya currency null, dan kodemu harus menanganinya, karena array, default(T), dan field yang belum diinisialisasi semuanya menghasilkan nilai seperti itu.

Versi yang lebih baru melonggarkan aturan ini:

// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
    public int Retries = 3;
    public Settings() { }
}

// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.

Constructor tanpa parameter di C# 10 dijalankan untuk new Settings(), tetapi tidak untuk default(Settings) atau elemen array, yang tetap berisi nol semua. Perbedaan ini sering mengejutkan orang, jadi pakailah dengan hati-hati.

readonly struct (C# 7.2)

Menandai struct itu sendiri sebagai readonly membuat compiler menegakkan sifat immutable: setiap field harus readonly dan setiap auto-property harus get-only.

public readonly struct Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius) { Celsius = celsius; }

    public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta);   // returns a new value
}

Selain mendokumentasikan maksudnya, ini membantu performa: saat struct yang tidak readonly disimpan di field readonly atau diteruskan sebagai parameter in, compiler menyalinnya sebelum setiap pemanggilan method (compiler tidak bisa tahu bahwa method itu tidak akan mengubahnya). readonly struct tidak butuh defensive copy seperti itu. Di C# 7.0 kamu tetap bisa membuat setiap field readonly, seperti yang dilakukan Money dengan property get-only: itu memberimu sifat immutable, tetapi tidak menghemat salinan, karena compiler hanya memercayai struct yang dideklarasikan readonly.

Kesamaan

Secara default, Equals pada struct membandingkan field demi field, dan itulah perilaku nilai yang kamu inginkan. Tetapi implementasi default-nya (ValueType.Equals) bisa memakai reflection dan lambat, dan operator == sama sekali tidak didefinisikan: a == b pada struct buatanmu adalah error CS0019. Implementasikan keduanya jika struct itu akan dibandingkan:

Output:

True
True
True

Mengimplementasikan IEquatable<T> penting untuk collection: HashSet<T>, Dictionary<TKey, TValue>, dan List<T>.Contains memanggil Equals(GridCell) secara langsung alih-alih melakukan boxing pada setiap nilai untuk memanggil Equals(object). Record struct (di bawah) membuat semua ini untukmu.

Boxing

Mengonversi struct menjadi object atau menjadi tipe interface akan melakukan boxing: runtime menyalin nilainya ke object baru di heap. Setelah itu, box dan nilai aslinya tidak saling bergantung:

Output:

2
0

Boxing butuh satu alokasi setiap kali terjadi, itulah sebabnya collection lama yang non-generic seperti ArrayList lambat untuk value type, dan itulah sebabnya generic menggantikannya. Ini juga berarti struct yang bisa diubah dan diakses lewat interface akan berubah di box-nya, bukan di nilai aslinya, satu alasan lagi untuk menjaga struct tetap immutable.

Struct vs class

structclass
Jenisvalue typereference type
Assignment dan parametermenyalin datamenyalin referensi
Bisa nulltidak (T? bisa)ya
Nilai defaultsemua field bernilai nolnull
Pewarisantidak ada; bisa mengimplementasikan interfacesatu base class, interface
==tidak didefinisikan kecuali kamu membuat overload-nyakesamaan referensi kecuali punya overload
Equals defaultmembandingkan fieldmembandingkan referensi
Alokasiinline (stack atau object yang memuatnya)heap, dikelola garbage collector
Cocok untuknilai kecil yang immutableentity, state besar atau yang dipakai bersama

Kapan memakai struct

Pilih struct jika semua hal ini terpenuhi: tipe itu adalah satu nilai logis (koordinat, jumlah uang, warna, rentang tanggal), ukurannya kecil (panduan Microsoft sekitar 16 byte, kira-kira empat int), immutable, dan kamu tidak butuh pewarisan. Struct menguntungkan saat kamu membuatnya dalam jumlah sangat banyak, misalnya jutaan titik di dalam array, karena struct menghindari satu alokasi heap dan satu entri garbage collector untuk setiap item.

Pilih class untuk apa pun yang punya identitas (pelanggan, pesanan), apa pun yang besar, apa pun yang diubah dari beberapa tempat, dan setiap kali kamu ragu. Struct besar yang bisa diubah memberimu biaya penyalinan plus kebingungan karena mengubah salinan.

record struct (C# 10)

C# 10 menambahkan record struct, yang membuat value equality, ==, ToString, dan dukungan with untuk sebuah struct dalam satu baris:

public readonly record struct Coordinate(double Lat, double Lng);

var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same);            // True
Console.WriteLine(home);                    // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };

record struct biasa punya property posisional yang bisa diubah; readonly record struct membuatnya init-only, dan biasanya itulah yang kamu inginkan. Halaman record membahas versi class-nya dan menunjukkan member yang ditulis oleh compiler.

Kesalahan umum

  • Mengubah struct lewat salinan. list[0].X = 1 (CS1612), variabel foreach (CS1654), hasil dari getter sebuah property. Tulis kembali salinan yang sudah diubah, atau buat struct-nya immutable.
  • Struct yang besar. Setiap assignment dan pemanggilan menyalin seluruh isinya. Jika sudah lebih dari beberapa field, class biasanya lebih cepat.
  • Lupa nilai default yang berisi nol. Array dan default(T) membuat nilai struct tanpa menjalankan constructor-mu, jadi field yang kamu validasi di sana tetap bisa bernilai nol atau null.
  • Membandingkan dengan == tanpa mendefinisikannya. CS0019. Implementasikan IEquatable<T> dan operator-operatornya, atau pakai record struct.
  • Struct yang bisa diubah di balik interface. Boxing berarti perubahan terjadi pada salinan yang tidak sedang kamu lihat.

Pertanyaan yang sering diajukan

Apa itu struct di C#?

Struct adalah value type yang kamu definisikan sendiri: struct Point { public int X; public int Y; }. Variabel bertipe struct menyimpan datanya sendiri, bukan referensi ke sebuah object, jadi assignment dan meneruskannya ke method akan menyalin seluruh nilainya. int, double, DateTime, dan Guid semuanya adalah struct.

Apa perbedaan struct dan class di C#?

Class adalah reference type: beberapa variabel bisa berbagi satu object, dan null diizinkan. Struct adalah value type: setiap variabel punya salinannya sendiri, tidak bisa null (kecuali kamu memakai Point?), tidak bisa mewarisi atau diwarisi, dan nilai default-nya adalah semua field bernilai nol. Struct cocok untuk nilai kecil yang immutable; class cocok untuk entity yang punya identitas dan perilaku.

Kapan sebaiknya memakai struct alih-alih class di C#?

Saat tipe itu mewakili satu nilai kecil (koordinat, jumlah uang, rentang tanggal), idealnya immutable, dibandingkan berdasarkan isinya, dan dibuat dalam jumlah besar sehingga menghindari alokasi heap jadi penting. Panduan Microsoft menambahkan ukuran sekitar 16 byte atau kurang. Jika tipe itu punya identitas, banyak field, atau diubah lewat referensi, pakai class.

Kenapa muncul error "Cannot modify the return value because it is not a variable"?

Itu adalah error CS1612, biasanya dari list[0].X = 5 pada List<Point> berisi struct. Indexer mengembalikan salinan struct, jadi perubahan pada salinan itu akan hilang, dan compiler menolaknya. Salin elemennya ke variabel, ubah, lalu isi kembali: var p = list[0]; p.X = 5; list[0] = p;. Array tidak punya masalah ini karena array[0] merujuk ke elemen itu sendiri.

Apa itu readonly struct di C#?

readonly struct (C# 7.2) menyatakan bahwa tidak ada member struct yang mengubah state-nya: semua field harus readonly dan auto-property harus get-only. Compiler menegakkan aturan ini, dan ini membuat compiler bisa menghindari defensive copy saat struct diteruskan dengan in atau disimpan di field readonly. Sebagian besar struct sebaiknya readonly.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai