Menu

Generics C#: Class Generik, Method Generik dan Constraint

Cara kerja generics di C#: menulis class dan method generik dengan parameter tipe, inferensi tipe, constraint dengan where (class, struct, new(), base class, interface), default(T), member static per tipe, dan kovarians dengan IEnumerable<out T>.

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

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:

ConstraintArtinyaMembuatmu bisa
where T : classreference typememberikan null, memakai as
where T : structvalue type yang tidak nullablememakai T? sebagai Nullable<T>
where T : new()constructor publik tanpa parametermemanggil new T() (harus ditulis paling akhir)
where T : Entityturunan dari Entitymemakai member milik Entity
where T : IComparable<T>mengimplementasikan interface tersebutmemanggil CompareTo
where T : Uturunan dari parameter tipe lainmenghubungkan dua parameter
where T : unmanaged (C# 7.3)struct tanpa referensi di dalamnyapointer, stackalloc
where T : Enum (C# 7.3)tipe enummengirimnya ke method Enum
where T : notnull (C# 8)tipe yang tidak nullableanotasi 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 object di tempat yang seharusnya memakai parameter tipe. Cara ini membuang pemeriksaan saat compile dan melakukan boxing pada value type.
  • Membandingkan dua nilai T dengan ==. Tanpa constraint class, kodenya tidak bisa di-compile (CS0019), dan dengan constraint itu yang dibandingkan adalah referensinya. Pakai EqualityComparer<T>.Default.Equals(a, b), yang bekerja untuk setiap T dan menghormati Equals milik tipe itu sendiri.
  • Mengembalikan default(T) sebagai "tidak ditemukan". Hasilnya ambigu untuk value type. Pakai bool 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 menjadi List<Base>. Terima IEnumerable<Base> atau IReadOnlyList<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.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai