Pada kode non-generik, int? punya satu makna: pembungkus Nullable<int>. Pada kode generik, compiler belum tahu apakah T adalah tipe nilai atau tipe referensi, sehingga makna T? diikat pada constraint yang menyertainya. Bab Generic Nullability di section Generics sudah memperkenalkan peta maknanya; bab ini menekankan mekanisme runtime dan method generik yang mengembalikan hasil mungkin-kosong.

Makna T? yang Terikat Constraint #

ConstraintMakna T?
where T : structSingkatan Nullable<T>, pembungkus struct sungguhan
where T : classNullable reference type, murni anotasi untuk compiler
Tanpa constraintT boleh berisi default(T) sejak C# 9

Ketiga bentuk mengumumkan hal yang sama, yaitu hasil bisa kosong, tetapi mekanismenya berbeda: satu bentuk menambah struct pembungkus, dua bentuk lainnya hanya anotasi.

Contoh: Method Generik dengan Hasil Mungkin Kosong #

csharp
T? CariPertama<T>(IEnumerable<T> sumber, Func<T, bool> syarat) where T : class
{
    foreach (T item in sumber)
    {
        if (syarat(item))
        {
            return item;
        }
    }

    return null;
}

List<string> produk = new List<string> { "Kopi Gayo", "Teh Melati", "Cokelat Lampung" };

string? hasilTeh = CariPertama(produk, p => p.StartsWith("Teh"));
string? hasilGula = CariPertama(produk, p => p.StartsWith("Gula"));

Console.WriteLine(hasilTeh ?? "Tidak ditemukan");
Console.WriteLine(hasilGula ?? "Tidak ditemukan");

Console.WriteLine($"int? sama dengan Nullable<int>: {typeof(int?) == typeof(Nullable<int>)}");
Console.WriteLine($"Underlying type: {Nullable.GetUnderlyingType(typeof(int?))!.Name}");
Console.WriteLine($"Nullable<double> adalah struct: {typeof(Nullable<double>).IsValueType}");

Output yang Dihasilkan:

text
Teh Melati
Tidak ditemukan
int? sama dengan Nullable<int>: True
Underlying type: Int32
Nullable<double> adalah struct: True

Mari kita bedah baris demi baris:

  1. T? CariPertama<T>(...) where T : class: Constraint class membuat T? berarti nullable reference type, sehingga return null sah tanpa warning.
  2. foreach (T item in sumber): Pencarian berhenti pada item pertama yang lolos syarat; bila tidak ada, method jatuh ke return null.
  3. string? hasilTeh: Type inference menetapkan T sebagai string, dan hasil method diwarisi sebagai string?.
  4. typeof(int?) == typeof(Nullable<int>): Perbandingan tipe membuktikan bahwa int? bukan tipe baru, melainkan nama lain Nullable<int>.
  5. Nullable.GetUnderlyingType(...): Method utilitas mengembalikan Int32, yaitu tipe nilai yang dibungkus.
  6. IsValueType pada Nullable<double>: Hasil True menegaskan pembungkus ini struct, sehingga ia disalin per nilai dan hidup di stack seperti value type lain.

Boxing Pembungkus Generik #

Perilaku boxing pada bab Nullable Value Types berlaku sama untuk semua Nullable<T>: pembungkus berisi nilai di-boxing sebagai T murni, dan pembungkus kosong di-boxing sebagai null reference. Karena itu API yang menerima object tidak bisa membedakan int biasa dengan int? yang terisi.