Generics membuatmu bisa menulis sebuah tipe atau method sekali lalu memakainya dengan banyak tipe, tanpa kehilangan keamanan tipe. Alih-alih List berisi object yang butuh cast dan menerima apa saja, kamu mendapat List<int> dan List<Order>, yang masing-masing diperiksa oleh compiler. Tipe pengganti di dalam kurung sudut, yang menurut kebiasaan bernama T, adalah parameter tipe; tipe yang diberikan pemanggil adalah argumen tipe.
Kenapa generics: masalah yang diselesaikannya
Sebelum ada generics (C# 1), koleksi menyimpan object. Kodenya bisa di-compile, tapi kesalahan baru ketahuan saat program berjalan, dan setiap int di-boxing saat dimasukkan:
var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50"); // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
total += (decimal)p; // InvalidCastException on the string
Dengan List<decimal>, Add yang kedua menjadi error saat compile, loop-nya tidak butuh cast, dan nilai decimal disimpan sebagai decimal.
Class generik
Deklarasikan parameter tipe setelah nama class, lalu pakai seperti tipe lainnya di dalam class:
Output:
Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False
Page<string> dan Page<int> adalah dua tipe berbeda yang dihasilkan dari satu definisi. Sebuah class bisa menerima beberapa parameter tipe (Dictionary<TKey, TValue>); beri nama T kalau hanya ada satu dan TSomething kalau ada beberapa.
Method generik dan inferensi tipe
Sebuah method bisa punya parameter tipenya sendiri, entah class-nya generik atau tidak. Pemanggil biasanya tidak menulis argumen tipe, karena compiler menyimpulkannya dari argumen:
Output:
2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR
Inferensi hanya bekerja dari argumen, tidak pernah dari tipe kembalian. Repeat(0, 3) tidak butuh anotasi, tapi method seperti T Create<T>() tidak punya apa pun untuk dijadikan dasar inferensi, jadi pemanggil harus menulis Create<Customer>(). Sebagian besar LINQ adalah method generik yang mengandalkan inferensi ini: prices.Where(p => p > 1) adalah Where<decimal> tanpa perlu kamu tulis.
Constraint: tipe apa yang boleh menjadi T
Di dalam method generik, compiler hanya mengizinkan hal yang bisa berjalan untuk setiap kemungkinan T. Tanpa constraint, itu tidak jauh lebih banyak dari yang didukung object. Untuk memanggil CompareTo, membuat instance, atau membaca sebuah property, beri tahu compiler harus berupa apa T itu, dengan where:
Output:
17
plum
19.99
42 Zoe
Constraint yang akan kamu pakai:
| Constraint | Artinya | Membuatmu bisa |
|---|---|---|
where T : class | reference type | memberikan null, memakai as |
where T : struct | value type yang tidak nullable | memakai T? sebagai Nullable<T> |
where T : new() | constructor publik tanpa parameter | memanggil new T() (harus ditulis paling akhir) |
where T : Entity | turunan dari Entity | memakai member milik Entity |
where T : IComparable<T> | mengimplementasikan interface tersebut | memanggil CompareTo |
where T : U | turunan dari parameter tipe lain | menghubungkan dua parameter |
where T : unmanaged (C# 7.3) | struct tanpa referensi di dalamnya | pointer, stackalloc |
where T : Enum (C# 7.3) | tipe enum | mengirimnya ke method Enum |
where T : notnull (C# 8) | tipe yang tidak nullable | anotasi nullable reference |
Beberapa constraint pada satu parameter dipisahkan dengan koma; beberapa parameter masing-masing mendapat klausa where sendiri: class Cache<TKey, TValue> where TKey : notnull where TValue : class.
Constraint yang terlewat muncul sebagai error saat compile di tempat kamu memakai member tersebut, misalnya CS1061 ('T' does not contain a definition for 'CompareTo') atau CS0304 saat memanggil new T() tanpa new().
default(T)
Kode generik kadang butuh "nilai kosong" dari tipe yang tidak diketahuinya. default(T) memberikan 0 untuk angka, false untuk bool, null untuk reference type dan nullable type, dan struct berisi nol untuk lainnya:
Output:
31
0
Porto
True
0001-01-01
Baris kedua menunjukkan kelemahan pola ini: untuk value type, "tidak ditemukan" dan "menemukan 0" terlihat sama. Itulah sebabnya method di framework memakai bentuk TryGet (bool TryGetValue(TKey key, out TValue value)). Sejak C# 7.1 kamu bisa menulis return default; yang lebih singkat saat tipenya sudah jelas dari konteks.
Member static berlaku per tipe yang dibentuk
Setiap tipe yang dibentuk (Counter<int>, Counter<string>) mendapat salinan field static class itu sendiri:
Output:
2
1
False
Hal ini kadang berguna (cache per tipe seperti TypeInfo<T>.Name yang dihitung sekali untuk setiap T) dan kadang mengejutkan saat kamu mengira ada satu penghitung bersama. Taruh state bersama di class yang tidak generik.
Koleksi generik
Koleksi generik mencukupi sebagian besar kebutuhan: List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>, beserta interface yang mereka miliki bersama, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T>. Terima interface paling sempit yang dibutuhkan sebuah method (IEnumerable<T> kalau method itu hanya melakukan perulangan), dan kembalikan tipe konkret atau tipe read-only.
Kovarians dan kontravarians
Dog adalah Animal, tapi List<Dog> bukan List<Animal>:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'
Seandainya itu diperbolehkan, animals.Add(new Cat()) akan memasukkan kucing ke list anjing. Interface yang hanya menghasilkan nilai aman dipakai, dan interface seperti itu dideklarasikan kovarian dengan out:
IEnumerable<Animal> readOnly = dogs; // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs; // fine: IReadOnlyList<out T>
Kebalikannya, kontravarians, yang ditandai dengan in, berlaku untuk interface dan delegate yang hanya memakai nilai: Action<Animal> bisa dipakai di tempat yang mengharapkan Action<Dog>, karena sesuatu yang bisa menangani hewan apa pun pasti bisa menangani seekor anjing. IComparer<in T> bersifat kontravarian, dan Func<in T, out TResult> memakai kedua penanda itu. Varians hanya berlaku untuk interface dan delegate, dan hanya untuk argumen berupa reference type: IEnumerable<int> tidak bisa dikonversi menjadi IEnumerable<object>.
Kesalahan yang sering terjadi
- Memakai
objectdi tempat yang seharusnya memakai parameter tipe. Cara ini membuang pemeriksaan saat compile dan melakukan boxing pada value type. - Membandingkan dua nilai
Tdengan==. Tanpa constraintclass, kodenya tidak bisa di-compile (CS0019), dan dengan constraint itu yang dibandingkan adalah referensinya. PakaiEqualityComparer<T>.Default.Equals(a, b), yang bekerja untuk setiapTdan menghormatiEqualsmilik tipe itu sendiri. - Mengembalikan
default(T)sebagai "tidak ditemukan". Hasilnya ambigu untuk value type. Pakaibool TryX(..., out T value). - Terlalu banyak constraint. Setiap constraint mempersempit siapa yang bisa memakai method tersebut. Batasi hanya apa yang benar-benar dibutuhkan badan method.
- Mengira
List<Derived>bisa dikonversi menjadiList<Base>. TerimaIEnumerable<Base>atauIReadOnlyList<Base>sebagai gantinya.
Pertanyaan yang sering diajukan
Apa itu generics di C#?
Generics membuatmu bisa menulis class, interface atau method sekali dengan tipe pengganti, T, yang diisi oleh pemanggilnya: List<int>, List<Customer>. Compiler memeriksa setiap pemakaian terhadap tipe yang sebenarnya, jadi kamu mendapat keamanan tipe tanpa cast, dan value type disimpan tanpa boxing. Koleksi di System.Collections.Generic adalah contoh yang paling dikenal.
Bagaimana cara menulis method generik di C#?
Taruh parameter tipe setelah nama method: static T Largest<T>(List<T> items) where T : IComparable<T> { ... }. Pemanggil biasanya tidak menulis argumen tipe karena compiler menyimpulkannya dari argumen: Largest(prices) bisa dipakai saat prices adalah List<decimal>. Tulis secara eksplisit, Largest<decimal>(...), saat tidak ada yang bisa dijadikan dasar inferensi.
Apa arti where T : class di C#?
Itu adalah constraint: T harus berupa reference type, jadi kode generik boleh memberikan null ke T dan memakai as T. Constraint lainnya adalah where T : struct (value type yang tidak nullable), where T : new() (punya constructor publik tanpa parameter, jadi kode bisa memanggil new T()), where T : SomeBaseClass, dan where T : ISomeInterface, yang membuat kode bisa memanggil member interface tersebut pada T.
Apa itu default(T) di C#?
Nilai default dari tipe apa pun yang ternyata menjadi T: 0 untuk angka, false untuk bool, null untuk reference type dan nullable type, serta struct berisi nol untuk value type lainnya. Kode generik memakainya saat butuh "tidak ada nilai" tanpa mengetahui tipenya, misalnya sebagai nilai kembalian dari pencarian yang gagal. C# 7.1 menambahkan literal default yang lebih singkat.
Kenapa saya tidak bisa memberikan List<Dog> ke List<Animal> di C#?
Karena List<Animal> menerima hewan apa saja: kalau pemberian itu diperbolehkan, kode bisa menambahkan Cat ke sesuatu yang sebenarnya adalah List<Dog>. Compiler menolaknya (CS0029). Interface yang hanya bisa dibaca bersifat kovarian, jadi IEnumerable<Animal> animals = dogs; dan IReadOnlyList<Animal> animals = dogs; sama-sama bisa di-compile, karena tidak ada yang bisa ditambahkan lewat interface itu.