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, que inclui os tipos record struct . 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, declarado ou implícito, 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, incluindo registros. |
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 : 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 não pode 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. |
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 : allows ref struct |
Essa anti-restrição declara que o argumento de tipo para T pode ser um tipo de ref struct . O tipo ou método genérico deve obedecer às regras de segurança de ref para qualquer instância de T porque pode ser um ref struct . |
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 restrições
struct
,class
,class?
,notnull
eunmanaged
. 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
ouwhere T : Base?
) não pode ser combinada com nenhuma das restriçõesstruct
,class
,class?
,notnull
ouunmanaged
. - 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?
. - Você não pode nomear a forma não anulável e anulável de uma interface como uma restrição.
- A restrição
new()
não pode ser combinada à restriçãostruct
ouunmanaged
. Se você especificar a restriçãonew()
, ela deverá ser a última restrição para esse parâmetro de tipo. As antirrestrições, se aplicáveis, podem seguir a restriçãonew()
. - A restrição
default
pode ser aplicada somente em implementações de interface explícitas ou de substituição. Ela não pode ser combinada com as restriçõesstruct
ouclass
. - A antirrestrição
allows ref struct
não pode ser combinada à restriçãoclass
ouclass?
. - A antirrestrição
allows ref struct
deve seguir todas as restrições para esse argumento de tipo.
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. Você aplica restrições ao parâmetro de tipo 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 sem suporte por System.Object. Por exemplo, a restrição de classe base informa ao compilador que somente os objetos desse tipo ou derivados desse tipo podem substituir este argumento 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 : notnull, Employee, IComparable<T>, new()
{
// ...
public void AddDefault()
{
T t = new T();
// ...
}
}
Ao aplicar a restrição where T : class
, evite os operadores ==
e !=
no parâmetro de tipo, pois esses operadores testam 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, aplique a restrição where T : IEquatable<T>
ou where T : IComparable<T>
e implemente a interface em qualquer classe 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
. T?
pode ser usado quando nenhuma das restrições é 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. 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 anterior 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>
{
static abstract T operator +(T left, T right);
static abstract 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>
{
static abstract IAdditionSubtraction<T> operator +(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
static abstract 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.
Permite ref struct
A antirrestrição allows ref struct
declara que o argumento de tipo correspondente para pode ser um tipo de ref struct
. As instâncias desse parâmetro de tipo devem obedecer às seguintes regras:
- Não pode ser demarcado.
- Ele participa regras de segurança de ref.
- Não é possível usar instâncias em que um tipo de
ref struct
não é permitido, como camposstatic
. - As instâncias podem ser marcadas com o modificador
scoped
.
A cláusula allows ref struct
não é herdada. No seguinte código:
class SomeClass<T, S>
where T : allows ref struct
where S : T
{
// etc
}
O argumento para S
não pode ser um ref struct
porque S
não tem a cláusula allows ref struct
.
Um parâmetro de tipo que tem a cláusula allows ref struct
não pode ser usado como um argumento de tipo, a menos que o parâmetro de tipo correspondente também tenha a cláusula allows ref struct
. Essa regra é demonstrada no exemplo a seguir:
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
}
O exemplo anterior mostra que um argumento de tipo que pode ser um tipo de ref struct
não pode ser substituído por um parâmetro de tipo que não pode ser um tipo de ref struct
.