Compartilhar via


Tipos e métodos genéricos

Dica

Novo no desenvolvimento de software? Comece primeiro com os tutoriais de Introdução . Você encontrará genéricos assim que usar coleções como List<T>.

Experimentou em outro idioma? Os genéricos de C# são semelhantes aos genéricos em Java ou aos modelos no C++, mas com informações completas de tipo de runtime e sem remoção de tipo. Percorra as seções de expressões de coleção e covariância e contravariância para padrões específicos de C#.

Os genéricos permitem que você escreva código que funcione com qualquer tipo, mantendo a segurança completa de tipos. Em vez de escrever classes ou métodos separados para int, stringe todos os outros tipos de que você precisa, escreva uma versão com um ou mais parâmetros de tipo (como T, ou TKey e ) e especifique TValueos tipos reais ao usá-la. O compilador verifica os tipos em tempo de compilação; portanto, você não precisa de conversões de tipo de runtime nem corre riscos InvalidCastException.

Você encontra genéricos constantemente no C# do dia a dia. Coleções, tipos de retorno assíncronos, delegados e LINQ dependem 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 de tipo em colchetes angulares (<int>, <string>, <Product>) informa ao tipo genérico em que tipo de dados ele contém ou opera. O compilador impõe a segurança do tipo. Você não pode adicionar acidentalmente um string a um List<int>.

Consumindo tipos genéricos

Com mais frequência, você consume tipos genéricos da biblioteca de classes .NET em vez de criar seus próprios. As seções a seguir mostram os tipos genéricos mais comuns que você usará.

Coleções genéricas

O System.Collections.Generic namespace fornece classes de coleção seguras para tipos. Sempre use essas 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 impedem erros de tipo de runtime porque os erros surgem durante a compilação. Essas coleções também evitam o boxing para tipos de valor, o que melhora o desempenho.

Métodos genéricos

Um método genérico declara seu próprio parâmetro de tipo. O compilador geralmente infere o argumento de tipo dos valores que você passa, portanto, você não precisa especificá-lo 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. Você pode escrever Print<int>(42) explicitamente, mas a inferência de tipo mantém o código mais limpo.

Expressões de coleção

As expressões de coleção (C# 12) fornecem uma sintaxe concisa para criar coleções. Use colchetes em vez de chamadas de construtor ou sintaxe de 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 de spread (..) inlineia os elementos de uma coleção em outra, 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ção funcionam com matrizes, List<T>, Span<T>ImmutableArray<T>e qualquer tipo que dê suporte ao padrão do construtor de coleções. Para obter a referência de sintaxe completa, consulte Expressões de coleção.

Inicialização de dicionário

Você pode inicializar dicionários concisamente com inicializadores de indexador. Essa sintaxe usa colchetes 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}");
}

Você pode mesclar 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

Restrições restringem quais argumentos de tipo um tipo ou método genérico aceita. As restrições permitem que você chame métodos ou acesse propriedades do 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:

Constraint 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

Você pode combinar restrições: where T : class, IComparable<T>, new(). Restrições menos comuns incluem where T : System.Enum, where T : System.Delegatee where T : unmanaged para cenários especializados. Para obter a lista completa, consulte Restrições nos parâmetros de tipo.

Covariância e contravariância

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

// 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 IEnumerable<Dog> pode ser usado onde IEnumerable<Animal> é esperado porque Dog deriva de Animal. A out palavra-chave no parâmetro de tipo permite isso. 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 manipula Animal também pode manipular Dog. A in palavra-chave permite isso. Parâmetros de tipo contravariante só podem aparecer em posições de entrada (parâmetros).

Muitas interfaces integradas e delegates já são variantes: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult> e Action<in T>. Você se beneficia da variação automaticamente quando trabalha com esses tipos. Para obter um tratamento aprofundado da criação de interfaces e delegates de variante, consulte Covariância e contravariância.

Criar seus próprios tipos genéricos

Você pode definir suas próprias classes genéricas, structs, interfaces e métodos. O exemplo a seguir mostra uma lista vinculada genérica simples para ilustração. Na prática, use List<T> ou outra coleção embutida:

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

Tipos genéricos não se limitam a classes. Você pode definir tipos interface, struct e record genéricos. Para obter mais informações sobre como criar algoritmos genéricos e combinações de restrições complexas, consulte Generics em .NET.

Consulte também