KembaliKembali ke artikel

Mengupas Value Type dan Reference Type di C#

6 Agustus 2026

34 menit baca

15 pembaca

Mengupas Value Type dan Reference Type di C#

Memahami perbedaan value type dan reference type merupakan salah satu fondasi terpenting dalam C#. Artikel ini membahas bagaimana stack, heap, Garbage Collector (GC), class, dan struct saling berkaitan, lengkap dengan contoh kode, benchmark, serta praktik terbaik agar Anda dapat memilih tipe data yang tepat dan menulis aplikasi yang lebih efisien.

C#Advanced C#

Pendahuluan

Konsep value dan reference type sering kali kurang mendapat perhatian, terutama bagi developer yang sehari-hari lebih banyak bekerja dengan paradigma object-oriented. Padahal, memahami konsep ini akan membantu kita untuk mengerti konsep fundamental dalam C#, contohnya mengapa C# menyediakan struct selain class, serta kapan masing-masing sebaiknya digunakan.

Pada materi ini, kita akan membahas konsep tersebut secara bertahap. Kita akan mulai dari dasar-dasar seperti stack, heap, dan garbage collector (GC) agar memiliki gambaran yang utuh tentang bagaimana data dikelola di dalam memori. Setelah fondasinya dipahami, barulah kita melihat bagaimana konsep-konsep tersebut diterapkan dalam C# melalui value type dan reference type.

Stack dan Heap Itu Apa?

Saat sebuah program dijalankan, sistem operasi mengalokasikan sebagian RAM (memori utama komputer) agar dapat digunakan oleh program tersebut. Di dalam aplikasi C#, memori yang dialokasikan ini kemudian dikelola oleh CLR (Common Language Runtime). Salah satu tugas CLR adalah mengatur bagaimana memori digunakan selama program berjalan.

Dalam pengelolaan memori tersebut, ada dua area yang paling sering dibahas, yaitu stack dan heap.

Perlu dipahami bahwa stack dan heap sama-sama berada di RAM. Keduanya bukan jenis memori yang berbeda secara fisik, melainkan dua cara berbeda dalam mengelola ruang memori yang tersedia. Perbedaannya terletak pada mekanisme pengelolaannya, seperti cara data dialokasikan, diakses, dan dibebaskan dari memori, bukan pada lokasi fisiknya.

Stack itu seperti meja kerja yang rapi

Bayangkan stack seperti tumpukan piring. Setiap kali sebuah method dipanggil, runtime menambahkan satu "piring" baru di bagian paling atas. Piring tersebut menyimpan informasi yang dibutuhkan selama method berjalan, termasuk variabel-variabel lokal yang berada di dalamnya.

Ketika method selesai dieksekusi, piring yang berada di paling atas akan langsung diambil. Karena prosesnya selalu mengikuti urutan yang terakhir masuk akan menjadi yang pertama keluar (Last In, First Out atau LIFO), pengelolaan data di stack menjadi sangat sederhana dan efisien.

Inilah salah satu alasan mengapa operasi pada stack sangat cepat. Alokasi memori dilakukan dengan menambahkan data di bagian atas stack, sedangkan pelepasannya cukup dengan mengembalikan posisi penunjuk (stack pointer) ke kondisi sebelumnya. Tidak diperlukan proses pencarian memori yang sudah tidak digunakan maupun mekanisme seperti Garbage Collector (GC) untuk membersihkannya.

Heap itu seperti gudang besar yang butuh "petugas kebersihan"

Berbeda dengan stack, heap dapat dibayangkan seperti sebuah gudang besar yang menyimpan berbagai macam barang dengan ukuran yang beragam. Tidak ada aturan bahwa data yang terakhir disimpan harus menjadi data pertama yang diambil. Karena itu, pengelolaan data di heap jauh lebih fleksibel dibandingkan stack.

Data yang berada di heap juga dapat memiliki masa hidup yang lebih panjang. Sebuah objek tetap dapat digunakan meskipun method yang membuatnya telah selesai dijalankan, selama masih ada bagian lain dari program yang mereferensikannya.

Fleksibilitas ini membuat pengelolaan heap menjadi lebih kompleks. Ketika sebuah objek sudah tidak lagi memiliki referensi dan tidak dapat diakses oleh program, memori yang ditempatinya tidak langsung dibebaskan secara otomatis. Di sinilah Garbage Collector (GC) berperan, yaitu dengan mendeteksi objek-objek yang sudah tidak digunakan lagi, kemudian mengembalikan memori yang ditempatinya agar dapat digunakan kembali.

Garbage Collector (GC) Itu Apa?

Garbage Collector (GC) adalah salah satu komponen di dalam CLR (Common Language Runtime) yang bertugas mengelola memori pada heap. Tugas utamanya adalah mencari objek-objek yang sudah tidak dapat dijangkau lagi oleh program (unreachable), yaitu objek yang sudah tidak memiliki referensi aktif. Setelah dipastikan tidak lagi digunakan, memori yang ditempati objek tersebut akan dibebaskan sehingga dapat digunakan kembali.

Sebagai analogi, bayangkan GC seperti petugas yang sesekali berkeliling di sebuah gudang untuk memeriksa barang-barang yang sudah tidak memiliki pemilik. Barang-barang tersebut kemudian dipindahkan agar ruang penyimpanan bisa digunakan kembali. Berbeda dengan stack, yang dapat dibersihkan secara langsung ketika sebuah method selesai dieksekusi, proses ini memerlukan waktu dan sumber daya CPU untuk melakukan pemeriksaan terhadap objek-objek di heap.

Karena itulah aktivitas GC memiliki biaya (overhead). Dalam kondisi tertentu, GC perlu menghentikan sebagian eksekusi aplikasi untuk memastikan proses pembersihan berlangsung dengan aman. Kondisi ini dikenal sebagai GC pause. Pada .NET modern, sebagian besar pekerjaan GC dapat berjalan secara bersamaan (concurrently) dengan aplikasi sehingga jeda ini biasanya sangat singkat. Namun, jika aplikasi terus-menerus membuat banyak objek baru, GC akan lebih sering dijalankan. Akibatnya, penggunaan CPU dapat meningkat dan waktu respons (latency) aplikasi menjadi kurang stabil.

Dari pembahasan ini, kita dapat menarik kesimpulan sementara. Stack menawarkan pengelolaan memori yang sangat cepat karena alokasi dan pelepasannya berlangsung secara otomatis mengikuti siklus hidup pemanggilan method. Sementara itu, heap memberikan fleksibilitas yang lebih besar karena objek dapat hidup lebih lama, tetapi konsekuensinya adalah adanya biaya tambahan untuk mengelola dan membersihkan memori melalui Garbage Collector (GC).

Apa Itu Value Type dan Reference Type?

Setelah memahami bagaimana stack, heap, dan Garbage Collector (GC) bekerja, sekarang kita akan mulai masuk ke inti pembahasan, yaitu value type dan reference type.

Value type dan reference type adalah konsep yang menjelaskan bagaimana suatu tipe data direpresentasikan dan dikelola di memori. Perbedaan keduanya bukan terletak pada jenis data yang dapat disimpan, melainkan pada cara data tersebut disimpan, disalin, dan diakses selama program berjalan.

Konsep ini bukan hanya dimiliki oleh C#. Hampir semua bahasa pemrograman modern mengenalnya, meskipun istilah maupun cara implementasinya dapat berbeda-beda. Dengan memahami konsep ini, akan lebih mudah untuk mengerti contohnya mengapa tipe datastring bisa diisi null sedangkan int tidak bisa.

Value type

Sebuah tipe data dikatakan sebagai value type jika variabelnya menyimpan nilai secara langsung. Artinya, nilai tersebut berada di dalam variabel itu sendiri, bukan berupa alamat atau penunjuk ke lokasi lain di memori.

Sebagai ilustrasi, bayangkan kita menuliskan angka 5 di selembar kertas. Ketika melihat kertas tersebut, kita langsung mengetahui nilainya tanpa perlu mencari informasi tambahan di tempat lain. Begitu pula dengan value type, variabelnya sudah menyimpan data yang dibutuhkan secara langsung.

Pada banyak kasus di C#, value type disimpan di stack, khususnya ketika digunakan sebagai variabel lokal di dalam sebuah method. Karena stack memiliki mekanisme alokasi dan pelepasan memori yang sangat sederhana, operasi terhadap value type biasanya dapat dilakukan dengan sangat cepat. Ketika method dipanggil, ruang di stack cukup "ditambah" untuk menyimpan nilainya. Ketika method selesai, ruang tersebut langsung dilepaskan tanpa perlu melalui proses Garbage Collection (GC).

Perlu dicatat bahwa ini bukan berarti semua value type selalu berada di stack. Misalnya, jika sebuah value type menjadi bagian dari sebuah objek (class), maka nilainya akan berada bersama objek tersebut di heap. Jadi, value type tidak ditentukan oleh lokasi penyimpanannya, melainkan oleh cara tipe tersebut menyimpan dan menyalin nilainya.

Karena nilainya disimpan secara langsung, ketika sebuah value type disalin ke variabel lain, yang disalin adalah nilainya, bukan referensi ke lokasi memori yang sama. Akibatnya, setiap variabel memiliki salinannya sendiri, sehingga perubahan pada satu variabel tidak akan memengaruhi variabel lainnya.

Konsep ini mungkin terlihat sederhana, tetapi menjadi salah satu perbedaan paling mendasar antara value type dan reference type. Pada bagian berikutnya, kita akan melihat bagaimana reference type bekerja dan mengapa perilakunya berbeda.

Reference type

Berbeda dengan value type, sebuah tipe data dikatakan sebagai reference type jika variabelnya tidak menyimpan data secara langsung, melainkan menyimpan referensi yang menunjuk ke lokasi tempat data tersebut berada.

Sebagai ilustrasi, bayangkan kita menuliskan alamat sebuah rumah di selembar kertas. Kertas tersebut tidak berisi isi rumahnya, melainkan hanya informasi mengenai lokasinya. Untuk mengetahui apa yang ada di dalam rumah tersebut, kita harus menuju alamat itu terlebih dahulu. Begitu pula dengan reference type, variabelnya hanya menyimpan referensi, sedangkan objek yang sebenarnya berada di lokasi lain di memori.

Di C#, objek dari reference type umumnya dialokasikan di heap. Berbeda dengan stack yang memiliki mekanisme alokasi dan pelepasan memori yang sederhana, objek di heap dapat tetap hidup meskipun method yang membuatnya telah selesai dieksekusi, selama masih ada referensi yang menunjuk ke objek tersebut. Ketika sudah tidak ada lagi referensi yang dapat menjangkaunya, Garbage Collector (GC) akan membebaskan memori yang digunakan oleh objek tersebut.

Perhatikan contoh berikut:

C#

int x = 5; // value type: x LANGSUNG berisi angka 5
var list = new List<int>(); // reference type: list berisi ALAMAT
// menuju objek List yang sebenarnya ada di heap

Pada contoh di atas, variabel x langsung menyimpan nilai 5. Sebaliknya, variabel list tidak menyimpan seluruh isi dari List<int>, melainkan hanya referensi menuju objek List<int> yang berada di heap. Ketika kita memanggil list.Add(1), operasi tersebut dilakukan terhadap objek yang dirujuk oleh referensi tersebut, bukan terhadap variabel list itu sendiri.

Perbedaan ini menjadi sangat penting ketika sebuah variabel disalin atau dikirim sebagai parameter ke method. Pada value type, yang disalin adalah nilainya. Sementara pada reference type, yang disalin adalah referensinya, sehingga dua variabel dapat menunjuk ke objek yang sama di memori.

Kenapa konsep ini berkaitan dengan stack dan heap?

Sekarang kita mengetahui bahwa konsep value type dan reference type sangat berhubungan erat dengan mbahasan mengenai stack dan heap.

Pada value type, variabel menyimpan nilainya secara langsung. Karena itu, ketika sebuah value type digunakan sebagai variabel lokal di dalam method, nilainya biasanya disimpan di stack. Hal ini membuat proses alokasi dan pelepasan memorinya sangat cepat karena cukup mengikuti mekanisme stack, tanpa memerlukan bantuan Garbage Collector (GC).

Sebaliknya, pada reference type, variabel hanya menyimpan referensi menuju objek yang sebenarnya. Variabel referensi tersebut dapat berada di stack apabila merupakan variabel lokal, tetapi objek yang dirujuk akan dialokasikan di heap. Selama objek tersebut masih memiliki referensi aktif, objek akan tetap berada di heap. Ketika sudah tidak lagi dapat dijangkau oleh program, Garbage Collector (GC) akan membebaskan memori yang digunakannya.

Penting untuk diingat bahwa hubungan antara value type dan stack, maupun reference type dan heap, bukanlah aturan mutlak. Sebagai contoh, sebuah value type yang menjadi anggota dari sebuah objek (class) akan ikut disimpan bersama objek tersebut di heap. Oleh karena itu, value type dan reference type adalah konsep yang menjelaskan cara sebuah tipe data menyimpan dan menyalin nilai, sedangkan stack dan heap adalah mekanisme pengelolaan memori yang digunakan saat program berjalan.

Tipe Data Apa Saja yang Termasuk Value Type dan Reference Type?

Setelah memahami perbedaan konsepnya, sekarang mari kita lihat bagaimana pengelompokan tipe data di C#. Perlu diingat bahwa yang membedakan value type dan reference type bukanlah lokasi penyimpanannya, melainkan cara tipe data tersebut menyimpan dan menyalin nilainya.

Value Type

Tipe data berikut termasuk value type:

  • Tipe data bawaan seperti bool, byte, char, short, int, long, float, double, decimal, serta tipe numerik lainnya.

  • struct yang dibuat sendiri.

  • enum.

  • Berbagai struct bawaan .NET, seperti DateTime, TimeSpan, Guid, DateOnly, TimeOnly, dan lain sebagainya.

Karena menyimpan nilainya secara langsung, value type umumnya dialokasikan di stack ketika digunakan sebagai variabel lokal. Namun, jika menjadi bagian dari sebuah objek (class) atau elemen sebuah array, nilainya akan berada bersama objek tersebut di heap.

Reference Type

Tipe data berikut termasuk reference type:

  • Semua class, baik bawaan .NET maupun yang dibuat sendiri.

  • string (meskipun memiliki perilaku yang berbeda karena bersifat immutable, string tetap merupakan sebuah class).

  • Koleksi seperti List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, dan sebagainya.

  • Array (int[], string[], dan lain-lain). Meskipun elemennya bisa berupa value type, objek array itu sendiri merupakan reference type.

  • delegate dan objek yang dihasilkan oleh closure.

Pada reference type, variabel hanya menyimpan referensi menuju objek yang berada di heap. Ketika variabel tersebut disalin, yang disalin adalah referensinya sehingga beberapa variabel dapat menunjuk ke objek yang sama.

Catatan tentang string

string sering dianggap mirip value type karena bersifat immutable (isinya tidak dapat diubah setelah dibuat) dan dapat dibandingkan menggunakan operator ==. Namun, secara implementasi string tetap merupakan sebuah reference type karena dideklarasikan sebagai class. Artinya, variabel string tetap menyimpan referensi ke objek string yang berada di heap.

Definisi Dasar class dan struct

Setelah memahami konsep value type dan reference type, sekarang kita bisa melihat bagaimana konsep tersebut diterapkan di C#. Dua cara yang paling umum untuk mendefinisikan tipe data baru adalah menggunakan class dan struct.

Perbedaan paling mendasar di antara keduanya adalah bahwa class merupakan reference type, sedangkan struct merupakan value type. Perbedaan inilah yang memengaruhi cara objek disimpan, disalin, dan dikelola di memori.

class si reference type

class digunakan untuk mendefinisikan sebuah reference type. Ketika sebuah objek dibuat dari class, objek tersebut akan dialokasikan di heap, sedangkan variabelnya hanya menyimpan referensi yang menunjuk ke objek tersebut.

C#

public class PointClass
{
public int X;
public int Y;
}

Karena variabel class hanya menyimpan referensi, menyalin sebuah variabel bertipe class tidak akan membuat objek baru. Yang disalin hanyalah referensinya, sehingga kedua variabel akan menunjuk ke objek yang sama di heap. Akibatnya, perubahan yang dilakukan melalui salah satu variabel juga akan terlihat ketika objek diakses melalui variabel lainnya.

struct si value type

struct digunakan untuk mendefinisikan sebuah value type. Berbeda dengan class, sebuah struct menyimpan nilainya secara langsung, bukan melalui referensi.

C#

public struct PointStruct
{
public int X;
public int Y;
}

Dalam banyak kasus, sebuah struct yang digunakan sebagai variabel lokal akan disimpan di stack. Namun, jika struct menjadi anggota dari sebuah objek (class) atau elemen sebuah array, nilainya akan berada bersama objek tersebut di heap. Jadi, yang membedakan struct bukanlah lokasi penyimpanannya, melainkan fakta bahwa ia merupakan value type.

Karena struct adalah value type, ketika sebuah variabel bertipe struct disalin ke variabel lain, yang disalin adalah seluruh nilainya. Hasilnya, masing-masing variabel memiliki salinan data sendiri sehingga perubahan pada salah satu variabel tidak akan memengaruhi variabel lainnya.

Pada bagian berikutnya, kita akan melihat perbedaan tersebut secara langsung melalui beberapa contoh kode sehingga perilaku class dan struct akan terlihat lebih jelas.

Perbedaan class vs struct

class (reference type)struct (value type)
Lokasi dataHeapStack (selama tidak "kabur")
Isi variabelAlamat menuju dataNilai datanya langsung
Saat disalin (b = a)Menyalin alamat → a dan b menunjuk objek yang samaMenyalin seluruh nilai → a dan b jadi dua data terpisah
Butuh GC untuk dibersihkan?YaTidak, selama tetap di stack
Contoh bawaan .NETstring, List<T>, semua class customint, double, bool, DateTime, struct custom

Contoh perbedaan perilaku saat disalin:

C#

// struct - value type
var a = new PointStruct { X = 1, Y = 2 };
var b = a; // copy nilai, bukan reference
b.X = 99;
Console.WriteLine(a.X); // tetap 1, karena b adalah salinan terpisah
// class - reference type
var c = new PointClass { X = 1, Y = 2 };
var d = c; // copy reference, menunjuk objek yang sama
d.X = 99;
Console.WriteLine(c.X); // ikut berubah jadi 99

Penting dicatat juga bahwa value type yang kita bahas di sini adalah konsep bahasa pemrograman, bukan Value Object dalam Domain-Driven Design (DDD) keduanya memiliki istilah yang mirip tapi konteksnya berbeda.

Mengapa Ada struct?

Mungkin muncul pertanyaan, jika class sudah dapat digunakan untuk membuat berbagai macam tipe data, mengapa C# masih menyediakan struct?

Jawabannya berkaitan dengan cara data dikelola di memori. Seperti yang telah dibahas sebelumnya, objek yang dibuat dari class umumnya dialokasikan di heap. Meskipun proses ini sudah sangat dioptimalkan oleh .NET, setiap objek yang dialokasikan di heap tetap perlu dikelola oleh Garbage Collector (GC). Untuk sebagian besar aplikasi, biaya ini hampir tidak terasa. Namun, pada skenario yang melibatkan pembuatan objek dalam jumlah sangat besar, biaya tersebut dapat mulai memengaruhi performa.

Bayangkan sebuah aplikasi yang memproses jutaan koordinat, jutaan titik piksel pada gambar, atau jutaan baris data. Jika setiap data kecil tersebut direpresentasikan sebagai sebuah class, maka akan ada jutaan objek di heap yang harus dialokasikan dan pada akhirnya dibersihkan oleh GC. Semakin banyak objek yang dibuat, semakin besar pula pekerjaan yang harus dilakukan oleh GC.

Di sinilah struct menjadi solusi. Karena struct merupakan value type, data yang berukuran kecil dapat disimpan secara langsung tanpa harus selalu membuat objek terpisah di heap. Pada banyak kasus, terutama ketika digunakan sebagai variabel lokal, hal ini memungkinkan data dikelola melalui stack yang memiliki mekanisme alokasi dan pelepasan memori yang jauh lebih sederhana.

Oleh karena itu, struct umumnya cocok digunakan untuk tipe data yang memiliki karakteristik berikut:

  • Berukuran relatif kecil.
  • Bersifat sederhana dan hanya terdiri dari beberapa field.
  • Sering dibuat dalam jumlah besar.
  • Memiliki masa hidup yang pendek.
  • Merepresentasikan sebuah nilai (value) dan tidak perlu dibagikan sebagai objek yang sama ke banyak bagian program.

Perlu diingat bahwa struct bukanlah pengganti class. Keduanya dirancang untuk kebutuhan yang berbeda. Sebagian besar model bisnis (domain model), layanan (service), repository, dan objek yang memiliki identitas lebih tepat dibuat menggunakan class. Sebaliknya, struct lebih sesuai untuk merepresentasikan nilai-nilai kecil seperti koordinat, ukuran, warna, vektor, tanggal, waktu, atau tipe data sederhana lainnya.

Kalau struct Lebih Ringan, Mengapa Masih Membutuhkan class?

Setelah mengetahui bahwa struct dapat mengurangi alokasi di heap pada kondisi tertentu, mungkin muncul pertanyaan lain: mengapa tidak menggunakan struct saja untuk semuanya?

Jawabannya adalah karena struct dan class dirancang untuk menyelesaikan masalah yang berbeda. Meskipun struct dapat memberikan keuntungan dari sisi performa pada kasus tertentu, ada banyak situasi di mana class justru merupakan pilihan yang lebih tepat.

1. Ketika Objek Memiliki Identitas

Beberapa objek bukan sekadar kumpulan nilai, tetapi memiliki identitas. Misalnya HttpClient, koneksi database, atau objek User yang digunakan oleh beberapa bagian aplikasi secara bersamaan.

Pada kasus seperti ini, kita ingin semua bagian program mengakses objek yang sama. Jika salah satu bagian mengubah keadaan objek tersebut, perubahan itu juga harus terlihat oleh bagian lain yang menggunakan objek yang sama. Karakteristik ini secara alami dimiliki oleh reference type, sehingga class menjadi pilihan yang tepat.

Apabila objek seperti ini dibuat sebagai struct, setiap proses penyalinan akan menghasilkan salinan baru yang berdiri sendiri. Akibatnya, perubahan pada satu salinan tidak akan memengaruhi salinan lainnya.

2. Ketika Data Berukuran Besar

struct selalu menyalin seluruh nilainya ketika di-assign ke variabel lain atau dikirim sebagai parameter (kecuali menggunakan ref, in, atau out).

Jika sebuah struct memiliki banyak field atau ukurannya cukup besar, biaya penyalinan ini dapat menjadi lebih mahal dibandingkan hanya menyalin sebuah referensi yang ukurannya jauh lebih kecil. Pada kondisi seperti ini, menggunakan class sering kali lebih efisien.

3. Ketika Membutuhkan Inheritance dan Polymorphism

struct tidak mendukung inheritance. Sebuah struct tidak dapat menjadi turunan dari struct lain maupun dijadikan kelas dasar bagi tipe lain. Meskipun demikian, struct tetap dapat mengimplementasikan satu atau lebih interface.

Sebaliknya, class mendukung pewarisan (inheritance), polimorfisme (polymorphism), serta berbagai pola desain yang memanfaatkan hierarki objek.

4. Ketika Objek Bersifat Opsional

Variabel bertipe class secara alami dapat bernilai null, yang menandakan bahwa objek tersebut belum ada atau belum dibuat.

Sebaliknya, sebuah struct selalu memiliki nilai. Jika diperlukan kondisi "tidak memiliki nilai", biasanya digunakan nullable value type seperti int?, DateTime?, atau Point?, yang akan kita bahas pada bagian selanjutnya.

Memilih struct atau class

Tidak ada pilihan yang selalu lebih baik. Yang terpenting adalah memilih tipe data yang paling sesuai dengan karakteristik data yang dimodelkan.

Secara umum:

  • Gunakan struct untuk data yang kecil, sederhana, dan merepresentasikan sebuah nilai.

  • Gunakan class untuk objek yang memiliki identitas, sering dibagikan ke banyak bagian program, berukuran besar, atau membutuhkan fitur seperti inheritance dan polymorphism.

Dengan memahami perbedaan ini, kita tidak hanya dapat menulis kode yang benar, tetapi juga dapat membuat keputusan desain yang lebih tepat sesuai kebutuhan aplikasi.

Null dan Kaitannya dengan Value Type dan Reference Type

Setelah memahami perbedaan antara value type dan reference type, ada satu konsep lain yang tidak kalah penting, yaitu null dan nullable value type. Konsep ini sering menimbulkan pertanyaan, misalnya mengapa string dapat bernilai null, sedangkan int tidak.

Mengapa Reference Type Bisa Bernilai null?

Seperti yang telah dibahas sebelumnya, variabel bertipe reference type tidak menyimpan objek secara langsung, melainkan menyimpan referensi yang menunjuk ke objek tersebut.

Ketika sebuah variabel reference type bernilai null, artinya variabel tersebut belum menunjuk ke objek mana pun. Dengan kata lain, tidak ada objek di heap yang direferensikan oleh variabel tersebut.

Plain Text

string s = null;
PointClass p = null;

Pada contoh di atas, baik s maupun p tidak memiliki referensi ke objek apa pun. Karena tidak ada objek yang dibuat, tidak ada pula alokasi memori di heap.

Mengapa Value Type Tidak Bisa Bernilai null?

Berbeda dengan reference type, sebuah value type selalu menyimpan nilainya secara langsung. Oleh karena itu, secara default sebuah value type harus selalu memiliki nilai.

Sebagai contoh, sebuah int selalu merepresentasikan sebuah bilangan. Tidak ada keadaan di mana sebuah int berarti "belum memiliki nilai".

int x = null; // Error

Karena alasan tersebut, kode di atas tidak dapat dikompilasi.

Selain itu, setiap value type memiliki nilai bawaan (default value). Misalnya:

  • int memiliki nilai default 0.
  • bool memiliki nilai default false.
  • char memiliki nilai default '\0'.
  • Sebuah struct akan memiliki setiap field yang diinisialisasi dengan nilai default dari tipe masing-masing.

Perlu diperhatikan bahwa nilai default bukanlah null. Nilai tersebut tetap merupakan nilai yang valid sesuai tipe datanya.

Solusinya: Nullable<T> atau T?

Ada kalanya sebuah value type memang perlu merepresentasikan keadaan "belum memiliki nilai", misalnya tanggal lahir yang belum diisi atau nilai ujian yang belum tersedia.

Untuk kebutuhan tersebut, C# menyediakan Nullable<T>, yang dapat ditulis lebih ringkas menggunakan sintaks T?.

Plain Text

int? umur = null;
DateTime? tanggalLahir = null;

Meskipun dapat bernilai null, Nullable<T> tetap merupakan sebuah value type, bukan berubah menjadi reference type.

Secara konseptual, Nullable<T> dapat dibayangkan sebagai sebuah struct yang menyimpan dua informasi:

  • Nilai sebenarnya (Value).
  • Penanda (HasValue) yang menunjukkan apakah nilai tersebut ada atau tidak.

Contohnya:

C#

int? x = null;
Console.WriteLine(x.HasValue); // False
Console.WriteLine(x.GetValueOrDefault()); // 0
int? y = 10;
Console.WriteLine(y.HasValue); // True
Console.WriteLine(y.Value); // 10

Ketika HasValue bernilai false, berarti variabel tersebut tidak memiliki nilai. Sebaliknya, jika HasValue bernilai true, nilai aslinya dapat diakses melalui properti Value.

Kaitannya dengan Stack dan Heap

Karena Nullable<T> tetap merupakan value type, ia mengikuti aturan yang sama dengan value type lainnya. Dalam banyak kasus, ketika digunakan sebagai variabel lokal, nilainya akan berada di stack dan tidak memerlukan objek tambahan di heap maupun pengelolaan oleh Garbage Collector (GC).

Namun, seperti value type lainnya, lokasi penyimpanannya tetap bergantung pada konteks penggunaannya. Jika Nullable<T> menjadi field dari sebuah class atau elemen sebuah array, nilainya akan berada bersama objek tersebut di heap.

Dengan demikian, kemampuan sebuah tipe untuk bernilai null tidak menentukan apakah tipe tersebut merupakan value type atau reference type. null hanyalah representasi bahwa sebuah nilai belum ada. Pada reference type, null berarti tidak ada objek yang direferensikan. Pada value type, kemampuan tersebut disediakan melalui pembungkus Nullable<T>, yang tetap merupakan sebuah value type.

Objek Disimpan di Mana? Kaitannya dengan Garbage Collector (GC)

Sekarang kita dapat menghubungkan seluruh konsep yang telah dipelajari untuk memahami bagaimana sebuah objek dikelola di memori.

Ketika kita membuat sebuah objek menggunakan new pada sebuah class, objek tersebut akan dialokasikan di heap.

C#

Person person = new Person();

Secara konseptual, proses di atas dapat dibayangkan seperti berikut:

  1. CLR mengalokasikan ruang di heap untuk menyimpan objek Person.

  2. Seluruh field pada objek tersebut diinisialisasi dengan nilai default masing-masing.

  3. Sebuah referensi yang menunjuk ke objek tersebut dikembalikan.

  4. Referensi tersebut kemudian disimpan ke dalam variabel person.

Artinya, variabel person tidak menyimpan objek Person itu sendiri. Variabel tersebut hanya menyimpan referensi menuju lokasi objek yang berada di heap.

Secara sederhana, ilustrasinya dapat digambarkan seperti ini:

Plain Text

Stack Heap
+-------------------+ +----------------------+
| person ---------- |------------> | Person |
| (reference) | | Name = null |
+-------------------+ | Age = 0 |
+----------------------+

Pada contoh di atas, karena person merupakan variabel lokal di dalam sebuah method, referensinya biasanya berada di stack, sedangkan objek Person yang sebenarnya berada di heap. Jika person merupakan field dari objek lain, maka referensi tersebut akan ikut menjadi bagian dari objek tersebut dan berada di heap bersama objek induknya.

Inilah mengapa kita sering mengatakan bahwa reference type berada di heap. Yang sebenarnya berada di heap adalah objeknya, sedangkan referensinya berada di lokasi tempat variabel tersebut disimpan, yaitu tetap di stack.

Mengapa Ini Berkaitan dengan GC?

Semua objek yang berada di heap akan dikelola oleh Garbage Collector (GC). Selama masih ada referensi yang dapat menjangkau sebuah objek, GC akan menganggap objek tersebut masih digunakan.

Sebaliknya, ketika tidak ada lagi referensi yang mengarah ke objek tersebut, objek menjadi unreachable. Pada saat itulah GC dapat membebaskan memori yang digunakan sehingga ruang tersebut dapat dipakai kembali.

Berbeda dengan class, sebuah struct tidak membuat objek terpisah di heap. Karena merupakan value type, nilainya disimpan langsung di lokasi tempat variabel tersebut berada. Jika struct digunakan sebagai variabel lokal, nilainya biasanya berada di stack. Ketika method selesai dieksekusi, memori tersebut langsung dilepaskan tanpa perlu melalui proses Garbage Collection.

Inilah salah satu alasan mengapa struct dapat memberikan keuntungan performa pada kondisi tertentu. Jika sebuah aplikasi membuat jutaan data kecil yang hanya digunakan dalam waktu singkat, penggunaan struct dapat mengurangi jumlah objek yang harus dialokasikan di heap. Semakin sedikit objek yang dibuat di heap, semakin ringan pula pekerjaan Garbage Collector, sehingga penggunaan CPU dan jeda akibat GC juga dapat berkurang.

Tentu saja, keuntungan tersebut hanya berlaku jika struct memang sesuai dengan karakteristik datanya. Memilih struct semata-mata demi menghindari GC bukanlah tujuan utama. Yang terpenting adalah memilih tipe data yang paling tepat untuk merepresentasikan model yang sedang dibangun, lalu mempertimbangkan aspek performa ketika memang diperlukan.

Kapan value type bisa kabur ke heap?

Sebelumnya kita telah membahas bahwa value type sering kali disimpan di stack ketika digunakan sebagai variabel lokal. Namun, penting untuk dipahami bahwa ini bukan aturan mutlak.

Dalam beberapa kondisi, sebuah value type tetap berada di heap. Hal ini bukan karena value type berubah menjadi reference type, melainkan karena lokasi penyimpanannya mengikuti konteks penggunaannya.

Berikut adalah beberapa kondisi yang paling umum ketika value type bisa pindah ke heap, kita gunakan contoh pada penggunaan struct .

Boxing

Boxing terjadi ketika sebuah value type perlu diperlakukan sebagai reference type, misalnya ketika dikonversi ke object atau ke sebuah interface.

Pada saat itulah CLR membuat salinan baru dari value type tersebut di heap.

C#

struct Point
{
public int X;
public int Y;
}
Point p = new() { X = 1, Y = 2 };
object boxed = p; // Boxing

Ilustrasinya dapat digambarkan seperti berikut.

Plain Text

Stack Heap
+-------------+ +----------------------+
| p | | Boxed Point |
| X = 1 | | X = 1 |
| Y = 2 | | Y = 2 |
+-------------+ +----------------------+
object boxed

Perlu diperhatikan bahwa boxing membuat salinan baru. Setelah boxing terjadi, perubahan pada p tidak akan memengaruhi objek yang berada di heap, begitu pula sebaliknya.

Contoh lain yang sering tidak disadari adalah ketika sebuah value type dikirim ke API yang menerima parameter bertipe object.

C#

Console.WriteLine(p);

Pada beberapa overload Console.WriteLine, pemanggilan seperti di atas dapat menyebabkan boxing. (Pada .NET modern, compiler sering memilih overload yang lebih spesifik sehingga boxing tidak selalu terjadi.)

struct Menjadi Field dari Sebuah class

Sebuah struct dapat menjadi anggota (field) dari sebuah class.

C#

public class Container
{
public PointStruct Position;
}

Pada kasus ini, Position tetap merupakan value type, tetapi karena ia menjadi bagian dari objek Container, nilainya ikut disimpan bersama objek tersebut di heap.

Plain Text

Heap
+-----------------------------------+
| Container |
| |
| Position |
| X = 1 |
| Y = 2 |
+-----------------------------------+

Perhatikan contoh berikut.

C#

var c1 = new Container
{
Position = new PointStruct { X = 1, Y = 2 }
};
var c2 = c1;
c2.Position.X = 99;
Console.WriteLine(c1.Position.X); // 99

Mengapa nilainya ikut berubah?

Bukan karena PointStruct berubah menjadi reference type, melainkan karena c1 dan c2 menunjuk ke objek Container yang sama. Keduanya mengakses field Position yang sama di dalam objek tersebut.

Sebaliknya, jika kita menyalin Position ke variabel lain, perilaku khas value type kembali terlihat.

C#

var position = c1.Position;
position.X = 500;
Console.WriteLine(c1.Position.X); // Tetap 99

Pada contoh di atas, position merupakan salinan baru sehingga perubahan terhadapnya tidak memengaruhi Position milik Container.

struct Ditangkap oleh Closure

Ketika sebuah variabel lokal digunakan oleh lambda expression atau anonymous method, compiler akan membuat sebuah objek tambahan (closure object) untuk menyimpan variabel tersebut.

Akibatnya, struct yang ditangkap oleh closure akan ikut berada di heap.

C#

PointStruct p = new() { X = 1, Y = 2 };
Func<int> getX = () => p.X;

struct Disimpan di Dalam Koleksi

Koleksi seperti List<T> atau array merupakan reference type sehingga objek koleksinya berada di heap.

Jika elemen yang disimpan berupa struct, nilai struct tersebut akan berada di dalam memori koleksi, bukan menjadi objek terpisah.

C#

List<PointStruct> points = new();
points.Add(new PointStruct { X = 1, Y = 2 });

Artinya, tidak ada satu objek heap untuk setiap PointStruct. Nilai-nilai PointStruct disimpan secara langsung sebagai bagian dari buffer internal milik List<T>.

Digunakan pada async/await

Method yang menggunakan async/await akan diubah oleh compiler menjadi sebuah state machine.

Apabila sebuah struct masih diperlukan setelah melewati await, nilainya akan menjadi bagian dari state machine tersebut. Karena state machine biasanya dialokasikan di heap, struct tersebut juga ikut berada di heap.

C#

public async Task Example()
{
PointStruct point = new() { X = 1, Y = 2 };
await Task.Delay(1000);
Console.WriteLine(point.X);
}

Contoh Kasus dan Benchmark

Sejauh ini kita telah membahas teori mengenai value type, reference type, stack, heap, dan Garbage Collector (GC). Sekarang saatnya melihat bagaimana perbedaan tersebut memengaruhi performa aplikasi melalui sebuah contoh sederhana.

Misalkan kita memiliki dua tipe data yang merepresentasikan sebuah titik (point), satu menggunakan struct dan satu lagi menggunakan class.

C#

public struct PointStruct
{
public int X;
public int Y;
}
public class PointClass
{
public int X;
public int Y;
}

Selanjutnya, kita membuat dua method yang melakukan pekerjaan yang sama, yaitu membuat sebuah titik pada setiap iterasi lalu menjumlahkan koordinat X dan Y.

C#

public static long SumWithStruct(int iterations)
{
long total = 0;
for (int i = 0; i < iterations; i++)
{
var point = new PointStruct
{
X = i,
Y = i
};
total += point.X + point.Y;
}
return total;
}
public static long SumWithClass(int iterations)
{
long total = 0;
for (int i = 0; i < iterations; i++)
{
var point = new PointClass
{
X = i,
Y = i
};
total += point.X + point.Y;
}
return total;
}

Sekilas kedua method tersebut terlihat hampir identik. Perbedaannya hanya terletak pada penggunaan PointStruct dan PointClass. Namun, cara CLR mengelola memorinya sangat berbeda.

Pada SumWithStruct, setiap PointStruct merupakan value type yang hanya digunakan di dalam iterasi tersebut. Nilainya tidak pernah dikembalikan, disimpan ke koleksi, maupun dikirim ke tempat lain. Dalam kondisi seperti ini, CLR dapat menyimpan nilainya sebagai variabel lokal tanpa membuat objek baru di heap. Akibatnya, tidak ada pekerjaan tambahan bagi Garbage Collector (GC).

Sebaliknya, pada SumWithClass, setiap pemanggilan new PointClass() membuat objek baru di heap. Jika proses ini dilakukan jutaan kali, maka akan terbentuk jutaan objek yang pada akhirnya harus dibersihkan oleh GC setelah tidak lagi digunakan.

Mengukur dengan BenchmarkDotNet

Daripada hanya mengandalkan teori, kita dapat mengukurnya secara langsung menggunakan BenchmarkDotNet, yaitu library standar untuk melakukan micro benchmark pada aplikasi .NET.

C#

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class PointBenchmark
{
private const int Iterations = 10_000_000;
[Benchmark(Baseline = true)]
public long UsingStruct() => SumWithStruct(Iterations);
[Benchmark]
public long UsingClass() => SumWithClass(Iterations);
}
public class Program
{
public static void Main()
{
BenchmarkRunner.Run<PointBenchmark>();
}
}

Attribute [MemoryDiagnoser] akan meminta BenchmarkDotNet untuk menampilkan informasi mengenai penggunaan memori, termasuk jumlah alokasi di heap dan aktivitas Garbage Collector.

Hasil benchmark dapat berbeda pada setiap komputer, tetapi polanya biasanya akan terlihat seperti berikut.

MethodMeanHeap Allocated
UsingStruct3.746 ms-
UsingClass19.155 ms240 MB

Perbedaan paling menarik ada pada kolom Heap Allocated.

  • UsingStruct tidak melakukan alokasi objek di heap sehingga penggunaan memorinya dapat bernilai 0 B.

  • UsingClass membuat objek baru pada setiap iterasi sehingga menghasilkan alokasi memori yang sangat besar.

Selain itu, laporan BenchmarkDotNet biasanya juga menampilkan informasi seperti Gen0, Gen1, dan Gen2, yang menunjukkan berapa kali Garbage Collector dijalankan selama benchmark berlangsung. Pada versi struct, angka tersebut biasanya bernilai 0 atau sangat kecil karena hampir tidak ada objek yang perlu dibersihkan. Sebaliknya, versi class akan memicu GC lebih sering akibat banyaknya objek yang dialokasikan di heap.

Catatan penting

Benchmark ini dibuat untuk memperlihatkan perbedaan biaya alokasi memori antara struct dan class, bukan untuk menyimpulkan bahwa struct selalu lebih cepat. Pada aplikasi nyata, performa dipengaruhi oleh banyak faktor, seperti ukuran data, pola akses, algoritma, cache CPU, optimasi JIT, serta apakah sebuah struct harus sering disalin. Oleh karena itu, pilihlah struct atau class berdasarkan karakteristik data yang dimodelkan, kemudian gunakan benchmark untuk memastikan keputusan tersebut memang memberikan manfaat pada kasus yang sedang dihadapi.

Dari benchmark sederhana ini, kita dapat melihat bahwa perbedaan antara struct dan class bukan hanya masalah sintaks. Cara keduanya dikelola di memori dapat memberikan dampak nyata terhadap penggunaan memori, aktivitas Garbage Collector, dan performa aplikasi, terutama pada kode yang berjalan dalam jumlah iterasi yang sangat besar.

Alat Bantu untuk Menganalisis Alokasi Memori di C#

Setelah memahami bagaimana stack, heap, dan Garbage Collector (GC) bekerja, langkah berikutnya adalah mengetahui bagaimana cara membuktikan apa yang sebenarnya terjadi ketika aplikasi dijalankan.

Untungnya, .NET menyediakan berbagai alat yang dapat digunakan untuk menganalisis penggunaan memori dan aktivitas GC. Beberapa di antaranya adalah sebagai berikut.

BenchmarkDotNet (MemoryDiagnoser)

Jika ingin membandingkan performa sekaligus melihat jumlah alokasi memori, BenchmarkDotNet merupakan pilihan yang paling direkomendasikan.

Dengan menambahkan atribut [MemoryDiagnoser], BenchmarkDotNet akan menampilkan informasi seperti:

  • Waktu eksekusi (execution time).

  • Jumlah memori yang dialokasikan (Allocated).

  • Aktivitas Garbage Collector, seperti Gen0, Gen1, dan Gen2.

Tool ini sangat cocok digunakan untuk melakukan micro benchmark dan membandingkan implementasi yang berbeda, misalnya penggunaan struct dan class.

Visual Studio Diagnostic Tools atau JetBrains dotMemory

Apabila ingin melihat kondisi heap secara langsung saat aplikasi berjalan, Anda dapat menggunakan:

  • Visual Studio Diagnostic Tools, yang telah tersedia di Visual Studio.

  • JetBrains dotMemory, profiler memori khusus untuk aplikasi .NET.

Dengan kedua alat tersebut, kita dapat melihat:

  • Objek apa saja yang masih berada di heap.

  • Berapa banyak instance dari setiap tipe.

  • Siapa yang masih mereferensikan sebuah objek.

  • Objek mana yang menyebabkan kebocoran memori (memory leak).

Tool seperti ini sangat membantu ketika melakukan investigasi terhadap penggunaan memori pada aplikasi nyata.

GC.GetAllocatedBytesForCurrentThread()

Untuk pengukuran sederhana tanpa menggunakan profiler atau BenchmarkDotNet, .NET menyediakan method:

C#

GC.GetAllocatedBytesForCurrentThread()

Method ini mengembalikan jumlah byte yang telah dialokasikan oleh thread saat ini sejak thread tersebut mulai berjalan.

Contohnya:

C#

long before = GC.GetAllocatedBytesForCurrentThread();
SumWithClass(1_000_000);
long after = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"Allocated: {after - before:N0} bytes");

Jika dijalankan menggunakan SumWithClass, biasanya akan terlihat adanya alokasi memori yang cukup besar karena setiap iterasi membuat objek baru di heap.

Sebaliknya, jika diganti menjadi:

C#

long before = GC.GetAllocatedBytesForCurrentThread();
SumWithStruct(1_000_000);
long after = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"Allocated: {after - before:N0} bytes");

hasilnya sering kali mendekati 0 byte, karena pada contoh tersebut PointStruct tidak menyebabkan alokasi objek di heap.

Catatan

GC.GetAllocatedBytesForCurrentThread() hanya menghitung alokasi memori, bukan penggunaan memori yang sedang aktif. Method ini juga hanya menghitung alokasi yang dilakukan oleh thread saat ini, sehingga hasilnya dapat berbeda apabila aplikasi menggunakan banyak thread.

Kapan Menggunakan Masing-Masing?

Secara umum:

ToolKegunaan
BenchmarkDotNetMembandingkan performa dan jumlah alokasi memori secara akurat.
Visual Studio Diagnostic Tools / dotMemoryMenganalisis objek yang berada di heap dan mencari penyebab penggunaan memori atau memory leak.
GC.GetAllocatedBytesForCurrentThread()Mengukur alokasi memori pada potongan kode tertentu dengan cepat tanpa profiler.

Ketiga alat tersebut saling melengkapi. Dalam praktiknya, BenchmarkDotNet biasanya digunakan untuk mengukur performa, Diagnostic Tools atau dotMemory digunakan untuk menganalisis isi heap, sedangkan GC.GetAllocatedBytesForCurrentThread() berguna ketika ingin melakukan pengukuran sederhana langsung dari dalam kode aplikasi.

Fitur C# yang Membantu Mengoptimalkan Penggunaan struct

Seiring berkembangnya C#, berbagai fitur baru ditambahkan untuk membuat penggunaan value type menjadi lebih efisien. Fitur-fitur ini membantu mengurangi penyalinan data yang tidak diperlukan, mencegah boxing, serta memastikan data tetap berada di stack pada kondisi tertentu.

readonly struct

Secara default, isi sebuah struct dapat diubah setelah dibuat.

C#

public struct Money
{
public decimal Amount;
public string Currency;
}

Namun, apabila sebuah struct memang dirancang agar nilainya tidak berubah (immutable), sebaiknya gunakan readonly struct.

C#

public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}

Dengan menandai sebuah struct sebagai readonly, compiler mengetahui bahwa seluruh anggotanya tidak akan berubah setelah objek dibuat. Informasi ini memungkinkan compiler melakukan optimasi, salah satunya menghindari defensive copy, yaitu penyalinan sementara yang terkadang perlu dilakukan untuk menjaga agar nilai asli tidak berubah.

Selain memberikan peluang optimasi, readonly struct juga membuat tujuan desain tipe tersebut menjadi lebih jelas: objeknya memang dimaksudkan untuk tidak dapat dimodifikasi.

record struct

Jika sebuah struct hanya digunakan untuk merepresentasikan sekumpulan nilai, C# menyediakan sintaks yang lebih ringkas melalui record struct.

C#

public readonly record struct Point(int X, int Y);

Penggunaannya pun sederhana.

C#

var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b); // True

Berbeda dengan struct biasa, record struct secara otomatis menyediakan:

  • Perbandingan berdasarkan nilai (value equality).

  • Implementasi Equals() dan GetHashCode().

  • ToString() yang lebih informatif.

  • Sintaks deklarasi yang lebih ringkas.

Fitur ini sangat cocok untuk membuat tipe data sederhana seperti koordinat, ukuran, atau objek transfer data (DTO) yang berbasis nilai.

ref struct

Pada beberapa kasus, kita ingin memastikan bahwa sebuah struct tidak pernah berpindah ke heap.

Untuk kebutuhan tersebut, C# menyediakan ref struct.

C#

public ref struct StackOnlyBuffer
{
public Span<byte> Data;
}

Contoh paling terkenal dari ref struct adalah Span<T>.

Karena dirancang agar selalu aman berada di stack, ref struct memiliki beberapa batasan. Sebagai contoh, ref struct:

  • Tidak dapat di-boxing.

  • Tidak dapat menjadi field dari sebuah class.

  • Tidak dapat ditangkap oleh closure.

  • Tidak dapat digunakan melewati await maupun yield.

Jika salah satu aturan tersebut dilanggar, compiler akan menghasilkan compile error. Dengan demikian, compiler dapat menjamin bahwa ref struct tidak berpindah ke heap.

Parameter in

Salah satu kekurangan struct adalah seluruh nilainya akan disalin ketika dikirim sebagai parameter.

Untuk struct yang berukuran cukup besar, penyalinan ini dapat menjadi biaya tambahan.

Sebagai solusinya, C# menyediakan parameter in.

C#

public static decimal CalculateTotal(in Money money, int quantity)
{
return money.Amount * quantity;
}

Parameter in meneruskan struct berdasarkan referensi (by reference), tetapi tetap bersifat read-only.

Artinya:

  • Tidak terjadi penyalinan seluruh isi struct.

  • Method tidak dapat mengubah isi struct tersebut.

Fitur ini sangat berguna ketika bekerja dengan struct yang berukuran cukup besar dan sering diteruskan ke berbagai method.

Ringkasan

Pada akhirnya, perbedaan antara value type dan reference type bukan hanya soal memilih menggunakan struct atau class, tetapi tentang memahami bagaimana data dikelola di memori. Pemahaman ini akan membantu kita menulis kode yang tidak hanya benar, tetapi juga lebih efisien dan mudah dipelihara.

Beban Garbage Collector (GC) muncul karena objek yang berada di heap harus terus dipantau dan dibersihkan ketika sudah tidak lagi digunakan. Semakin sedikit objek yang dialokasikan ke heap, semakin ringan pula pekerjaan GC. Dalam banyak kasus, hal ini dapat membantu menjaga penggunaan memori, mengurangi aktivitas GC, dan membuat waktu respons (latency) aplikasi lebih stabil.

Sebagai rangkuman, berikut beberapa prinsip yang dapat dijadikan pedoman:

  1. Pahami konsep dasarnya terlebih dahulu.
    Value type menyimpan nilainya secara langsung, sedangkan reference type menyimpan referensi yang menunjuk ke objek.

  2. Pilih struct atau class sesuai karakteristik datanya.
    Gunakan struct untuk data yang kecil, sederhana, dan merepresentasikan sebuah nilai. Gunakan class untuk objek yang memiliki identitas, sering dibagikan ke banyak bagian aplikasi, membutuhkan inheritance, atau berukuran besar.

  3. Pahami bahwa value type tidak selalu berada di stack.
    Sebuah struct dapat berada di heap ketika menjadi field dari class, mengalami boxing, menjadi bagian dari koleksi, ditangkap oleh closure, atau digunakan dalam state machine async.

  4. Waspadai boxing.
    Boxing menyebabkan sebuah value type dialokasikan ke heap. Meskipun sering kali tidak menjadi masalah, boxing yang terjadi berulang-ulang pada kode yang sering dieksekusi dapat menambah beban alokasi dan Garbage Collector.

  5. Gunakan Nullable<T> (T?) ketika value type perlu memiliki keadaan "tidak ada nilai".
    Nullable<T> tetap merupakan sebuah value type sehingga tidak mengubah karakteristik dasar dari tipe tersebut.

  6. Manfaatkan fitur-fitur modern C#.
    Fitur seperti readonly struct, record struct, ref struct, serta parameter in dapat membantu mengurangi penyalinan data, menghindari alokasi yang tidak perlu, dan meningkatkan efisiensi ketika digunakan pada kasus yang tepat.

  7. Lakukan pengukuran sebelum melakukan optimasi.
    Jangan mengasumsikan bahwa struct selalu lebih cepat daripada class. Gunakan alat seperti BenchmarkDotNet, Visual Studio Diagnostic Tools, atau dotMemory untuk memastikan bahwa perubahan yang dilakukan benar-benar memberikan manfaat.

Terakhir, perlu diingat bahwa optimasi bukanlah tujuan utama. Memilih struct hanya karena dianggap lebih cepat justru dapat membuat kode menjadi lebih sulit dipahami apabila tidak sesuai dengan karakteristik datanya. Dalam sebagian besar aplikasi bisnis, penggunaan class sudah lebih dari cukup.

Optimasi baru menjadi penting ketika terdapat kebutuhan nyata, misalnya aplikasi yang memproses data dalam jumlah sangat besar, memiliki kebutuhan latency yang rendah, atau berada pada bagian kode yang sangat sering dieksekusi (hot path). Pada kondisi seperti itulah pemahaman mengenai stack, heap, Garbage Collector (GC), value type, dan reference type akan memberikan manfaat yang paling terasa.

Dengan memahami konsep-konsep tersebut, Anda tidak hanya mengetahui bagaimana C# bekerja, tetapi juga mengapa bahasa ini menyediakan class dan struct, serta kapan masing-masing sebaiknya digunakan.

Daftar isi

Pendahuluan
Stack dan Heap Itu Apa?

Baca juga

Artikel sebelumnya

Artikel berikutnya

Dibuat pelan-pelan dengan rasa hangat dari Kuningan.

© 2026 Rifky Haekal Al-Fadillah