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.

Você vai aprender
  • 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 tipo T.
  • class, struct e notnull.
  • where T : INumber<T>: um método que soma int, decimal e double C# 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 outro T." O where ("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 T tem CompareTo (aula IComparable e IComparer). Positivo quer dizer "a vem depois de b", ou seja, a é o maior.
Maior(3, 7), Maior("Ana", "Carla")…
int, string e decimal implementam IComparable, 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, onde T é algum tipo de animal."
animal.Nome, animal.Som()
Liberados pela restrição: todo Animal tem Nome e Som(). O Som() chamado é o da classe filha (o override; veja Polimorfismo).
new Abrigo<Gato>()
Um abrigo só de gatos: gatos.Receber(new Cachorro { … }) daria CS1503. É isso que o genérico traz a mais do que um simples List<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çãoO T tem de ser…AceitaRecusa
where T : classum tipo de referência (classe, string, record, interface, array)string, Alunoint, decimal (CS0452)
where T : structum tipo de valor não anulável (veja Valor × Referência)int, bool, DateTime, enumsstring, int? (CS0453)
where T : notnullum tipo que não aceita nullint, stringstring?, 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 nos using automá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": 0 para int, 0m para decimal. 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 INumber sabe 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:

  1. primeiro, a família: class, struct, notnull ou unmanaged (no máximo uma), ou uma classe base;
  2. depois, as interfaces, quantas quiser;
  3. 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.
Quanto pedir?

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çãoExigeLibera
where T : IComparable<T>Tipo comparávela.CompareTo(b)
where T : UmaInterfaceImplementa a interfaceOs membros da interface
where T : ClasseBaseÉ a classe ou uma filhaOs membros da classe base
where T : new()Construtor público sem parâmetrosnew T()
where T : classTipo de referênciaComparar com null, T? como referência anulável
where T : structTipo de valor não anulávelT? como Nullable<T>
where T : notnullTipo não anulávelUsar como chave de dicionário sem aviso
where T : INumber<T> C# 11Tipo numérico (using System.Numerics;)+ - * /, T.Zero, T.One, comparações
where T : class, IX, new()Várias juntasOrdem: família, interfaces, new()

Exercícios

1. A palavra da restrição

Complete para exigir que T seja comparável:

T Maior<T>(T a, T b) T : IComparable<T>

2. Poder criar

Complete para o método poder fazer new T():

T Criar<T>() where 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 o T e 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 o CompareTo; uma classe base libera as propriedades e métodos dela; new() libera o new T().
  • class, struct e notnull dizem a família do tipo.
  • INumber<T> (de System.Numerics) libera as contas e o T.Zero: um Somar para todos os números.
  • Várias restrições vão na ordem família, interfaces, new() (CS0401). Peça só o necessário.