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

Restrições informam o compilador sobre as funcionalidades que um argumento de tipo deve ter. Sem nenhuma restrição, o argumento de tipo poderia ser qualquer tipo. O compilador pode assumir somente os membros de System.Object, que é a classe base definitiva para qualquer tipo .NET. 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. Restrições são especificadas usando a palavra-chave contextual where. A tabela a seguir lista os sete tipos de restrições:

Constraint Descrição
where T : struct O argumento de tipo deve ser um tipo de valor não anulável. Para obter informações sobre tipos que permitem valor nulo, consulte Tipos que permitem valor nulo. Como todos os tipos de valor têm um construtor sem parâmetros acessível, a restrição struct implica a restrição new() e não pode ser combinada com a restrição new(). Você não pode combinar a restrição struct com a restrição unmanaged.
where T : class O argumento de tipo deve ser um tipo de referência. Essa restrição se aplica também 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 de tipo deve ser um tipo de referência, anulável ou não anulável. Essa restrição se aplica também a qualquer classe, interface, delegado ou tipo de matriz.
where T : notnull O argumento de tipo 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 : default Essa restrição resolve a ambiguidade quando você precisa especificar um parâmetro de tipo não treinado ao substituir um método ou fornecer uma implementação de interface explícita. A restrição default implica o método base sem a restrição class a struct. Para obter mais informações, consulte a proposta de especificação de restrição default.
where T : unmanaged O argumento de tipo deve ser um tipo não gerenciado não anulável. A restrição unmanaged implica a restrição struct e não pode ser combinada com as restrições struct ou new().
where T : new() O argumento de tipo deve ter um construtor público sem parâmetros. Quando usado em conjunto com outras restrições, a restrição new() deve ser a última a ser especificada. A restrição new() não pode ser combinada com as restrições restrições struct e unmanaged.
where T :<nome da classe base> O argumento de tipo 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 de tipo 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 de tipo deve ser ou implementar a interface especificada. Várias restrições de interface podem ser especificadas. A interface de restrição 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 de tipo deve ser ou implementar a interface especificada. Várias restrições de interface podem ser especificadas. A interface de restrição 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 pode não ser um tipo de valor anulável.
where T : U O argumento de tipo 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.

Por que usar 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 as chamadas de método do tipo de restrição. Se sua classe ou método genérico usar qualquer operação nos membros genéricos além da atribuição simples ou chamar quaisquer métodos sem suporte por System.Object, você aplicará restrições ao parâmetro de tipo. Por exemplo, a restrição de classe base informa ao compilador que somente os objetos desse tipo ou derivados desse tipo serão usados como argumentos de tipo. Uma vez que o compilador tiver essa garantia, ele poderá permitir que métodos desse tipo sejam chamados na classe genérica. O exemplo de código a seguir demonstra a funcionalidade que pode ser adicionada à classe GenericList<T> (em Introdução aos Genéricos) ao aplicar 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 propriedade Employee.Name. A restrição especifica que todos os itens do tipo T são um objeto Employee ou um objeto que herda de Employee.

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

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

Ao aplicar a restrição where T : class, evite os operadores == e != no parâmetro de tipo, pois esses operadores testarão somente a identidade de referência e não a igualdade de valor. Esse comportamento ocorrerá mesmo se esses operadores forem sobrecarregados em um tipo usado como argumento. O código a seguir ilustra esse ponto; a saída é false, muito embora a classe String 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 sabe apenas que T é um tipo de referência no tempo de compilação e deve usar os operadores padrão válidos para todos os tipos de referência. Caso seja necessário testar a igualdade de valor, a maneira recomendada é também aplicar a restrição where T : IEquatable<T> ou where T : IComparable<T> e implementar a interface em qualquer classe que seja usada para construir a classe genérica.

Restringindo vários parâmetros

É possível 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 associado

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

  • Os operadores != e == não podem ser usados, pois não há garantia de que o argumento de tipo concreto oferecerá suporte a eles.
  • Eles podem ser convertidos para e de System.Object ou explicitamente convertidos para qualquer tipo de interface.
  • Você pode compará-los com nulo. Se um parâmetro não associado for comparado a null, a comparação sempre retornará false se o argumento de tipo 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 será útil quando uma função membro com parâmetro de tipo próprio tiver que restringir esse parâmetro para o parâmetro de tipo do tipo recipiente, conforme 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 Add e um parâmetro de tipo não associado no contexto da classe List.

Parâmetros de tipo também podem ser usados como restrições em definições de classe genérica. O parâmetro de tipo deve ser declarado entre colchetes angulares junto 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, pois o compilador não pode presumir nada sobre o parâmetro de tipo, exceto que ele deriva de System.Object. Use parâmetros de tipo como restrições em classes genéricas em cenários nos quais deseja impor uma relação de herança entre dois parâmetros de tipo.

restrição de notnull

Você pode usar a restrição notnull para especificar que o argumento de tipo 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 de tipo violar a restrição notnull, o compilador gerará um aviso em vez de um erro.

A restrição notnull só tem efeito quando usada em um contexto anulável. Se você adicionar a restrição notnull em um contexto alheio anulável, o compilador não gerará avisos ou erros para violações da restrição.

restrição de class

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

restrição de default

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 restrição struct ou class, mas uma deve estar presente. Quando a restrição class era usada, T? referia-se ao tipo de referência anulável para T. A partir do C# 9, 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 for uma instância de Nullable<T>, T? será o mesmo que T. Em outras palavras, ele não se torna T??.

Como T? agora pode ser usado sem a restrição class ou struct, as 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 class ou struct, as classes derivadas precisam especificar de alguma forma uma substituição aplicada ao método base sem qualquer restrição. É quando o método derivado aplica a restrição default. A restrição default não esclarece a restrição nem a restrição class nem struct.

Restrição não gerenciada

Você pode usar a restrição unmanaged para especificar que o parâmetro de tipo deve ser um tipo não gerenciado não anulável. A restrição unmanaged 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 contexto unsafe porque ele usa o operador sizeof em um tipo não conhecido como um tipo interno. Sem a restrição unmanaged, o operador sizeof não está disponível.

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

Restrições de delegado

Você pode usar System.Delegate ou System.MulticastDelegate como uma restrição de classe base. O CLR sempre permitia essa restrição, mas a linguagem C# não a permite. A restrição System.Delegate permite que você escreva código que funcione com delegados de uma maneira fortemente tipada. O código a seguir define um método de extensão que combina dois delegados fornecidos que são 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ê remover a marca de comentário na última linha, ela não será compilada. Tanto first quanto test são tipos de representante, mas são tipos diferentes de representantes.

Restrições de enum

Você também pode especificar o tipo System.Enum como uma restrição de classe base. O CLR sempre permitia essa restrição, mas a linguagem C# não a permite. Genéricos usando System.Enum fornecem programação fortemente tipada para armazenar em cache os resultados do uso de métodos estáticos em System.Enum. O exemplo a seguir localiza todos os valores válidos para um tipo 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 usam reflexão, que tem implicações de 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 conforme mostrado no exemplo a seguir para criar uma enum e compilar um dicionário de seus nomes e valores:

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 de tipo 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 que contém os operadores sobrecarregados ou qualquer método static virtual ou static abstract. Ele fornece a sintaxe para que os operadores de adição e subtração possam ser definidos em um tipo que contém. Sem essa restrição, os parâmetros e argumentos seriam necessários para serem declarados como a interface, em vez do parâmetro de tipo:

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.

Confira também