Batasan pada parameter jenis (Panduan Pemrograman C#)

Batasan menginformasikan pengompilasi tentang kemampuan yang harus dimiliki argumen jenis. Tanpa batasan, argumen jenis bisa berupa jenis apa pun. Pengompilasi hanya dapat mengasumsikan anggota System.Object, yang merupakan kelas dasar utama untuk semua jenis .NET. Untuk informasi selengkapnya, lihat Mengapa harus menggunakan batasan. Jika kode klien menggunakan jenis yang tidak memenuhi batasan, pengompilasi akan mengeluarkan kesalahan. Batasan ditentukan dengan menggunakan where kata kunci kontekstual. Tabel berikut ini mencantumkan berbagai jenis batasan:

Batasan Deskripsi
where T : struct Argumen jenis harus berupa tipe nilai yang tidak dapat diubah ke null, yang mencakup record struct jenis. Untuk informasi selengkapnya tentang jenis nilai yang dapat diubah ke null, lihat Jenis nilai yang dapat diubah ke null. Karena semua jenis nilai memiliki konstruktor tanpa parameter yang dapat diakses, baik dinyatakan atau implisit, struct batasan menyiratkan new() batasan dan tidak dapat dikombinasikan dengan new() batasan. Anda tidak dapat menggabungkan batasan struct dengan batasan unmanaged.
where T : class Argumen jenis harus berupa jenis referensi. Batasan ini juga berlaku untuk kelas, antarmuka, delegasi, atau jenis array. Dalam konteks nullable, T harus berupa jenis referensi yang tidak dapat diubah ke null.
where T : class? Argumen jenis harus berupa jenis referensi, baik yang dapat maupun tidak dapat diubah ke null. Batasan ini juga berlaku untuk kelas, antarmuka, delegasi, atau jenis array apa pun, termasuk rekaman.
where T : notnull Argumen jenis harus berupa jenis yang tidak dapat diubah ke null. Argumen dapat berupa tipe referensi yang tidak dapat diubah ke null atau tipe nilai yang tidak dapat diubah ke null.
where T : unmanaged Argumen jenis harus berupa jenis tak terkelola yang tidak dapat diubah ke null. Batasan unmanaged mengartikan batasan struct dan tidak dapat digabungkan dengan batasan struct atau new().
where T : new() Argumen jenis harus memiliki konstruktor tanpa parameter publik. Ketika digunakan bersamaan dengan batasan lain, batasan new() harus ditentukan terakhir. Batasan new() tidak dapat dikombinasikan dengan batasan struct dan unmanaged.
where T :<nama kelas dasar> Argumen jenis harus berupa atau berasal dari kelas dasar yang ditentukan. Dalam konteks nullable, T harus berupa jenis referensi yang tidak dapat diubah ke null yang berasal dari kelas dasar yang ditentukan.
where T :<nama kelas dasar>? Argumen jenis harus berupa atau berasal dari kelas dasar yang ditentukan. Dalam konteks nullable, T dapat berupa jenis nullable atau non-nullable yang berasal dari kelas dasar yang ditentukan.
where T :<nama antarmuka> Argumen jenis harus berupa atau mengimplementasikan antarmuka yang ditentukan. Beberapa batasan antarmuka dapat ditentukan. Antarmuka pembatas juga dapat bersifat generik. Dalam konteks nullable, T harus berupa jenis yang tidak dapat diubah ke null yang mengimplementasikan antarmuka yang ditentukan.
where T :<nama antarmuka>? Argumen jenis harus berupa atau mengimplementasikan antarmuka yang ditentukan. Beberapa batasan antarmuka dapat ditentukan. Antarmuka pembatas juga dapat bersifat generik. Dalam konteks nullable, T bisa berupa jenis referensi nullable, jenis referensi yang tidak dapat diubah ke null, atau jenis nilai. T tidak dapat berupa tipe nilai yang dapat diubah ke null.
where T : U Argumen jenis yang disediakan untuk T harus berupa atau berasal dari argumen yang disediakan untuk U. Dalam konteks nullable, jika U adalah jenis referensi yang tidak dapat diubah ke null, T harus merupakan jenis referensi yang tidak dapat diubah ke null. Jika U merupakan tipe referensi yang dapat diubah ke null, T dapat berupa nullable atau non-nullable.
where T : default Batasan ini menyelesaikan ambiguitas ketika Anda perlu menentukan parameter jenis yang tidak dibatasi saat Anda mengambil alih metode atau memberikan implementasi antarmuka eksplisit. Batasan default menyiratkan metode dasar tanpa batasan class atau struct. Untuk informasi selengkapnya, lihat proposal spesifikasi defaultbatasan.

Beberapa batasan saling eksklusif, dan beberapa batasan harus dalam urutan tertentu:

  • Anda dapat menerapkan paling banyak salah satu batasan struct, , classclass?, notnull, dan unmanaged . Jika Anda menyediakan salah satu batasan ini, batasan pertama harus ditentukan untuk parameter jenis tersebut.
  • Batasan kelas dasar, (where T : Base atau where T : Base?), tidak dapat dikombinasikan dengan batasan structapa pun , , class, class?, notnullatau unmanaged.
  • Anda dapat menerapkan paling banyak satu batasan kelas dasar, dalam salah satu bentuk. Jika Anda ingin mendukung jenis dasar yang dapat diubah ke null, gunakan Base?.
  • Anda tidak dapat memberi nama bentuk antarmuka yang tidak dapat diubah ke null dan null sebagai batasan.
  • Batasan new() tidak dapat digabungkan dengan batasan struct atau unmanaged. Jika Anda menentukan batasan new() , batasan tersebut harus menjadi batasan terakhir untuk parameter jenis tersebut.
  • Batasan default hanya dapat diterapkan pada mengambil alih atau implementasi antarmuka eksplisit. Ini tidak dapat dikombinasikan dengan batasan struct atau class .

Mengapa harus menggunakan batasan

Batasan menentukan kemampuan dan harapan parameter jenis. Menyatakan batasan tersebut berarti Anda dapat menggunakan operasi dan panggilan metode dari jenis pembatasan. Anda menerapkan batasan ke parameter jenis saat kelas atau metode generik Anda menggunakan operasi apa pun pada anggota generik di luar penugasan sederhana, yang mencakup memanggil metode apa pun yang tidak didukung oleh System.Object. Misalnya, batasan kelas dasar memberi tahu pengkompilasi bahwa hanya objek jenis ini atau berasal dari jenis ini yang dapat menggantikan argumen jenis tersebut. Setelah pengompilasi memiliki jaminan ini, ia dapat memungkinkan metode jenis tersebut dipanggil di kelas generik. Contoh kode berikut menunjukkan fungsionalitas yang dapat Anda tambahkan ke GenericList<T> kelas (dalam Pengantar Generik) dengan menerapkan batasan kelas dasar.

public class Employee
{
    public Employee(string name, int id) => (Name, ID) = (name, id);
    public string Name { get; set; }
    public int ID { get; set; }
}

public class GenericList<T> where T : Employee
{
    private class Node
    {
        public Node(T t) => (Next, Data) = (null, t);

        public Node? Next { get; set; }
        public T Data { get; set; }
    }

    private Node? head;

    public void AddHead(T t)
    {
        Node n = new Node(t) { Next = head };
        head = n;
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node? current = head;

        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    public T? FindFirstOccurrence(string s)
    {
        Node? current = head;
        T? t = null;

        while (current != null)
        {
            //The constraint enables access to the Name property.
            if (current.Data.Name == s)
            {
                t = current.Data;
                break;
            }
            else
            {
                current = current.Next;
            }
        }
        return t;
    }
}

Batasan memungkinkan kelas generik untuk menggunakan Employee.Name properti. Batasan menentukan bahwa semua item jenis T dijamin berupa Employee objek atau objek yang mewarisi dari Employee.

Beberapa batasan dapat diterapkan ke parameter jenis yang sama, dan batasan itu sendiri dapat berupa jenis generik, sebagai berikut:

class EmployeeList<T> where T : Employee, System.Collections.Generic.IList<T>, IDisposable, new()
{
    // ...
}

Saat menerapkan batasan where T : class , hindari == operator dan != pada parameter jenis karena operator ini menguji untuk identitas referensi saja, bukan untuk kesetaraan nilai. Perilaku ini terjadi bahkan jika operator ini kelebihan beban dalam jenis yang digunakan sebagai argumen. Kode berikut mengilustrasikan titik ini; output tersebut salah meskipun String kelas membebani == operator.

public static void OpEqualsTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s == t);
}

private static void TestStringEquality()
{
    string s1 = "target";
    System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
    string s2 = sb.ToString();
    OpEqualsTest<string>(s1, s2);
}

Pengompilasi hanya tahu bahwa T merupakan jenis referensi pada waktu kompilasi dan harus menggunakan operator default yang valid untuk semua jenis referensi. Jika Anda harus menguji kesetaraan nilai, terapkan where T : IEquatable<T> atau where T : IComparable<T> batasan dan terapkan antarmuka di kelas apa pun yang digunakan untuk membangun kelas generik.

Membatasi beberapa parameter

Anda dapat menerapkan batasan ke beberapa parameter, dan beberapa batasan ke satu parameter, seperti yang ditunjukkan dalam contoh berikut:

class Base { }
class Test<T, U>
    where U : struct
    where T : Base, new()
{ }

Parameter jenis tak terbatas

Parameter jenis yang tidak memiliki batasan, seperti T di kelas publik SampleClass<T>{}, disebut parameter jenis tak terbatas. Parameter jenis tak terbatas memiliki aturan berikut:

  • Operator != dan == tidak dapat digunakan karena tidak ada jaminan bahwa argumen jenis beton mendukung operator ini.
  • Mereka dapat dikonversi ke dan dari System.Object maupun secara eksplisit dikonversi ke jenis antarmuka apa pun.
  • Anda dapat membandingkannya dengan null. Jika parameter yang tidak terikat dibandingkan dengan , perbandingan nullselalu mengembalikan false jika argumen jenis adalah jenis nilai.

Parameter jenis sebagai batasan

Penggunaan parameter jenis generik sebagai batasan berguna ketika fungsi anggota dengan parameter jenisnya sendiri harus membatasi parameter tersebut ke parameter jenis dari jenis pemuat seperti yang ditunjukkan dalam contoh berikut:

public class List<T>
{
    public void Add<U>(List<U> items) where U : T {/*...*/}
}

Dalam contoh sebelumnya, T adalah batasan jenis dalam konteks Add metode, dan parameter jenis yang tidak terbatas dalam konteks List kelas.

Parameter jenis juga dapat digunakan sebagai batasan dalam definisi kelas generik. Parameter jenis harus dideklarasikan dalam tanda kurung sudut bersama dengan parameter jenis lainnya:

//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }

Kegunaan parameter jenis sebagai batasan dengan kelas generik terbatas karena pengompilasi tidak dapat mengasumsikan apa pun tentang parameter jenis kecuali bahwa ia berasal dari System.Object. Gunakan parameter jenis sebagai batasan pada kelas generik dalam skenario di mana Anda ingin menerapkan hubungan pewarisan antara dua parameter jenis.

Batasan notnull

Anda dapat menggunakan notnull batasan untuk menentukan bahwa argumen jenis harus merupakan tipe nilai yang tidak dapat diubah ke null atau tipe referensi yang tidak dapat diubah ke null. Tidak seperti batasan lainnya, jika argumen jenis melanggar notnull batasan, pengompilasi membuat peringatan alih-alih kesalahan.

Batasan notnull hanya memiliki efek saat digunakan dalam konteks yang dapat diubah ke null. Jika Anda menambahkan notnull batasan dalam konteks tidak jelas yang dapat diubah ke null, maka pengompilasi tidak menghasilkan peringatan atau kesalahan untuk pelanggaran batasan.

Batasan class

Batasan class dalam konteks nullable menentukan bahwa argumen jenis harus merupakan jenis referensi yang tidak dapat diubah ke null. Dalam konteks yang dapat diubah ke null, ketika argumen jenis adalah jenis referensi yang dapat diubah ke null, pengompilasi akan menghasilkan peringatan.

Batasan default

Penambahan jenis referensi yang dapat diubah ke null mempersulit penggunaan T? dalam jenis atau metode generik. T? dapat digunakan dengan batasan struct atau class , tetapi salah satunya harus ada. Ketika batasan class digunakan, T? mengacu pada jenis referensi yang dapat diubah ke null untuk T. T? dapat digunakan ketika tidak ada batasan yang diterapkan. Dalam hal ini, T? ditafsirkan sebagai T? untuk jenis nilai dan jenis referensi. Namun, jika T adalah instans dari Nullable<T>, maka T? akan sama denganT. Dengan kata lain, itu tidak menjadi T??.

Karena T? sekarang dapat digunakan tanpa batasan class atau struct, ambiguitas dapat muncul dalam mengambil alih atau mengimplementasikan antarmuka eksplisit. Dalam kedua kasus tersebut, pengambilalihan tidak meliputi batasan, tetapi mewarisinya dari kelas dasar. Ketika kelas dasar tidak menerapkan batasan class atau struct, kelas turunan perlu menentukan pengambilalihan yang berlaku untuk metode dasar tanpa batasan. Metode turunan menerapkan batasan default . Batasan default tersebut tidak mengklarifikasi baik batasan , , class, ataupun struct.

Batasan tidak terkelola

Anda dapat menggunakan unmanaged batasan untuk menentukan bahwa parameter jenis harus merupakan jenis tidak terkelola yang tidak dapat diubah ke null. Batasan unmanaged memungkinkan Anda menulis rutinitas yang dapat digunakan kembali untuk bekerja dengan jenis yang dapat dimanipulasi sebagai blok memori, seperti yang ditunjukkan dalam contoh berikut:

unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
    var size = sizeof(T);
    var result = new Byte[size];
    Byte* p = (byte*)&argument;
    for (var i = 0; i < size; i++)
        result[i] = *p++;
    return result;
}

Metode sebelumnya harus dikompilasi dalam unsafe konteks karena menggunakan sizeof operator pada jenis yang tidak diketahui sebagai jenis bawaan. Tanpa batasan unmanaged, maka operator sizeof tidak tersedia.

Batasan unmanaged mengartikan batasan struct dan tidak dapat digabungkan dengannya. Karena batasan struct menyiratkan batasannew(), maka batasan unmanaged juga tidak dapat dikombinasikan dengan batasan new().

Mendelegasikan batasan

Anda dapat menggunakan System.Delegate atau System.MulticastDelegate sebagai batasan kelas dasar. CLR selalu mengizinkan batasan ini, tetapi bahasa C# melarangnya. Batasan System.Delegate memungkinkan Anda menulis kode yang berfungsi dengan delegasi dengan cara yang aman. Kode berikut mendefinisikan metode ekstensi yang menggabungkan dua delegasi asalkan jenisnya sama:

public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
    where TDelegate : System.Delegate
    => Delegate.Combine(source, target) as TDelegate;

Anda dapat menggunakan metode di atas untuk menggabungkan delegasi dengan jenis yang sama:

Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");

var combined = first.TypeSafeCombine(second);
combined!();

Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);

Jika Anda membatalkan komentar baris terakhir, baris tersebut tidak akan dikompilasi. Baik first dan test merupakan jenis delegasi, tetapi keduanya berbeda.

Batasan enum

Anda juga dapat menentukan System.Enum jenis sebagai batasan kelas dasar. CLR selalu mengizinkan batasan ini, tetapi bahasa C# melarangnya. Generik yang menggunakan System.Enum menyediakan pemrograman aman untuk menyimpan hasil dari penggunaan metode statis pada System.Enum. Sampel berikut menemukan semua nilai yang valid untuk jenis enum, lalu menyusun kamus yang memetakan nilai tersebut ke representasi stringnya.

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    var values = Enum.GetValues(typeof(T));

    foreach (int item in values)
        result.Add(item, Enum.GetName(typeof(T), item)!);
    return result;
}

Enum.GetValues dan Enum.GetName menggunakan refleksi yang memiliki implikasi performa. Anda dapat memanggil EnumNamedValues untuk membangun koleksi yang di-cache dan digunakan kembali alih-alih mengulangi panggilan yang memerlukan refleksi.

Anda dapat menggunakannya seperti yang ditunjukkan dalam sampel berikut untuk membuat enum dan membuat kamus nilai beserta namanya:

enum Rainbow
{
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet
}
var map = EnumNamedValues<Rainbow>();

foreach (var pair in map)
    Console.WriteLine($"{pair.Key}:\t{pair.Value}");

Ketik argumen mengimplementasikan antarmuka yang dideklarasikan

Beberapa skenario mengharuskan argumen yang disediakan untuk parameter jenis mengimplementasikan antarmuka tersebut. Contohnya:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static T operator +(T left, T right);
    public abstract static T operator -(T left, T right);
}

Pola ini memungkinkan pengkompilasi C# untuk menentukan jenis yang berisi untuk operator yang kelebihan beban, atau metode atau static abstract apa punstatic virtual. Ini menyediakan sintaks sehingga operator penambahan dan pengurangan dapat ditentukan pada jenis yang berisi. Tanpa batasan ini, parameter dan argumen akan diperlukan untuk dideklarasikan sebagai antarmuka, bukan parameter jenis:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    public abstract static IAdditionSubtraction<T> operator -(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);
}

Sintaks sebelumnya akan mengharuskan pelaksana untuk menggunakan implementasi antarmuka eksplisit untuk metode tersebut. Memberikan batasan tambahan memungkinkan antarmuka untuk menentukan operator dalam hal parameter jenis. Jenis yang mengimplementasikan antarmuka dapat secara implisit mengimplementasikan metode antarmuka.

Lihat juga