Partilhar via


Tipos e métodos genéricos

Sugestão

Novo no desenvolvimento de software? Começa primeiro pelos tutoriais para começar . Vais encontrar genéricos assim que usares coleções como List<T>.

Experiente noutra língua? Os genéricos em C# são semelhantes aos genéricos em Java ou aos templates em C++, mas com informação completa dos tipos de execução e sem apagamento de tipos. Folheie as expressões de coleção e as secções de covariância e contravariância em busca de padrões específicos de C#.

Os genéricos permitem-te escrever código que funciona com qualquer tipo, mantendo a segurança total dos tipos. Em vez de escrever classes ou métodos separados para int, string, e todos os outros tipos de que precisares, escreve uma versão com um ou mais parâmetros de tipo (como T, ou TKey e TValue) e especifica os tipos reais quando a usares. O compilador verifica os tipos em tempo de compilação, por isso não é necessário conversões em tempo de execução nem correr riscos InvalidCastException.

Encontras genéricos constantemente no C# do dia a dia. Coleções, tipos de retorno assíncronos, delegados e LINQ dependem todos de tipos genéricos:

List<int> scores = [95, 87, 72, 91];
Dictionary<string, decimal> prices = new()
{
    ["Widget"] = 19.99m,
    ["Gadget"] = 29.99m
};
Task<string> greeting = Task.FromResult("Hello, generics!");
Func<int, bool> isPositive = n => n > 0;

Console.WriteLine($"First score: {scores[0]}");
Console.WriteLine($"Widget price: {prices["Widget"]:C}");
Console.WriteLine($"Greeting: {await greeting}");
Console.WriteLine($"Is 5 positive? {isPositive(5)}");

Em cada caso, o argumento do tipo entre colchetes angulares (<int>, <string>, <Product>) indica ao tipo genérico que tipo de dados detém ou em que opera. O compilador impõe a segurança de tipo. Não podes acidentalmente adicionar a string a um List<int>.

Consumo de tipos genéricos

Mais frequentemente, consumes tipos genéricos da biblioteca de classes .NET em vez de criares os teus próprios. As secções seguintes mostram os tipos genéricos mais comuns que irá usar.

Coleções genéricas

O System.Collections.Generic namespace fornece classes de coleção seguras para tipos. Use sempre estas coleções em vez de coleções não genéricas como ArrayList:

// A strongly typed list of strings
List<string> names = ["Alice", "Bob", "Carol"];
names.Add("Dave");
// names.Add(42); // Compile-time error: can't add an int to List<string>

// A dictionary mapping string keys to int values
var inventory = new Dictionary<string, int>
{
    ["Apples"] = 50,
    ["Oranges"] = 30
};
inventory["Bananas"] = 25;

// A set that prevents duplicates
HashSet<int> uniqueIds = [1, 2, 3, 1, 2];
Console.WriteLine($"Unique count: {uniqueIds.Count}"); // 3

// A FIFO queue
Queue<string> tasks = new();
tasks.Enqueue("Build");
tasks.Enqueue("Test");
Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Build

As coleções genéricas evitam erros de tipo durante a execução, uma vez que esses erros são identificados no momento da compilação. Estas coleções também evitam o boxing por tipos de valor, o que melhora o desempenho.

Métodos genéricos

Um método genérico declara o seu próprio parâmetro de tipo. O compilador frequentemente infere o argumento de tipo a partir dos valores que passa, por isso não precisa de o especificar explicitamente:

static void Print<T>(T value) =>
    Console.WriteLine($"Value: {value}");

Print(42);        // Compiler infers T as int
Print("hello");   // Compiler infers T as string
Print(3.14);      // Compiler infers T as double

Na chamada Print(42), o compilador infere T como int a partir do argumento. Podes escrever Print<int>(42) explicitamente, mas a inferência de tipos mantém o código mais limpo.

Expressões de coleções

Expressões de colecções (C# 12) fornecem uma sintaxe concisa para criar coleções. Use colchetes quadrados em vez de chamadas de construtores ou sintaxe do inicializador:

// Create a list with a collection expression
List<string> fruits = ["Apple", "Banana", "Cherry"];

// Create an array
int[] numbers = [1, 2, 3, 4, 5];

// Works with any supported collection type
IReadOnlyList<double> temperatures = [72.0, 68.5, 75.3];

Console.WriteLine($"Fruits: {string.Join(", ", fruits)}");
Console.WriteLine($"Numbers: {string.Join(", ", numbers)}");
Console.WriteLine($"Temps: {string.Join(", ", temperatures)}");

O operador spread (..) integra os elementos de uma coleção noutra, o que é útil para combinar sequências:

List<int> first = [1, 2, 3];
List<int> second = [4, 5, 6];

// Spread both lists into a new combined list
List<int> combined = [.. first, .. second];
Console.WriteLine(string.Join(", ", combined));
// Output: 1, 2, 3, 4, 5, 6

// Add extra elements alongside spreads
List<int> withExtras = [0, .. first, 99, .. second];
Console.WriteLine(string.Join(", ", withExtras));
// Output: 0, 1, 2, 3, 99, 4, 5, 6

As expressões de coleções funcionam com arrays, List<T>, Span<T>, ImmutableArray<T>, e qualquer tipo que suporte o padrão construtor de coleções. Para a referência sintática completa, veja Expressões de coleção.

Inicialização do dicionário

Pode inicializar dicionários de forma concisa com inicializadores de indexadores. Esta sintaxe utiliza colchetes quadrados para definir pares-chave-valor:

Dictionary<string, int> scores = new()
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Carol"] = 92
};

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

Pode fundir dicionários copiando um e aplicando substituições:

Dictionary<string, int> defaults = new()
{
    ["Timeout"] = 30,
    ["Retries"] = 3
};
Dictionary<string, int> overrides = new()
{
    ["Timeout"] = 60
};

// Merge defaults and overrides into a new dictionary
Dictionary<string, int> config = new(defaults);
foreach (var (key, value) in overrides)
{
    config[key] = value;
}

Console.WriteLine($"Timeout: {config["Timeout"]}");  // 60
Console.WriteLine($"Retries: {config["Retries"]}");   // 3

Restrições de tipo

As restrições restringem que tipo de argumentos um tipo genérico ou método aceita. As restrições permitem-lhe chamar métodos ou aceder a propriedades no parâmetro de tipo que não estariam disponíveis apenas com object :

static T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana")); // banana

static T CreateDefault<T>() where T : new() => new T();

var list = CreateDefault<List<int>>(); // Creates an empty List<int>
Console.WriteLine($"Empty list count: {list.Count}"); // 0

As restrições mais comuns são:

Restrição Meaning
where T : class T Deve ser um tipo de referência
where T : struct T deve ser um tipo de valor não anulável
where T : new() T deve ter um construtor público sem parâmetros
where T : BaseClass T deve derivar de BaseClass
where T : IInterface T deve implementar IInterface

Pode combinar restrições: where T : class, IComparable<T>, new(). Restrições menos comuns incluem where T : System.Enum, where T : System.Delegate, e where T : unmanaged para cenários especializados. Para a lista completa, veja Restrições sobre parâmetros de tipo.

Covariância e contravariância

Covariância e contravariância descrevem como os tipos genéricos se comportam com a herança. Eles determinam se pode usar um argumento de tipo mais ou menos derivado do que o originalmente especificado:

// Covariance: IEnumerable<Dog> can be used as IEnumerable<Animal>
// because IEnumerable<out T> is covariant
List<Dog> dogs = [new("Rex"), new("Buddy")];
IEnumerable<Animal> animals = dogs; // Allowed because Dog derives from Animal

foreach (var animal in animals)
{
    Console.WriteLine(animal.Name);
}

// Contravariance: Action<Animal> can be used as Action<Dog>
// because Action<in T> is contravariant
Action<Animal> printAnimal = a => Console.WriteLine($"Animal: {a.Name}");
Action<Dog> printDog = printAnimal; // Allowed because any Animal handler can handle Dog

printDog(new Dog("Spot"));
  • Covariância (out T): Um(a) IEnumerable<Dog> pode ser usado(a) onde IEnumerable<Animal> é esperado porque Dog deriva de Animal. A out palavra-chave no parâmetro de tipo permite isto. Parâmetros de tipo covariante só podem aparecer em posições de saída (tipos de retorno).
  • Contravariância (in T): Um Action<Animal> pode ser usado onde Action<Dog> é esperado porque qualquer ação que trate Animal também pode lidar com Dog. A in palavra-chave permite isto. Parâmetros de tipo contravariantes só podem aparecer em posições de entrada (parâmetros).

Muitas interfaces e delegados incorporados já são variantes: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult> e Action<in T>. Beneficia automaticamente da variância ao trabalhar com estes tipos. Para um tratamento aprofundado do desenho de interfaces variantes e delegados, veja Covariância e contravariância.

Crie os seus próprios tipos genéricos

Podes definir as tuas próprias classes, structs, interfaces e métodos genéricos. O exemplo seguinte mostra uma lista ligada genérica e simples para exemplificação. Na prática, use List<T> ou outra coleção pré-definida:

public class GenericList<T>
{
    private class Node(T data)
    {
        public T Data { get; set; } = data;
        public Node? Next { get; set; }
    }

    private Node? head;

    public void AddHead(T data)
    {
        var node = new Node(data) { Next = head };
        head = node;
    }

    public IEnumerator<T> GetEnumerator()
    {
        var current = head;
        while (current is not null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
}
var list = new GenericList<int>();
for (var i = 0; i < 5; i++)
{
    list.AddHead(i);
}

foreach (var item in list)
{
    Console.Write($"{item} ");
}
Console.WriteLine();
// Output: 4 3 2 1 0

Os tipos genéricos não se limitam às classes. Pode definir tipos genéricos interface, struct, e record . Para mais informações sobre o desenho de algoritmos genéricos e combinações complexas de restrições, veja Generics in .NET.

Ver também