Foto oleh Ann H via Pexels

Collection generic di C# adalah struktur data yang bekerja dengan tipe data spesifik yang ditentukan saat deklarasi, memberikan keamanan tipe (type safety) sekaligus performa yang lebih baik dibanding collection non-generic lama seperti ArrayList yang menyimpan semuanya sebagai object.

Tiga collection generic yang paling sering dipakai adalah List untuk koleksi berurutan yang bisa bertambah, Dictionary untuk pasangan kunci-nilai dengan pencarian cepat, dan HashSet untuk koleksi nilai unik tanpa duplikat.

Artikel ini membahas karakteristik masing-masing collection ini, kapan sebaiknya dipakai, serta perbandingan kompleksitas operasi pencarian di antara ketiganya yang sering menjadi faktor penentu pemilihan struktur data yang tepat.

List<T> untuk Koleksi Berurutan

List mempertahankan urutan penambahan elemen

List<string> namaKota = new List<string> { "Jakarta", "Bandung", "Surabaya" }; namaKota.Add("Medan"); bool ada = namaKota.Contains("Bandung"); -- Expected: true namaKota.Remove("Surabaya");
List mempertahankan urutan elemen sesuai urutan penambahannya dan memungkinkan duplikat, menjadikannya pilihan default yang baik untuk sebagian besar kebutuhan penyimpanan koleksi data sederhana, meski operasi pencarian Contains pada List membutuhkan waktu linear karena harus memeriksa elemen satu per satu.

Dictionary untuk Pencarian Cepat Berdasarkan Kunci

Dictionary memungkinkan pencarian nilai berdasarkan kunci secara cepat

Dictionary<string, decimal> hargaProduk = new Dictionary<string, decimal> { { "Laptop", 8000000 }, { "Mouse", 150000 } }; if (hargaProduk.TryGetValue("Laptop", out decimal harga)) Console.WriteLine(harga); -- Expected: 8000000
Dictionary menyimpan data sebagai pasangan kunci-nilai dan memungkinkan pencarian nilai berdasarkan kunci dengan kompleksitas rata-rata O(1), jauh lebih cepat dibanding mencari elemen dalam List yang kompleksitasnya O(n). TryGetValue adalah cara yang aman untuk mencari nilai tanpa risiko exception jika kunci tidak ditemukan.

HashSet untuk Koleksi Unik Tanpa Duplikat

HashSet otomatis mencegah duplikat

HashSet<int> idUnik = new HashSet<int> { 1, 2, 3 }; bool berhasil = idUnik.Add(2); -- Expected: false, karena 2 sudah ada bool berhasil2 = idUnik.Add(4); -- Expected: true Console.WriteLine(idUnik.Count); -- Expected: 4
HashSet secara otomatis menolak elemen duplikat, method Add akan mengembalikan false jika elemen yang ditambahkan sudah ada sebelumnya, tanpa perlu memeriksa secara manual dengan Contains terlebih dahulu. HashSet juga menyediakan operasi himpunan seperti UnionWith, IntersectWith, dan ExceptWith yang berguna untuk operasi gabungan, irisan, dan selisih antar koleksi.

Memilih Collection yang Tepat Berdasarkan Kebutuhan

Kriteria sederhana memilih collection generic

// Butuh urutan dan boleh duplikat? -> List<T> // Butuh pencarian cepat berdasarkan kunci? -> Dictionary<TKey, TValue> // Butuh nilai unik tanpa duplikat dan tidak peduli urutan? -> HashSet<T>
Pemilihan collection yang tepat berdampak langsung pada performa aplikasi, terutama pada data berskala besar. Memakai List untuk kebutuhan yang sebenarnya butuh pencarian cepat berdasarkan kunci, misalnya, akan membuat aplikasi jauh lebih lambat dibanding jika memakai Dictionary yang memang dirancang untuk kebutuhan tersebut.

Selain tiga collection ini, namespace System.Collections.Generic juga menyediakan Queue untuk antrean FIFO (first-in-first-out) dan Stack untuk tumpukan LIFO (last-in-first-out), yang masing-masing cocok dipakai untuk kasus khusus seperti pemrosesan tugas berurutan atau fitur undo pada aplikasi editor teks.

Memilih Dictionary, HashSet, atau List Berdasarkan Kebutuhan Pencarian

Dictionary untuk pasangan key-value, HashSet untuk keunikan data

var produk = new Dictionary<string, decimal> { ["SKU-001"] = 15000, ["SKU-002"] = 32000 }; var kodeUnik = new HashSet<string> { "SKU-001", "SKU-002" }; Console.WriteLine(produk["SKU-001"]); // Output: 15000 Console.WriteLine(kodeUnik.Contains("SKU-002")); // Output: True Console.WriteLine(kodeUnik.Add("SKU-001")); // Output: False (sudah ada)

Dictionary dirancang untuk menyimpan pasangan key-value dengan pencarian berdasarkan key yang sangat cepat, dengan kompleksitas rata-rata O(1) berkat struktur hash table di belakangnya, jauh lebih efisien dibanding List yang harus memeriksa elemen satu per satu (O(n)) ketika mencari data berdasarkan suatu nilai tertentu menggunakan method seperti Find atau FirstOrDefault.

HashSet memiliki karakteristik performa pencarian yang serupa dengan Dictionary karena sama-sama berbasis hash table, namun hanya menyimpan nilai unik tanpa pasangan key-value; method Add pada HashSet akan mengembalikan false jika nilai yang ditambahkan sudah ada sebelumnya, seperti terlihat pada percobaan menambahkan SKU-001 kedua kalinya di atas, menjadikannya pilihan ideal untuk kasus yang membutuhkan validasi keunikan data secara efisien.

Sebagai panduan praktis, List paling cocok dipakai ketika urutan elemen penting dan duplikasi diperbolehkan, Dictionary paling cocok ketika data perlu diakses cepat berdasarkan suatu identifier unik seperti kode produk atau ID pelanggan, sedangkan HashSet paling cocok ketika yang dibutuhkan hanyalah memastikan tidak ada data duplikat tanpa peduli urutan penyimpanannya.

Queue dan Stack: Struktur Data FIFO dan LIFO Bawaan .NET

Queue mengikuti FIFO (First In First Out), Stack mengikuti LIFO (Last In First Out)

Queue<string> antrianLayanan = new Queue<string>(); antrianLayanan.Enqueue("Nasabah A"); antrianLayanan.Enqueue("Nasabah B"); Console.WriteLine(antrianLayanan.Dequeue()); // Output: Nasabah A Stack<string> riwayatHalaman = new Stack<string>(); riwayatHalaman.Push("Beranda"); riwayatHalaman.Push("Profil"); Console.WriteLine(riwayatHalaman.Pop()); // Output: Profil

Queue adalah struktur data yang mengikuti prinsip FIFO (First In First Out), di mana elemen yang pertama kali dimasukkan melalui method Enqueue akan menjadi elemen pertama yang dikeluarkan melalui method Dequeue, meniru perilaku antrian di dunia nyata seperti antrian layanan nasabah pada contoh di atas, sehingga nasabah yang datang lebih dulu akan dilayani lebih dulu pula ketika Dequeue dipanggil berulang kali.

Stack sebaliknya mengikuti prinsip LIFO (Last In First Out), di mana elemen yang paling terakhir dimasukkan melalui method Push akan menjadi elemen pertama yang dikeluarkan melalui method Pop, seperti tumpukan piring yang hanya bisa diambil dari bagian paling atas. Struktur ini sangat natural dipakai untuk merepresentasikan riwayat navigasi halaman seperti pada contoh di atas, di mana tombol back selalu kembali ke halaman yang paling terakhir dikunjungi.

Baik Queue maupun Stack menyediakan method Peek yang memungkinkan melihat elemen berikutnya yang akan dikeluarkan tanpa benar-benar menghapusnya dari koleksi, berguna ketika logika program perlu memeriksa elemen teratas sebelum memutuskan apakah akan memprosesnya atau tidak. Dari sisi penggunaan nyata, Queue sering dipakai pada sistem pemrosesan job secara berurutan, sedangkan Stack umum dipakai pada implementasi fitur undo atau algoritma rekursif yang disederhanakan menjadi iteratif. Baik Queue maupun Stack mengimplementasikan IEnumerable sehingga tetap dapat diiterasi menggunakan foreach atau diproses lebih lanjut dengan operator LINQ, meskipun urutan iterasinya tetap mengikuti aturan FIFO atau LIFO masing-masing struktur data tersebut.

Generic Constraint (where T : ...) untuk Membatasi Tipe Data

public class Repository<T> where T : class, IEntitas, new() { public T BuatBaru() { var entitas = new T(); Console.WriteLine($"Entitas baru dibuat: {entitas.Id}"); return entitas; } } public interface IEntitas { int Id { get; set; } } public class Pengguna : IEntitas { public int Id { get; set; } = 1; } var repo = new Repository<Pengguna>(); repo.BuatBaru(); // Output: // Entitas baru dibuat: 1

Generic constraint menggunakan kata kunci where membatasi tipe apa saja yang boleh digunakan sebagai parameter generic T pada class atau method, misalnya mewajibkan T berupa reference type (class), mengimplementasikan interface tertentu (IEntitas), serta memiliki constructor tanpa parameter (new()) agar bisa diinstansiasi langsung di dalam method generic tersebut tanpa menimbulkan error kompilasi. Beberapa constraint bisa digabungkan sekaligus dan urutan penulisannya mengikuti aturan tertentu, dengan constraint new() selalu diletakkan paling akhir.

Tanpa constraint, compiler hanya mengizinkan operasi yang berlaku untuk semua tipe secara umum, seperti ToString() atau Equals(), sehingga akses ke property seperti Id pada contoh di atas tidak akan dikenali sama sekali oleh compiler. Dengan menambahkan constraint yang tepat seperti pada Repository, kode generic tetap type-safe sekaligus bisa memanfaatkan anggota spesifik dari interface atau base class yang disyaratkan, menjadikan kode lebih fleksibel namun tetap aman dari kesalahan tipe saat kompilasi.

Kesalahan Umum yang Sering Terjadi

Memakai List untuk Pencarian Berulang Berdasarkan Kunci

Melakukan pencarian berulang kali dengan Contains atau Find pada List yang besar jauh lebih lambat dibanding mengonversinya menjadi Dictionary terlebih dahulu jika pencarian tersebut akan dilakukan berkali-kali dalam loop.

Mengakses Kunci Dictionary yang Tidak Ada Tanpa TryGetValue

Mengakses dictionary[kunciYangTidakAda] secara langsung tanpa memeriksa keberadaannya terlebih dahulu akan menghasilkan KeyNotFoundException, berbeda dengan TryGetValue yang aman dan mengembalikan false tanpa melempar exception.

Mengasumsikan HashSet Mempertahankan Urutan Penambahan

HashSet tidak menjamin urutan elemen sesuai urutan penambahannya karena strukturnya berbasis hashing, berbeda dengan List yang selalu mempertahankan urutan; mengasumsikan urutan tertentu pada HashSet bisa menghasilkan bug yang tidak konsisten.

Kapan Pendekatan Ini Tidak Cocok Dipakai

List memberikan kesederhanaan dan mempertahankan urutan data, sangat cocok untuk kebutuhan umum, namun performanya menurun untuk pencarian pada data besar dibanding Dictionary atau HashSet yang dioptimasi khusus untuk itu.

Dictionary dan HashSet memberikan performa pencarian yang jauh lebih baik, namun keduanya membutuhkan sedikit overhead memori tambahan untuk struktur hashing internal dibanding List yang lebih ringan untuk data kecil atau yang tidak membutuhkan pencarian cepat sama sekali.

Ringkasan

Memilih collection generic yang tepat, baik List untuk urutan sederhana, Dictionary untuk pencarian cepat berbasis kunci, atau HashSet untuk nilai unik, berdampak signifikan pada performa aplikasi terutama saat berurusan dengan data dalam jumlah besar. Pahami karakteristik masing-masing sebelum memutuskan struktur data mana yang dipakai dalam kode.

Seri Belajar C# Fundamental:

← Sebelumnya: Exception Handling Try Catch Finally

→ Selanjutnya: LINQ Dasar untuk Query Collection