Restrições em parâmetros de tipo (Guia de Programação em C#)

As restrições informam o compilador sobre os recursos que um argumento type deve ter. Sem quaisquer restrições, o argumento type pode ser de qualquer tipo. O compilador só pode assumir os membros do , que é a classe base definitiva para qualquer tipo . System.ObjectNET. Para obter mais informações, consulte Por que usar restrições. Se o código do cliente usa um tipo que não satisfaz uma restrição, o compilador emite um erro. As restrições são especificadas usando a where palavra-chave contextual. A tabela a seguir lista os vários tipos de restrições:

Restrição Description
where T : struct O argumento type deve ser um tipo de valor não anulável, que inclui record struct types. Para obter informações sobre tipos de valor anuláveis, consulte Tipos de valor anuláveis. Como todos os tipos de valor têm um construtor sem parâmetros acessível, declarado ou implícito, a struct restrição implica a new() restrição e não pode ser combinada com a new() restrição. Não é possível combinar a struct restrição com a unmanaged restrição.
where T : class O argumento type deve ser um tipo de referência. Essa restrição também se aplica a qualquer classe, interface, delegado ou tipo de matriz. Em um contexto anulável, T deve ser um tipo de referência não anulável.
where T : class? O argumento type deve ser um tipo de referência, anulável ou não anulável. Essa restrição também se aplica a qualquer classe, interface, delegado ou tipo de matriz, incluindo registros.
where T : notnull O argumento type deve ser um tipo não anulável. O argumento pode ser um tipo de referência não anulável ou um tipo de valor não anulável.
where T : unmanaged O argumento type deve ser um tipo não gerenciado não anulável. A unmanaged restrição implica a struct restrição e não pode ser combinada com as struct restrições ou new() .
where T : new() O argumento type deve ter um construtor sem parâmetros público. Quando usada em conjunto com outras restrições, a new() restrição deve ser especificada em último lugar. A new() restrição não pode ser combinada com as struct restrições e unmanaged .
where T :<Nome da classe base> O argumento type deve ser ou derivar da classe base especificada. Em um contexto anulável, T deve ser um tipo de referência não anulável derivado da classe base especificada.
where T :<Nome da> classe base? O argumento type deve ser ou derivar da classe base especificada. Em um contexto anulável, T pode ser um tipo anulável ou não anulável derivado da classe base especificada.
where T :<Nome da interface> O argumento type deve ser ou implementar a interface especificada. Várias restrições de interface podem ser especificadas. A interface restritiva também pode ser genérica. Em um contexto anulável, T deve ser um tipo não anulável que implementa a interface especificada.
where T :<Nome da> interface? O argumento type deve ser ou implementar a interface especificada. Várias restrições de interface podem ser especificadas. A interface restritiva também pode ser genérica. Em um contexto anulável, T pode ser um tipo de referência anulável, um tipo de referência não anulável ou um tipo de valor. T não pode ser um tipo de valor anulável.
where T : U O argumento type fornecido para T deve ser ou derivar do argumento fornecido para U. Em um contexto anulável, se U for um tipo de referência não anulável, T deve ser um tipo de referência não anulável. Se U for um tipo de referência anulável, T pode ser anulável ou não anulável.
where T : default Essa restrição resolve a ambiguidade quando você precisa especificar um parâmetro de tipo sem restrições quando substitui um método ou fornece uma implementação de interface explícita. A default restrição implica o método base sem a class restrição ou struct . Para obter mais informações, consulte a proposta de especificação de default restrição .

Algumas restrições são mutuamente exclusivas, e algumas restrições devem estar em uma ordem especificada:

  • Você pode aplicar no máximo uma das structrestrições , class, class?, notnull, e unmanaged . Se você fornecer qualquer uma dessas restrições, ela deverá ser a primeira restrição especificada para esse parâmetro de tipo.
  • A restrição de classe base, (where T : Base ou where T : Base?), não pode ser combinada com nenhuma das restrições struct, class, , class?notnull, ou unmanaged.
  • Você pode aplicar no máximo uma restrição de classe base, em qualquer um dos formulários. Se você quiser dar suporte ao tipo base anulável, use Base?.
  • Não é possível nomear a forma não anulável e anulável de uma interface como uma restrição.
  • A new() restrição não pode ser combinada com a struct restrição ou unmanaged . Se você especificar a new() restrição, ela deverá ser a última restrição para esse parâmetro type.
  • A default restrição pode ser aplicada somente em implementações de interface explícitas ou de substituição. Não pode ser combinado com as struct restrições ou class .

Porquê utilizar restrições

As restrições especificam os recursos e as expectativas de um parâmetro de tipo. Declarar essas restrições significa que você pode usar as operações e chamadas de método do tipo de restrição. Você aplica restrições ao parâmetro type quando sua classe ou método genérico usa qualquer operação nos membros genéricos além da atribuição simples, o que inclui chamar quaisquer métodos não suportados pelo System.Object. Por exemplo, a restrição de classe base informa ao compilador que somente objetos desse tipo ou derivados desse tipo podem substituir esse argumento de tipo. Uma vez que o compilador tem essa garantia, ele pode permitir que métodos desse tipo sejam chamados na classe genérica. O exemplo de código a seguir demonstra a funcionalidade que você pode adicionar à GenericList<T> classe (em Introduction to Generics) aplicando uma restrição de classe base.

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

A restrição permite que a classe genérica use a Employee.Name propriedade. A restrição especifica que todos os itens do tipo T têm a garantia de ser um Employee objeto ou um objeto herdado de Employee.

Várias restrições podem ser aplicadas ao mesmo parâmetro de tipo, e as próprias restrições podem ser tipos genéricos, da seguinte maneira:

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

Ao aplicar a where T : class restrição, evite os == operadores e != no parâmetro type porque esses operadores testam apenas a identidade de referência, não a igualdade de valor. Esse comportamento ocorre mesmo se esses operadores estão sobrecarregados em um tipo que é usado como um argumento. O código a seguir ilustra este ponto; A saída é falsa mesmo que a String classe sobrecarregue o == operador.

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

O compilador só sabe que T é um tipo de referência em tempo de compilação e deve usar os operadores padrão que são válidos para todos os tipos de referência. Se você precisar testar a igualdade de valor, aplique a where T : IEquatable<T> restrição ou where T : IComparable<T> e implemente a interface em qualquer classe usada para construir a classe genérica.

Restringir múltiplos parâmetros

Você pode aplicar restrições a vários parâmetros e várias restrições a um único parâmetro, conforme mostrado no exemplo a seguir:

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

Parâmetros de tipo não limitados

Os parâmetros de tipo que não têm restrições, como T na classe SampleClass<T>{}pública, são chamados de parâmetros de tipo não limitados. Os parâmetros de tipo não limitado têm as seguintes regras:

  • Os != operadores e == não podem ser usados porque não há garantia de que o argumento de tipo concreto suporte esses operadores.
  • Eles podem ser convertidos de e para ou explicitamente convertidos para qualquer tipo de System.Object interface.
  • Você pode compará-los com null. Se um parâmetro não limitado for comparado ao null, a comparação sempre retornará false se o argumento type for um tipo de valor.

Parâmetros de tipo como restrições

O uso de um parâmetro de tipo genérico como uma restrição é útil quando uma função membro com seu próprio parâmetro de tipo tem que restringir esse parâmetro ao parâmetro type do tipo que contém, como mostrado no exemplo a seguir:

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

No exemplo anterior, T é uma restrição de tipo no contexto do método e um parâmetro de Add tipo não limitado no contexto da List classe.

Os parâmetros de tipo também podem ser usados como restrições em definições de classe genéricas. O parâmetro de tipo deve ser declarado entre parênteses angulares juntamente com quaisquer outros parâmetros de tipo:

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

A utilidade dos parâmetros de tipo como restrições com classes genéricas é limitada porque o compilador não pode assumir nada sobre o parâmetro type, exceto que ele deriva de System.Object. Use parâmetros de tipo como restrições em classes genéricas em cenários nos quais você deseja impor uma relação de herança entre dois parâmetros de tipo.

notnull restrição

Você pode usar a notnull restrição para especificar que o argumento type deve ser um tipo de valor não anulável ou um tipo de referência não anulável. Ao contrário da maioria das outras restrições, se um argumento type violar a notnull restrição, o compilador gerará um aviso em vez de um erro.

A notnull restrição tem um efeito somente quando usada em um contexto anulável. Se você adicionar a notnull restrição em um contexto nulo esquecido, o compilador não gerará nenhum aviso ou erro para violações da restrição.

class restrição

A class restrição em um contexto anulável especifica que o argumento type deve ser um tipo de referência não anulável. Em um contexto anulável, quando um argumento type é um tipo de referência anulável, o compilador gera um aviso.

default restrição

A adição de tipos de referência anuláveis complica o uso de T? em um tipo ou método genérico. T? pode ser usado com a struct restrição OR class , mas um deles deve estar presente. Quando a class restrição foi usada, T? referiu-se ao tipo de referência anulável para T. T? pode ser usado quando nenhuma restrição é aplicada. Nesse caso, T? é interpretado como T? para tipos de valor e tipos de referência. No entanto, se T é uma instância de Nullable<T>, T? é o mesmo que T. Em outras palavras, não se torna T??.

Como T? agora pode ser usado sem a class restrição ou struct , ambiguidades podem surgir em substituições ou implementações de interface explícitas. Em ambos os casos, a substituição não inclui as restrições, mas as herda da classe base. Quando a classe base não aplica a restrição oustruct, as class classes derivadas precisam especificar de alguma forma uma substituição que se aplica ao método base sem qualquer restrição. O método derivado aplica a default restrição. A default restrição não esclarece nem a nem struct a class restrição.

Restrição não gerenciada

Você pode usar a unmanaged restrição para especificar que o parâmetro type deve ser um tipo não gerenciado não anulável. A unmanaged restrição permite que você escreva rotinas reutilizáveis para trabalhar com tipos que podem ser manipulados como blocos de memória, conforme mostrado no exemplo a seguir:

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

O método anterior deve ser compilado em um unsafe contexto porque usa o sizeof operador em um tipo não conhecido por ser um tipo interno. Sem a unmanaged restrição, o sizeof operador não está disponível.

A unmanaged restrição implica a struct restrição e não pode ser combinada com ela. Como a struct restrição implica a new() restrição, a unmanaged restrição não pode ser combinada com a new() restrição também.

Delegar restrições

Você pode usar System.Delegate ou System.MulticastDelegate como uma restrição de classe base. O CLR sempre permitiu essa restrição, mas a linguagem C# não permitiu. A System.Delegate restrição permite que você escreva código que funciona com delegados de uma maneira segura para digitação. O código a seguir define um método de extensão que combina dois delegados, desde que sejam do mesmo tipo:

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

Você pode usar o método acima para combinar delegados que são do mesmo tipo:

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

Se você descomentar a última linha, ela não será compilada. Ambos first e test são tipos de delegados, mas são tipos de delegados diferentes.

Restrições de Enum

Você também pode especificar o System.Enum tipo como uma restrição de classe base. O CLR sempre permitiu essa restrição, mas a linguagem C# não permitiu. Os genéricos que usam System.Enum fornecem programação segura para armazenar em cache os resultados do uso dos métodos estáticos no System.Enum. O exemplo a seguir localiza todos os valores válidos para um tipo de enum e, em seguida, cria um dicionário que mapeia esses valores para sua representação de cadeia de caracteres.

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 e Enum.GetName usar a reflexão, que tem implicações no desempenho. Você pode chamar EnumNamedValues para criar uma coleção que é armazenada em cache e reutilizada em vez de repetir as chamadas que exigem reflexão.

Você pode usá-lo como mostrado no exemplo a seguir para criar um enum e construir um dicionário de seus valores e nomes:

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

Argumentos de tipo implementam interface declarada

Alguns cenários exigem que um argumento fornecido para um parâmetro type implemente essa interface. Por exemplo:

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

Esse padrão permite que o compilador C# determine o tipo de contenção para os operadores sobrecarregados, ou qualquer static virtual método OR static abstract . Ele fornece a sintaxe para que os operadores de adição e subtração possam ser definidos em um tipo de contenção. Sem essa restrição, os parâmetros e argumentos precisariam ser declarados como a interface, em vez do parâmetro type:

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

A sintaxe anterior exigiria que os implementadores usassem a implementação de interface explícita para esses métodos. Fornecer a restrição extra permite que a interface defina os operadores em termos dos parâmetros de tipo. Os tipos que implementam a interface podem implementar implicitamente os métodos de interface.

Consulte também