C# Restrições (where)
Na aula Generics, o método Maior<T> não compilou: o compilador não sabia se o T que ia chegar podia ser comparado com >. Um T "livre" pode ser qualquer coisa, então só deixa você fazer o que qualquer coisa sabe fazer. Uma restrição (where) muda isso: ela diz quais tipos podem ocupar o T e, em troca, libera o que esses tipos sabem fazer.
- O que é uma restrição e como escrevê-la:
where T : …. where T : IComparable<T>: comparar qualquer tipo comparável.where T : Animal: usar as propriedades e os métodos de uma classe base.where T : new(): criar objetos do tipoT.class,structenotnull.where T : INumber<T>: um método que somaint,decimaledoubleC# 11.- Várias restrições juntas, e todos os erros reais que elas provocam.
Todos os programas desta aula rodaram com o .NET 10 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs. Você pode repetir cada um no editor do guia.
A ideia: os requisitos de uma vaga
Um anúncio de emprego diz: "Vaga de motorista. Requisito: carteira de habilitação." A vaga é para "qualquer pessoa", mas só pode ser ocupada por quem cumpre o requisito. E, por causa do requisito, a empresa pode mandar quem for contratado dirigir, sem precisar perguntar se a pessoa sabe.
Uma restrição é o requisito do T:
T Maior<T>(T a, T b) where T : IComparable<T>
└──────────┬──────────┘
"T pode ser qualquer tipo, DESDE QUE seja comparável"
- Para quem usa o genérico, ela é um limite: tipos que não cumprem o requisito são recusados na compilação.
- Para quem escreve o genérico, ela é uma garantia: dentro do método, você pode usar tudo o que o requisito promete (aqui, o
CompareTo).
1. Comparar: where T : IComparable<T>
Este é o Maior que não compilava na aula passada, agora com a restrição:
Console.WriteLine(Maior(3, 7));
Console.WriteLine(Maior("Ana", "Carla"));
Console.WriteLine(Maior(2.5m, 1.5m));
T Maior<T>(T a, T b) where T : IComparable<T>
{
if (a.CompareTo(b) > 0)
return a;
return b;
}
$ dotnet run maior.cs 7 Carla « "Carla" vem depois de "Ana": é a "maior" 2,5
- where T : IComparable<T>
- "Onde
Té um tipo que sabe se comparar com outroT." Owhere("onde") vem depois dos parâmetros do método e antes da chave{. Em classes, vem depois do nome:class Ranking<T> where T : IComparable<T>. - a.CompareTo(b) > 0
- A restrição garante que todo
TtemCompareTo(aula IComparable e IComparer). Positivo quer dizer "avem depois deb", ou seja,aé o maior. - Maior(3, 7), Maior("Ana", "Carla")…
int,stringedecimalimplementamIComparable, então cumprem o requisito.
E um tipo que não sabe se comparar? Ele é barrado antes de o programa rodar:
Console.WriteLine(Maior(new Aluno("Ana"), new Aluno("Bruno")));
T Maior<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
record Aluno(string Nome);
$ dotnet run naoserve.cs /home/ana/cs/naoserve.cs(1,19): error CS0311: O tipo "Aluno" não pode ser usado como parâmetro de tipo "T" no tipo ou método genérico "Maior<T>(T, T)". Não há conversão de referência implícita de "Aluno" em "System.IComparable<Aluno>". Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Compare com a aula IComparable e IComparer: lá, ordenar alunos que não sabiam se comparar compilava e explodia com InvalidOperationException durante a execução. Com a restrição, o mesmo engano vira um erro de compilação, apontando a linha 1. A mensagem diz: o Aluno não "é um" IComparable<Aluno>. O conserto é implementar a interface no Aluno.
Missão: o método Menor não compila (CS0019: o < não funciona com T). Acrescente a restrição where T : IComparable<T> e troque a < b por a.CompareTo(b) < 0.
9 uva 0,5
Console.WriteLine(Menor(12, 9));
Console.WriteLine(Menor("uva", "vagem"));
Console.WriteLine(Menor(0.5m, 3.0m));
T Menor<T>(T a, T b)
{
if (a < b)
return a;
return b;
}
Console.WriteLine(Menor(12, 9));
Console.WriteLine(Menor("uva", "vagem"));
Console.WriteLine(Menor(0.5m, 3.0m));
T Menor<T>(T a, T b) where T : IComparable<T>
{
if (a.CompareTo(b) < 0)
return a;
return b;
}
2. Uma classe base: where T : Animal
Sem restrição, o compilador não deixa usar nenhuma propriedade "do T", porque não sabe quais ele tem:
var abrigo = new Abrigo<string>();
class Abrigo<T>
{
private readonly List<T> animais = [];
public void Chamada()
{
foreach (T animal in animais)
Console.WriteLine(animal.Nome);
}
}
$ dotnet run semwhere.cs /home/ana/cs/semwhere.cs(10,38): error CS1061: ‘T’ não contém uma definição para "Nome" e não foi possível encontrar nenhum método de extensão "Nome" que aceite um primeiro argumento do tipo ‘T’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
De fato: alguém poderia criar um Abrigo<string>, como na primeira linha, e um texto não tem Nome. Com a restrição where T : Animal, o T só pode ser Animal ou uma classe filha dele (veja Herança), e tudo o que o Animal tem fica liberado:
var gatos = new Abrigo<Gato>();
gatos.Receber(new Gato { Nome = "Mingau" });
gatos.Receber(new Gato { Nome = "Frajola" });
gatos.Chamada();
var caes = new Abrigo<Cachorro>();
caes.Receber(new Cachorro { Nome = "Rex" });
caes.Chamada();
class Abrigo<T> where T : Animal
{
private readonly List<T> animais = [];
public void Receber(T animal) => animais.Add(animal);
public void Chamada()
{
foreach (T animal in animais)
Console.WriteLine($"{animal.Nome}: {animal.Som()}");
}
}
abstract class Animal
{
public string Nome { get; init; } = "";
public abstract string Som();
}
class Gato : Animal
{
public override string Som() => "miau";
}
class Cachorro : Animal
{
public override string Som() => "au au";
}
$ dotnet run base.cs Mingau: miau Frajola: miau Rex: au au
- class Abrigo<T> where T : Animal
- "Um abrigo de
T, ondeTé algum tipo de animal." - animal.Nome, animal.Som()
- Liberados pela restrição: todo
AnimaltemNomeeSom(). OSom()chamado é o da classe filha (ooverride; veja Polimorfismo). - new Abrigo<Gato>()
- Um abrigo só de gatos:
gatos.Receber(new Cachorro { … })dariaCS1503. É isso que o genérico traz a mais do que um simplesList<Animal>.
E new Abrigo<string>(), com a restrição? Agora quem recusa é a primeira linha: error CS0311: O tipo "string" não pode ser usado como parâmetro de tipo "T" no tipo ou método genérico "Abrigo<T>". Não há conversão de referência implícita de "string" em "Animal".
3. Criar objetos: where T : new()
Dentro de um genérico, new T() também é proibido: nem todo tipo tem um construtor sem parâmetros.
var lista = CriarVarios<Contador>(3);
Console.WriteLine(lista.Count);
List<T> CriarVarios<T>(int quantos)
{
var itens = new List<T>();
for (int i = 0; i < quantos; i++)
itens.Add(new T());
return itens;
}
class Contador
{
public int Valor { get; set; }
}
$ dotnet run novo.cs /home/ana/cs/novo.cs(8,19): error CS0304: Não é possível criar uma instância do tipo de variável "T" porque ela não tem a restrição new() Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
A própria mensagem dá o conserto: a restrição new(), "o T tem de ter um construtor público sem parâmetros":
var lista = CriarVarios<Contador>(3);
lista[0].Valor = 10;
Console.WriteLine($"{lista.Count} contadores: {string.Join(", ", lista.Select(c => c.Valor))}");
List<T> CriarVarios<T>(int quantos) where T : new()
{
var itens = new List<T>();
for (int i = 0; i < quantos; i++)
itens.Add(new T());
return itens;
}
class Contador
{
public int Valor { get; set; }
}
$ dotnet run novo2.cs 3 contadores: 10, 0, 0 « três objetos diferentes: só o primeiro mudou
O Contador não escreve construtor nenhum, então ganha o construtor vazio automático e cumpre o requisito. Uma classe cujo único construtor pede parâmetros, como public Aluno(string nome), é recusada: error CS0310: "Aluno" deve ser um tipo non-abstract com um construtor público sem-parâmetros para que possa ser usado como parâmetro "T"….
4. class, struct e notnull
Três restrições dizem só "de que família" o tipo é:
| Restrição | O T tem de ser… | Aceita | Recusa |
|---|---|---|---|
where T : class | um tipo de referência (classe, string, record, interface, array) | string, Aluno | int, decimal (CS0452) |
where T : struct | um tipo de valor não anulável (veja Valor × Referência) | int, bool, DateTime, enums | string, int? (CS0453) |
where T : notnull | um tipo que não aceita null | int, string | string?, int? (só um aviso, CS8714) |
$ cat classstruct.cs
var a = new SoReferencia<int>();
var b = new SoValor<string>();
class SoReferencia<T> where T : class { }
class SoValor<T> where T : struct { }
$ dotnet run classstruct.cs
/home/ana/cs/classstruct.cs(1,26): error CS0452: O tipo "int" deve ser um tipo de referência para que seja usado como parâmetro "T" no tipo ou método genérico "SoReferencia<T>"
/home/ana/cs/classstruct.cs(2,21): error CS0453: O tipo 'string' deve ser um tipo de valor não anulável para que seja usado como parâmetro 'T' no tipo ou método genérico 'SoValor<T>'
Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O notnull é o que o próprio Dictionary<TKey, TValue> usa na chave: where TKey : notnull. Por isso uma chave null dá ArgumentNullException e o compilador avisa quando a chave pode ser null.
5. Somar qualquer número: where T : INumber<T> C# 11
Até o C# 10, não havia como escrever um único Somar para int, decimal e double: o + não funciona com T. Desde o .NET 7, todos os tipos numéricos implementam a interface INumber<T> ("número"), que promete os operadores +, -, *, /, as comparações e valores como T.Zero. É a chamada generic math, "matemática genérica":
using System.Numerics;
List<int> quantidades = [3, 4, 5];
List<decimal> precos = [18.90m, 5.49m, 6.76m];
List<double> pesos = [1.5, 0.25];
Console.WriteLine(Somar(quantidades));
Console.WriteLine(Somar(precos));
Console.WriteLine(Somar(pesos));
T Somar<T>(List<T> valores) where T : INumber<T>
{
T total = T.Zero;
foreach (T v in valores)
total += v;
return total;
}
$ dotnet run numeros.cs 12 31,15 1,75
- using System.Numerics;
- O namespace do
INumber. Ele não vem nosusingautomáticos. Sem esta linha:error CS0246: O nome do tipo ou do namespace "INumber<>" não pode ser encontrado. - where T : INumber<T>
- "
Té algum tipo de número."int,long,decimal,double,float… todos servem. - T total = T.Zero;
- O zero "do tipo
T":0paraint,0mparadecimal. Repare: éT.Zero, um membro chamado no próprio tipo (um membro estático de interface, novidade do C# 11). - total += v;
- Liberado pela restrição: todo
INumbersabe somar.
É assim que o Sum() do LINQ funciona para qualquer tipo de número.
6. Várias restrições juntas
Um mesmo T pode ter vários requisitos, separados por vírgula, e cada parâmetro de tipo tem o seu próprio where:
class Cadastro<TChave, TItem>
where TChave : notnull
where TItem : class, IComparable<TItem>, new()
{
}
Leia: "a chave não pode ser nula; o item tem de ser uma classe, comparável, com construtor vazio". A ordem dentro de um where é fixa:
- primeiro, a família:
class,struct,notnullouunmanaged(no máximo uma), ou uma classe base; - depois, as interfaces, quantas quiser;
- por último, o
new().
Fora dessa ordem, o compilador reclama. Com where T : new(), class:
$ dotnet run ordem.cs /home/ana/cs/ordem.cs(1,28): error CS0401: A restrição new() deve ser a última restrição restritiva especificada /home/ana/cs/ordem.cs(1,35): error CS0449: As restrições 'class', 'struct', 'unmanaged', 'notnull' e 'default' não podem ser combinadas nem duplicadas e precisam ser especificadas primeiro na lista de restrições. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Peça o mínimo de que o código precisa. Cada restrição a mais recusa tipos que poderiam funcionar. Se o método só compara, IComparable<T> basta; não exija também class e new() "por garantia".
Quando dá errado
error CS0019: O operador ">" não pode ser aplicado a operandos dos tipos "T" e "T" / error CS1061: ‘T’ não contém uma definição para "Nome"…
O que significa: você usou com T algo que nem todo tipo tem: um operador ou uma propriedade.
Como consertar: acrescente a restrição que garante aquilo: IComparable<T> (e use CompareTo), INumber<T> (operadores de conta) ou uma classe base que tenha a propriedade.
error CS0311 / error CS0315: O tipo "…" não pode ser usado como parâmetro de tipo "T"… Não há conversão de referência implícita (ou boxing) de "…" em "…"
O que significa: o tipo que você passou não cumpre o requisito do where. O CS0311 aparece com classes; o CS0315, com tipos de valor como int. O fim da mensagem diz qual requisito falhou.
Como consertar: implemente a interface ou herde da classe pedida, ou use outro tipo.
error CS0304: Não é possível criar uma instância do tipo de variável "T" porque ela não tem a restrição new()
O que significa: há um new T() num genérico sem where T : new().
Como consertar: acrescente new() no fim do where.
error CS0310: "Aluno" deve ser um tipo non-abstract com um construtor público sem-parâmetros…
O que significa: o genérico pede new(), e o tipo passado só tem construtores com parâmetros (ou é abstrato).
Como consertar: dê à classe um construtor público sem parâmetros, ou repense se o genérico precisa mesmo criar objetos.
error CS0452 (precisa ser referência) / error CS0453 (precisa ser valor não anulável)
O que significa: o genérico tem where T : class e recebeu um int, ou tem where T : struct e recebeu um string (ou um int?).
Como consertar: use um tipo da família pedida.
error CS0401: A restrição new() deve ser a última restrição restritiva especificada (e CS0449)
O que significa: as restrições estão fora de ordem.
Como consertar: família primeiro (class, struct…), interfaces no meio, new() no fim.
warning CS8714: O tipo 'string?' não pode ser usado como parâmetro de tipo 'TChave'… A nulidade do argumento de tipo 'string?' não corresponde à restrição 'notnull'.
O que significa: a restrição é notnull, e você passou um tipo anulável. É só um aviso, mas um null ali pode virar exceção depois.
Como consertar: tire o ? (Cadastro<string>).
Referência rápida
| Restrição | Exige | Libera |
|---|---|---|
where T : IComparable<T> | Tipo comparável | a.CompareTo(b) |
where T : UmaInterface | Implementa a interface | Os membros da interface |
where T : ClasseBase | É a classe ou uma filha | Os membros da classe base |
where T : new() | Construtor público sem parâmetros | new T() |
where T : class | Tipo de referência | Comparar com null, T? como referência anulável |
where T : struct | Tipo de valor não anulável | T? como Nullable<T> |
where T : notnull | Tipo não anulável | Usar como chave de dicionário sem aviso |
where T : INumber<T> C# 11 | Tipo numérico (using System.Numerics;) | + - * /, T.Zero, T.One, comparações |
where T : class, IX, new() | Várias juntas | Ordem: família, interfaces, new() |
Exercícios
1. A palavra da restrição
Complete para exigir que T seja comparável:
2. Poder criar
Complete para o método poder fazer new T():
3. Com class Abrigo<T> where T : Animal, o que acontece com new Abrigo<string>()?
4. Qual restrição permite usar + e T.Zero num método genérico?
5. Qual destas está na ordem certa?
Missão final: o total de qualquer coisa
Missão: o caixa da lojinha soma quantidades (int), preços (decimal) e pesos (double), mas o Somar só aceita int. Torne-o genérico com where T : INumber<T> (não esqueça o using System.Numerics;), começando o total em T.Zero.
Itens: 12 Total: R$ 31,15 Peso: 1,75 kg
List<int> quantidades = [3, 4, 5];
List<decimal> precos = [18.90m, 5.49m, 6.76m];
List<double> pesos = [1.5, 0.25];
Console.WriteLine($"Itens: {Somar(quantidades)}");
Console.WriteLine($"Total: {Somar(precos):C}");
Console.WriteLine($"Peso: {Somar(pesos)} kg");
int Somar(List<int> valores)
{
int total = 0;
foreach (int v in valores)
total += v;
return total;
}
using System.Numerics;
List<int> quantidades = [3, 4, 5];
List<decimal> precos = [18.90m, 5.49m, 6.76m];
List<double> pesos = [1.5, 0.25];
Console.WriteLine($"Itens: {Somar(quantidades)}");
Console.WriteLine($"Total: {Somar(precos):C}");
Console.WriteLine($"Peso: {Somar(pesos)} kg");
T Somar<T>(List<T> valores) where T : INumber<T>
{
T total = T.Zero;
foreach (T v in valores)
total += v;
return total;
}
Resumo
- Uma restrição (
where T : …) diz quais tipos podem ocupar oTe libera, dentro do genérico, o que esses tipos garantem. - Tipos que não cumprem o requisito são recusados na compilação (
CS0311,CS0315,CS0310,CS0452,CS0453), e não com exceção durante a execução. IComparable<T>libera oCompareTo; uma classe base libera as propriedades e métodos dela;new()libera onew T().class,structenotnulldizem a família do tipo.INumber<T>(deSystem.Numerics) libera as contas e oT.Zero: umSomarpara todos os números.- Várias restrições vão na ordem família, interfaces,
new()(CS0401). Peça só o necessário.