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 default batasan. |
where T : allows ref struct |
Anti-batasan ini menyatakan bahwa argumen jenis untuk T bisa menjadi ref struct jenis. Jenis atau metode generik harus mematuhi aturan keamanan ref untuk instans T apa pun karena mungkin berupa ref struct . |
Beberapa batasan saling eksklusif, dan beberapa batasan harus dalam urutan tertentu:
- Anda dapat menerapkan paling banyak salah satu batasan
struct
, ,class
class?
,notnull
, danunmanaged
. Jika Anda menyediakan salah satu batasan ini, batasan pertama harus ditentukan untuk parameter jenis tersebut. - Batasan kelas dasar (
where T : Base
atauwhere T : Base?
) tidak dapat dikombinasikan dengan batasanstruct
apa pun , ,class
,class?
,notnull
atauunmanaged
. - 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 batasanstruct
atauunmanaged
. Jika Anda menentukan batasannew()
, batasan tersebut harus menjadi batasan terakhir untuk parameter jenis tersebut. Anti-batasan, jika berlaku, dapat mengikuti batasannew()
. - Batasan
default
hanya dapat diterapkan pada mengambil alih atau implementasi antarmuka eksplisit. Ini tidak dapat dikombinasikan dengan batasanstruct
atauclass
. - Anti-batasan
allows ref struct
tidak dapat dikombinasikan dengan batasanclass
atauclass?
. - Anti-batasan
allows ref struct
harus mengikuti semua batasan untuk parameter jenis tersebut.
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 : notnull, Employee, IComparable<T>, new()
{
// ...
public void AddDefault()
{
T t = new T();
// ...
}
}
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
null
selalu 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 sebelumnya 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 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>
{
static abstract T operator +(T left, T right);
static abstract 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>
{
static abstract IAdditionSubtraction<T> operator +(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
static abstract 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.
Memungkinkan struct ref
Anti-batasan allows ref struct
menyatakan bahwa argumen jenis yang sesuai dapat berupa jenis ref struct
. Instans parameter jenis tersebut harus mematuhi aturan berikut:
- Ini tidak bisa dikotak.
- Ini berpartisipasi dalam aturan keselamatan ref.
- Instans tidak dapat digunakan di mana
ref struct
jenis tidak diizinkan, sepertistatic
bidang. - Instans dapat ditandai dengan pengubah
scoped
.
Klausa allows ref struct
tidak diwariskan. Dalam kode berikut:
class SomeClass<T, S>
where T : allows ref struct
where S : T
{
// etc
}
Argumen untuk S
tidak bisa menjadi ref struct
karena S
tidak memiliki allows ref struct
klausa.
Parameter jenis yang memiliki allows ref struct
klausa tidak dapat digunakan sebagai argumen jenis kecuali parameter jenis yang sesuai juga memiliki allows ref struct
klausa . Aturan ini ditunjukkan dalam contoh berikut:
public class Allow<T> where T : allows ref struct
{
}
public class Disallow<T>
{
}
public class Example<T> where T : allows ref struct
{
private Allow<T> fieldOne; // Allowed. T is allowed to be a ref struct
private Disallow<T> fieldTwo; // Error. T is not allowed to be a ref struct
}
Sampel sebelumnya menunjukkan bahwa argumen jenis yang mungkin merupakan ref struct
jenis tidak dapat digantikan untuk parameter jenis yang tidak dapat menjadi ref struct
jenis.