Beperkingen voor typeparameters (C#-programmeerhandleiding)

Beperkingen informeren de compiler over de mogelijkheden die een typeargument moet hebben. Zonder beperkingen kan het typeargument elk type zijn. De compiler kan alleen uitgaan van de leden van System.Object, wat de ultieme basisklasse is voor elk .NET-type. Zie Waarom beperkingen gebruiken voor meer informatie. Als clientcode een type gebruikt dat niet voldoet aan een beperking, geeft de compiler een fout. Beperkingen worden opgegeven met behulp van het where contextuele trefwoord. De volgende tabel bevat de verschillende typen beperkingen:

Beperking Beschrijving
where T : struct Het typeargument moet een niet-null-waardetype zijn, dat typen bevatrecord struct. Zie Null-waardetypen voor informatie over typen null-waarden. Omdat alle waardetypen een toegankelijke parameterloze constructor hebben, gedeclareerd of impliciet, impliceert de struct beperking de new() beperking en kan deze niet worden gecombineerd met de new() beperking. U kunt de struct beperking niet combineren met de unmanaged beperking.
where T : class Het typeargument moet een verwijzingstype zijn. Deze beperking is ook van toepassing op elke klasse, interface, gemachtigde of matrixtype. In een null-context T moet een niet-null-verwijzingstype zijn.
where T : class? Het typeargument moet een verwijzingstype zijn, nullable of niet-nullable. Deze beperking is ook van toepassing op elke klasse, interface, gemachtigde of matrixtype, inclusief records.
where T : notnull Het typeargument moet een niet-null-type zijn. Het argument kan een niet-null-verwijzingstype of een niet-null-waardetype zijn.
where T : unmanaged Het typeargument moet een niet-nullable onbeheerd type zijn. De unmanaged beperking impliceert de struct beperking en kan niet worden gecombineerd met de struct of new() beperkingen.
where T : new() Het typeargument moet een openbare constructor zonder parameter hebben. Wanneer de beperking samen met andere beperkingen wordt gebruikt, moet de new() beperking het laatst worden opgegeven. De new() beperking kan niet worden gecombineerd met de struct en unmanaged beperkingen.
where T :<basisklassenaam> Het typeargument moet zijn of zijn afgeleid van de opgegeven basisklasse. In een null-context T moet een niet-null-referentietype zijn dat is afgeleid van de opgegeven basisklasse.
where T :<basisklassenaam>? Het typeargument moet zijn of zijn afgeleid van de opgegeven basisklasse. In een context met null-waarden T kan een niet-null-type zijn dat is afgeleid van de opgegeven basisklasse.
where T :<interfacenaam> Het typeargument moet de opgegeven interface zijn of implementeren. Er kunnen meerdere interfacebeperkingen worden opgegeven. De beperkingsinterface kan ook algemeen zijn. In een null-context T moet een niet-null-type zijn dat de opgegeven interface implementeert.
where T :<interfacenaam>? Het typeargument moet de opgegeven interface zijn of implementeren. Er kunnen meerdere interfacebeperkingen worden opgegeven. De beperkingsinterface kan ook algemeen zijn. In een context die null kan worden gebruikt, T kan dit een null-verwijzingstype, een niet-null-verwijzingstype of een waardetype zijn. T kan geen type null-waarde zijn.
where T : U Het opgegeven T typeargument moet zijn of zijn afgeleid van het opgegeven Uargument. Als het een niet-null-verwijzingstype is, T moet in een null-context U een niet-null-verwijzingstype zijn. Als U een null-verwijzingstype is, T kan dit nullable of niet-nullable zijn.
where T : default Deze beperking lost de dubbelzinnigheid op wanneer u een niet-gekoppelde typeparameter moet opgeven wanneer u een methode overschrijft of een expliciete interface-implementatie opgeeft. De default beperking impliceert de basismethode zonder de class of struct beperking. Zie het voorstel voor de default beperkingsspecificatie voor meer informatie.

Sommige beperkingen sluiten elkaar wederzijds uit en sommige beperkingen moeten zich in een opgegeven volgorde bevinden:

  • U kunt maximaal een van de structbeperkingen en notnullclassclass?unmanaged beperkingen toepassen. Als u een van deze beperkingen opgeeft, moet dit de eerste beperking zijn die is opgegeven voor die typeparameter.
  • De basisklassebeperking (where T : Base of ), kan niet worden gecombineerd met een van de beperkingen struct, class, class?, of unmanagednotnull.where T : Base?
  • U kunt maximaal één basisklassebeperking toepassen, in beide vormen. Als u het basistype nullable wilt ondersteunen, gebruikt u Base?.
  • U kunt de niet-null-bare en null-vorm van een interface niet als een beperking noemen.
  • De new() beperking kan niet worden gecombineerd met de struct of unmanaged beperking. Als u de new() beperking opgeeft, moet deze de laatste beperking voor die typeparameter zijn.
  • De default beperking kan alleen worden toegepast op onderdrukkings- of expliciete interface-implementaties. Het kan niet worden gecombineerd met de struct of class beperkingen.

Waarom beperkingen gebruiken

Beperkingen geven de mogelijkheden en verwachtingen van een typeparameter op. Als u deze beperkingen declareren, kunt u de bewerkingen en methodeaanroepen van het beperkingstype gebruiken. U past beperkingen toe op de typeparameter wanneer uw algemene klasse of methode elke bewerking op de algemene leden gebruikt, behalve eenvoudige toewijzing, waaronder het aanroepen van methoden die niet worden ondersteund door System.Object. De basisklassebeperking vertelt de compiler bijvoorbeeld dat alleen objecten van dit type of afgeleid van dit type dat type kunnen vervangen door dat typeargument. Zodra de compiler deze garantie heeft, kunnen methoden van dat type worden aangeroepen in de algemene klasse. In het volgende codevoorbeeld ziet u de functionaliteit die u kunt toevoegen aan de GenericList<T> klasse (in Introduction to Generics) door een beperking van de basisklasse toe te passen.

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

Met de beperking kan de algemene klasse de Employee.Name eigenschap gebruiken. De beperking geeft aan dat alle items van het type T gegarandeerd een Employee object of een object zijn dat overkomt van Employee.

Er kunnen meerdere beperkingen worden toegepast op dezelfde typeparameter en de beperkingen zelf kunnen algemene typen zijn, als volgt:

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

Vermijd bij het toepassen van de where T : class beperking de == operatoren op != de typeparameter, omdat deze operators alleen testen op referentie-identiteit, niet voor gelijkheid van waarden. Dit gedrag treedt zelfs op als deze operators overbelast zijn in een type dat als argument wordt gebruikt. De volgende code illustreert dit punt; de uitvoer is onwaar, ook al wordt de operator overbelast door de String== klasse.

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

De compiler weet alleen dat T dit een referentietype is tijdens het compileren en moet de standaardoperators gebruiken die geldig zijn voor alle referentietypen. Als u moet testen op gelijkheid van waarden, past u de where T : IEquatable<T> of where T : IComparable<T> beperking toe en implementeert u de interface in een klasse die wordt gebruikt om de algemene klasse te maken.

Meerdere parameters beperken

U kunt beperkingen toepassen op meerdere parameters en meerdere beperkingen op één parameter, zoals wordt weergegeven in het volgende voorbeeld:

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

Niet-afhankelijke typeparameters

Typeparameters zonder beperkingen, zoals T in openbare klasse SampleClass<T>{}, worden niet-afhankelijke typeparameters genoemd. Niet-afhankelijke typeparameters hebben de volgende regels:

  • De != operators en == operators kunnen niet worden gebruikt omdat er geen garantie is dat het argument voor het concrete type deze operators ondersteunt.
  • Ze kunnen worden geconverteerd naar en van System.Object of expliciet worden geconverteerd naar elk interfacetype.
  • U kunt ze vergelijken met null. Als een niet-afhankelijke parameter wordt vergeleken, nullretourneert de vergelijking altijd onwaar als het typeargument een waardetype is.

Parameters als beperkingen typen

Het gebruik van een algemene typeparameter als een beperking is handig wanneer een lidfunctie met een eigen typeparameter die parameter moet beperken tot de typeparameter van het type dat het type bevat, zoals wordt weergegeven in het volgende voorbeeld:

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

In het vorige voorbeeld T is een typebeperking in de context van de Add methode en een niet-afhankelijke typeparameter in de context van de List klasse.

Typeparameters kunnen ook worden gebruikt als beperkingen in algemene klassedefinities. De typeparameter moet worden gedeclareerd binnen de punthaken samen met andere typeparameters:

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

De bruikbaarheid van typeparameters als beperkingen met algemene klassen is beperkt omdat de compiler niets kan aannemen over de typeparameter, behalve dat deze is afgeleid van System.Object. Gebruik typeparameters als beperkingen voor algemene klassen in scenario's waarin u een overnamerelatie tussen twee typeparameters wilt afdwingen.

notnull Beperking

U kunt de notnull beperking gebruiken om op te geven dat het typeargument een niet-nullable waardetype of niet-nullable verwijzingstype moet zijn. In tegenstelling tot de meeste andere beperkingen, als een typeargument de notnull beperking schendt, genereert de compiler een waarschuwing in plaats van een fout.

De notnull beperking heeft alleen effect wanneer deze wordt gebruikt in een null-context. Als u de notnull beperking toevoegt in een niet-bruikbare context, genereert de compiler geen waarschuwingen of fouten voor schendingen van de beperking.

class Beperking

De class beperking in een null-context geeft aan dat het typeargument een niet-null-verwijzingstype moet zijn. Wanneer een typeargument een null-referentietype is in een null-context, genereert de compiler een waarschuwing.

default Beperking

De toevoeging van null-referentietypen maakt het gebruik van T? een algemeen type of methode ingewikkeld. T? kan worden gebruikt met de struct of class beperking, maar een van deze moet aanwezig zijn. Wanneer de class beperking is gebruikt, T? wordt verwezen naar het null-referentietype voor T. T? kan worden gebruikt wanneer geen van beide beperkingen wordt toegepast. In dat geval T? wordt dit geïnterpreteerd als T? voor waardetypen en verwijzingstypen. T Als het echter een exemplaar is van Nullable<T>, T? is hetzelfde als T. Met andere woorden, het wordt T??niet.

Omdat T? dit nu kan worden gebruikt zonder de class of struct beperking, kunnen er dubbelzinnigheden ontstaan in onderdrukkingen of expliciete interface-implementaties. In beide gevallen bevat de onderdrukking niet de beperkingen, maar neemt deze over van de basisklasse. Wanneer de basisklasse niet van toepassing is op de class of struct beperking, moeten afgeleide klassen op een of andere manier een onderdrukking opgeven die van toepassing is op de basismethode zonder een van beide beperkingen. De afgeleide methode past de default beperking toe. De default beperking verduidelijkt noch de class noch struct de beperking.

Niet-beheerde beperking

U kunt de unmanaged beperking gebruiken om op te geven dat de typeparameter een niet-nullable onbeheerd type moet zijn. Met de unmanaged beperking kunt u herbruikbare routines schrijven om te werken met typen die kunnen worden bewerkt als blokken geheugen, zoals wordt weergegeven in het volgende voorbeeld:

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

De voorgaande methode moet in een unsafe context worden gecompileerd omdat de sizeof operator wordt gebruikt voor een type dat niet bekend is als ingebouwd type. Zonder de unmanaged beperking is de sizeof operator niet beschikbaar.

De unmanaged beperking impliceert de struct beperking en kan er niet mee worden gecombineerd. Omdat de struct beperking de new() beperking impliceert, kan de unmanaged beperking niet ook worden gecombineerd met de new() beperking.

Beperkingen voor gedelegeerden

U kunt of System.MulticastDelegate als basisklassebeperking gebruikenSystem.Delegate. De CLR heeft deze beperking altijd toegestaan, maar de C#-taal heeft deze niet toegestaan. System.Delegate Met de beperking kunt u code schrijven die werkt met gemachtigden op een typeveilige manier. De volgende code definieert een extensiemethode die twee gemachtigden combineert op voorwaarde dat ze hetzelfde type zijn:

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

U kunt de bovenstaande methode gebruiken om gemachtigden te combineren die hetzelfde type zijn:

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

Als u de opmerkingen bij de laatste regel ongedaan maakt, wordt deze niet gecompileerd. Beide first en test zijn gedelegeerdentypen, maar ze zijn verschillende typen gemachtigden.

Opsommingsbeperkingen

U kunt ook het System.Enum type opgeven als basisklassebeperking. De CLR heeft deze beperking altijd toegestaan, maar de C#-taal heeft deze niet toegestaan. Generics die gebruikmaken van System.Enum typeveilig programmeren om resultaten van het gebruik van statische methoden in System.Enumde cache op te cachen. In het volgende voorbeeld worden alle geldige waarden voor een enumtype gevonden en wordt vervolgens een woordenlijst gemaakt waarmee deze waarden worden toegewezen aan de tekenreeksweergave.

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 en Enum.GetName gebruik weerspiegeling, wat gevolgen heeft voor de prestaties. U kunt aanroepen EnumNamedValues om een verzameling te maken die in de cache is opgeslagen en opnieuw wordt gebruikt in plaats van de aanroepen te herhalen waarvoor weerspiegeling is vereist.

U kunt deze gebruiken zoals wordt weergegeven in het volgende voorbeeld om een opsomming te maken en een woordenlijst met de bijbehorende waarden en namen te maken:

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

Typeargumenten implementeren gedeclareerde interface

Voor sommige scenario's is vereist dat een argument dat wordt opgegeven voor een typeparameter die interface implementeert. Voorbeeld:

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

Met dit patroon kan de C#-compiler het type bevatten voor de overbelaste operators of een of static abstract andere static virtual methode bepalen. De syntaxis biedt de syntaxis, zodat de operatoren voor optellen en aftrekken kunnen worden gedefinieerd voor een metingtype. Zonder deze beperking moeten de parameters en argumenten worden gedeclareerd als de interface in plaats van de typeparameter:

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

Voor de voorgaande syntaxis moeten implementeerfuncties expliciete interface-implementatie voor deze methoden gebruiken. Als u de extra beperking opgeeft, kan de interface de operators definiëren in termen van de typeparameters. Typen die de interface implementeren, kunnen de interfacemethoden impliciet implementeren.

Zie ook