Omezení parametrů typu (Průvodce programováním v C#)

Omezení informují kompilátor o schopnostech, které musí mít argument typu. Bez jakýchkoli omezení může být argument typu libovolný typ. Kompilátor může předpokládat pouze členy System.Object, což je konečná základní třída pro libovolný typ .NET. Další informace naleznete v tématu Proč používat omezení. Pokud kód klienta používá typ, který nevyhovuje omezení, kompilátor vydá chybu. Omezení jsou určena pomocí kontextového klíčového where slova. Následující tabulka uvádí různé typy omezení:

Omezení Popis
where T : struct Argument typu musí být nenulový typ hodnoty, který zahrnuje record struct typy. Informace o typech hodnot s možnou hodnotou null naleznete v tématu Typy hodnot s možnou hodnotou Null. Vzhledem k tomu, že všechny hodnotové typy mají implicitně nebo deklaratorně dostupný konstruktor bez parametrů, omezení struct implikuje omezení new() a nelze ho kombinovat s omezením new(). Nemůžete kombinovat omezení struct s omezením unmanaged.
where T : class Argument typu musí být referenčním typem. Toto omezení platí také pro libovolnou třídu, rozhraní, delegáta nebo typ pole. V kontextu s povolenými hodnotami null musí být T typem odkazu, který neumožňuje null.
where T : class? Argument typu musí být referenčním typem, buď s možnou hodnotou null, nebo nenulovou hodnotou. Toto omezení platí také pro všechny třídy, rozhraní, delegáty nebo typ pole, včetně záznamů.
where T : notnull Argument typu musí být nenulový. Argumentem může být nenulový odkazový typ nebo nenulový typ hodnoty.
where T : unmanaged Argument typu musí být nenulovatelný nespravovaný typ. Omezení unmanaged implikuje omezení struct a nelze ho zkombinovat s omezením struct nebo new().
where T : new() Argument typu musí mít veřejný konstruktor bez parametrů. Při použití společně s jinými omezeními new() musí být omezení zadáno jako poslední. Omezení new() nelze kombinovat s omezenímistruct.unmanaged
where T : <Název základní třídy> Argument typu musí být nebo musí být odvozen ze zadané základní třídy. V kontextu s hodnotou null musí být typ odkazu, který není nullable, odvozený od zadané základní třídy.
where T : <Název> základní třídy? Argument typu musí být nebo musí být odvozen ze zadané základní třídy. V kontextu s hodnotou null může být T buď nullable typ nebo non-nullable typ odvozený ze zadané základní třídy.
where T : <název rozhraní> Argument typu musí být nebo implementovat zadané rozhraní. Lze zadat více omezení rozhraní. Omezující rozhraní může být také obecné. V kontextu s možnou hodnotou null musí být nenulový typ, T který implementuje zadané rozhraní.
where T : <název> rozhraní? Argument typu musí být nebo implementovat zadané rozhraní. Lze zadat více omezení rozhraní. Omezující rozhraní může být také obecné. V kontextu nulovatelnosti může být T nulovatelný referenční typ, nenulovatelný referenční typ nebo datový typ hodnoty. T nemůže být nulovatelný typ hodnoty.
where T : U Argument typu zadaný pro T musí být nebo odvozen z argumentu zadaného pro U. V kontextu s možnou hodnotou null, pokud U je nenulový odkazový typ, T musí být nenulový odkazový typ. Pokud U je odkazový typ s možnou hodnotou null, T může být nullable nebo non-nullable.
where T : default Toto omezení vyřeší nejednoznačnost v případě, že při přepsání metody nebo poskytnutí explicitní implementace rozhraní potřebujete zadat parametr nekontrénovaného typu. Omezení default znamená základní metodu bez class omezení nebo struct omezení. Další informace najdete v default části omezení specifikace jazyka C#.
where T : allows ref struct Toto omezení deklaruje, že typový argument pro T může být typu ref struct. Obecný typ nebo metoda musí dodržovat pravidla bezpečnosti ref v případě jakékoliv instance T, protože může být ref struct.

Některá omezení se vzájemně vylučují a některá omezení musí být v zadaném pořadí:

  • Můžete použít maximálně jednu z structomezení , class, class?, notnulla unmanaged omezení. Pokud zadáte některá z těchto omezení, musí to být první omezení zadané pro tento parametr typu.
  • Omezení základní třídy (where T : Base nebo where T : Base?) nelze kombinovat s žádnou z omezení struct, , class, class?, notnullnebo unmanaged.
  • Omezení základní třídy můžete použít v nejvýše jedné podobě. Pokud chcete podporovat základní typ s možnou hodnotou null, použijte Base?.
  • Nelze pojmenovat nenulovou i nulovou formu rozhraní jako omezení.
  • Omezení new() nelze kombinovat s omezením struct ani s omezením unmanaged. Pokud zadáte new() omezení, musí to být poslední omezení pro tento parametr typu. Anti-omezení, pokud je to možné, může postupovat podle new() omezení.
  • Omezení default lze použít pouze u implementací přepsání nebo explicitního rozhraní. Nelze jej kombinovat ani s omezením struct, ani s omezením class.
  • Anti-constraint allows ref struct nelze kombinovat s omezením class nebo class?.
  • Anti-omezení allows ref struct musí dodržovat všechna omezení určená pro tento typový parametr.

Proč používat omezení

Omezení určují možnosti a očekávání parametru typu. Deklarace těchto omezení znamená, že můžete použít operace a volání metody typu omezení. Omezení použijete u parametru typu, pokud obecná třída nebo metoda používá jakoukoli operaci s obecnými členy nad rámec jednoduchého přiřazení, což zahrnuje volání všech metod, které nejsou podporovány System.Object. Omezení základní třídy například kompilátoru říká, že argument typu může nahradit pouze objekty tohoto typu nebo odvozené z tohoto typu. Jakmile má kompilátor tuto záruku, může povolit, aby metody tohoto typu byly volána v obecné třídě. Následující příklad kódu ukazuje funkce, které můžete přidat do GenericList<T> třídy (v úvodu do obecných typů) použitím omezení základní třídy.

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(t) { Next = head };
        head = n;
    }

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

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

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

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

Omezení umožňuje obecné třídě používat Employee.Name vlastnost. Omezení určuje, že je zaručeno, že všechny položky typu T budou buď jako Employee objekt, nebo jako objekt, který dědí z Employee.

U stejného parametru typu se dá použít více omezení a samotná omezení můžou být obecná, jak je znázorněno níže:

class EmployeeList<T> where T : notnull, Employee, IComparable<T>, new()
{
    public void AddDefault()
    {
        T t = new();
    }
}

Při použití omezení where T : class nepoužívejte operátory == a != na parametr typu, protože tyto operátory testují pouze referenční identitu, nikoli rovnost hodnot. K tomuto chování dochází i v případě, že jsou tyto operátory přetíženy v typu, který se používá jako argument. Následující kód ukazuje tento bod; výstup je false, i když String třída přetěžuje == operátor.

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

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

Kompilátor pouze ví, že T je referenčním typem v době kompilace a musí používat výchozí operátory platné pro všechny odkazové typy. Pokud je nutné otestovat rovnost hodnot, použijte where T : IEquatable<T> nebo where T : IComparable<T> omezení a implementujte rozhraní v libovolné třídě použité k vytvoření obecné třídy.

Omezení více parametrů

Omezení můžete použít na více parametrů a několik omezení na jeden parametr, jak je znázorněno v následujícím příkladu:

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

Parametry nevázaného typu

Parametry typu, které nemají žádná omezení, například T ve veřejné třídě SampleClass<T>{}, se nazývají nevázané parametry typu. Parametry nevázaného typu mají následující pravidla:

  • Operátory != a == operátory nelze použít, protože neexistuje žádná záruka, že argument konkrétního typu tyto operátory podporuje.
  • Mohou být převedeny na a z System.Object nebo explicitně převedeny na jakýkoli typ rozhraní.
  • Můžete je porovnat s hodnotou null. Pokud je null porovnáván nevázaný parametr, porovnání vždy vrátí hodnotu false, pokud je argumentem typu hodnotového typu.

Parametry typu jako omezení

Použití parametru obecného typu jako omezení je užitečné, když členová funkce s vlastním parametrem typu musí tento parametr omezit na parametr typu obsahujícího typu, jak je znázorněno v následujícím příkladu:

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

V předchozím příkladu T je omezení typu v kontextu Add metody a nevázaný parametr typu v kontextu List třídy.

Parametry typu lze také použít jako omezení v definicích obecných tříd. Parametr typu musí být deklarován v hranatých závorkách spolu s jinými parametry typu:

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

Užitečnost parametrů typu jako omezení s obecnými třídami je omezena, protože kompilátor nemůže předpokládat nic o parametru typu s tím rozdílem, že je odvozen z System.Object. Parametry typu použijte jako omezení obecných tříd ve scénářích, ve kterých chcete vynutit vztah dědičnosti mezi dvěma parametry typu.

notnull omezení

Pomocí notnull omezení můžete určit, že argument typu musí být neprázdný hodnotový typ nebo neprázdný referenční typ. Na rozdíl od většiny ostatních omezení, pokud argument typu porušuje notnull omezení, kompilátor místo chyby vygeneruje upozornění.

Omezení notnull má účinek pouze v případech, kdy se používá v kontextu s možnou hodnotou null. Pokud přidáte omezení notnull v kontextu ignorování nulovatelnosti, kompilátor nevygeneruje žádná upozornění ani chyby pro porušení omezení.

class omezení

Omezení class v kontextu s možnou hodnotou null určuje, že argument typu musí být nenulový odkazový typ. V nullovém kontextu, když je argument typu nullable reference type, kompilátor vygeneruje upozornění.

default omezení

Přidání odkazových typů s možnou hodnotou null komplikuje použití T? v obecném typu nebo metodě. T? lze použít buď s struct nebo class omezením, ale jedno z těchto omezení musí být přítomno. class Když bylo použití omezení, T? se odkazovalo na typ odkazu s možnou hodnotou null pro T. T? lze použít, pokud není použito žádné omezení. V takovém případě T? se interpretuje jako T? pro typy hodnot a odkazové typy. Pokud T je však instance Nullable<T>, T? je stejná jako T. Jinými slovy, nestane se to T??.

Vzhledem k tomu, že T? lze nyní použít bez omezení class nebo struct, může dojít k nejednoznačnostem v přepisování nebo explicitních implementacích rozhraní. V obou těchto případech přepsání nezahrnuje omezení, ale dědí je ze základní třídy. Pokud základní třída nepoužije ani class omezení, ani struct omezení, musí odvozené třídy nějakým způsobem určit, že přepisování se vztahuje na základní metodu i bez těchto omezení. Odvozená metoda použije default omezení. Omezení default neobjasňuje ani class ani struct omezení.

Nespravované omezení

Pomocí omezení unmanaged můžete určit, že parametr typu musí být nenulovatelným nespravovaným typem. Omezení unmanaged umožňuje psát opakovaně použitelné rutiny pro práci s typy, které lze manipulovat jako bloky paměti, jak je znázorněno v následujícím příkladu:

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;
}

Předchozí metoda musí být zkompilována v unsafe kontextu, protože používá sizeof operátor u typu, který není známý jako předdefinovaný typ. Bez unmanaged omezení není operátor sizeof k dispozici.

Omezení unmanaged znamená struct omezení a nedá se s ním kombinovat. struct Vzhledem k tomu, že omezení implikuje new() omezení, unmanaged omezení se nedá kombinovat s new() omezením.

Omezení delegace

Můžete použít System.Delegate nebo System.MulticastDelegate jako omezení základní třídy. Clr (Common Language Runtime) toto omezení vždy povolil, ale jazyk C# ho nepovolil. Toto System.Delegate omezení umožňuje psát kód, který funguje s delegáty bezpečným způsobem typu. Následující kód definuje rozšiřující metodu, která kombinuje dva delegáty za předpokladu, že jsou stejného typu:

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

Předchozí metodu můžete použít ke kombinování delegátů se stejným typem:

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);

Pokud odkomentujete poslední řádek, nekompiluje se. Oba first a test jsou typy delegátů, ale jsou to různé typy delegátů.

Omezení výčtu

Typ můžete také zadat System.Enum jako omezení základní třídy. CLR toto omezení vždy povolil, ale jazyk C# ho nepovolil. Generika využívající System.Enum poskytují typově bezpečné programování pro ukládání výsledků do mezipaměti pomocí statických metod v System.Enum. Následující ukázka najde všechny platné hodnoty pro výčtový typ a potom vytvoří slovník, který přiřazuje tyto hodnoty jejich řetězcové reprezentaci.

extension<T>(T) where T : System.Enum
{
    public static Dictionary<int, string> EnumNamedValues()
    {
        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 a Enum.GetName používají reflexi, která má vliv na výkon. Můžete volat EnumNamedValues pro sestavení kolekce, která je uložena v mezipaměti a znovu použita, namísto opakovaných volání vyžadujících reflexi.

Můžete ho použít, jak je znázorněno v následující ukázce, k vytvoření výčtu a vytvoření slovníku jeho hodnot a názvů:

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}");

Argumenty typu implementují deklarované rozhraní.

Některé scénáře vyžadují, aby argument zadaný pro parametr typu implementoval to rozhraní. Příklad:

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

Tento vzor umožňuje kompilátoru jazyka C# určit obsahující typ pro přetížené operátory nebo jakoukoli metodu static virtual nebo static abstract. Poskytuje syntaxi, aby mohly být operátory sčítání a odčítání definovány pro obsahující typ. Bez tohoto omezení by se parametry a argumenty musely deklarovat jako rozhraní, nikoli jako parametr typu:

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);
}

Předchozí syntaxe by vyžadovala, aby implementátory pro tyto metody používaly explicitní implementaci rozhraní. Poskytnutí dalšího omezení umožňuje rozhraní definovat operátory z hlediska parametrů typu. Typy, které implementují rozhraní, mohou implicitně implementovat metody rozhraní.

Umožňuje ref struct.

Antiomezení allows ref struct stanovuje, že odpovídající argument typu může být typu ref struct. Instance tohoto parametru typu musí dodržovat následující pravidla:

  • Nelze to zabalit do krabice.
  • Podílí se na bezpečnostních pravidlech ref.
  • Instance se nedají použít, pokud ref struct typ není povolený, například static pole.
  • Instance lze označit modifikátorem scoped .

Klauzule allows ref struct není zděděna. V následujícím kódu:

class SomeClass<T, S>
    where T : allows ref struct
    where S : T
{
    // etc
}

Argumentem nemůže být S nebo ref struct; S totiž neobsahuje klauzuli allows ref struct.

Parametr typu, který má allows ref struct klauzuli, nelze použít jako argument typu, pokud odpovídající parametr typu také klauzuli allows ref struct neobsahuje. Toto pravidlo je znázorněno v následujícím příkladu:

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
}

Předchozí ukázka ukazuje, že argument typu, který může být typem ref struct , nelze nahradit parametrem typu, který nemůže být typem ref struct .

Viz také