Compartir a través de


Métodos y tipos genéricos

Sugerencia

¿No está familiarizado con el desarrollo de software? Comience primero con los tutoriales de introducción . Encontrará genéricos tan pronto como use colecciones como List<T>.

¿Competente en otro idioma? Los genéricos de C# son similares a los genéricos de Java o plantillas de C++, pero con información de tipo en tiempo de ejecución completa y sin borrado de tipos. Eche un vistazo a las secciones sobre expresiones de selección, covarianza y contravarianza para conocer los patrones específicos de C#.

Los genéricos le permiten escribir código que funcione con cualquier tipo mientras mantiene la seguridad completa del tipo. En lugar de escribir clases o métodos independientes para int, stringy todos los demás tipos que necesite, escriba una versión con uno o varios parámetros de tipo (como T, o TKey y TValue) y especifique los tipos reales al usarlos. El compilador comprueba los tipos en tiempo de compilación, por lo que no es necesario realizar conversiones en tiempo de ejecución ni correr el riesgo de InvalidCastException.

Encuentras genéricos constantemente en el C# diario. Las colecciones, los tipos de valor devuelto asincrónico, los delegados y LINQ se basan en 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)}");

En cada caso, el argumento type entre corchetes angulares (<int>, <string>, <Product>) indica al tipo genérico qué tipo de datos contiene o opera. El compilador aplica la seguridad de tipos. No se puede agregar accidentalmente un string a un List<int>.

Consumo de tipos genéricos

Con mayor frecuencia, consumes tipos genéricos de la biblioteca de clases .NET en lugar de crear los tuyos propios. En las secciones siguientes se muestran los tipos genéricos más comunes que usará.

Colecciones genéricas

El espacio de nombres System.Collections.Generic proporciona clases de colección seguras para tipos. Use siempre estas colecciones en lugar de colecciones no 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

Las colecciones genéricas impiden errores de tipo en tiempo de ejecución porque los errores se muestran en tiempo de compilación en su lugar. Estas colecciones también evitan la conversión boxing para los tipos de valor, lo que mejora el rendimiento.

Métodos genéricos

Un método genérico declara su propio parámetro de tipo. El compilador a menudo deduce el argumento type de los valores que se pasan, por lo que no es necesario especificarlo explícitamente:

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

En la llamada a Print(42), el compilador deduce T como int del argumento . Puede escribir Print<int>(42) explícitamente, pero la inferencia de tipos mantiene el código más limpio.

Expresiones de colección

Las expresiones de colección (C# 12) proporcionan una sintaxis concisa para crear colecciones. Utilice corchetes en lugar de llamadas de constructor o sintaxis de inicialización.

// 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)}");

El operador de propagación (..) inserta los elementos de una colección en otra, lo que resulta útil para combinar secuencias:

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

Las expresiones de colección funcionan con matrices, List<T>, Span<T>, ImmutableArray<T>y cualquier tipo que admita el patrón del generador de colecciones. Para obtener la referencia de sintaxis completa, vea Expresiones de colección.

Inicialización del diccionario

Puede inicializar diccionarios de forma concisa con inicializadores de indexador. Esta sintaxis usa corchetes para establecer pares clave-valor:

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

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

Puede combinar diccionarios copiando uno y aplicando sobrescrituras:

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

Restricciones de tipo

Las restricciones restringen qué argumentos de tipo acepta un tipo o método genéricos. Las restricciones permiten llamar a métodos o acceder a propiedades en el parámetro de tipo que no estarían disponibles solo con 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

Las restricciones más comunes son:

Restricción Meaning
where T : class T debe ser un tipo de referencia
where T : struct T debe ser un tipo de valor que no acepta valores NULL.
where T : new() T debe tener un constructor público sin parámetros
where T : BaseClass T debe derivar de BaseClass
where T : IInterface T debe implementar IInterface

Puede combinar restricciones: where T : class, IComparable<T>, new(). Entre las restricciones menos comunes se incluyen where T : System.Enum, where T : System.Delegatey where T : unmanaged para escenarios especializados. Para obtener la lista completa, consulte Restricciones en parámetros de tipo.

Covarianza y contravarianza

La covarianza y la contravarianza describen cómo se comportan los tipos genéricos con la herencia. Determinan si puede usar un argumento de tipo más derivado o menos derivado del 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"));
  • Covarianza (out T): IEnumerable<Dog> se puede usar donde IEnumerable<Animal> se espera porque Dog se deriva de Animal. La out palabra clave del parámetro type habilita esto. Los parámetros de tipo covariante solo pueden aparecer en posiciones de salida (tipos devueltos).
  • Contravarianza (in T): Un Action<Animal> puede utilizarse en el lugar de un Action<Dog> porque cualquier acción que maneje Animal también puede controlar Dog. La in palabra clave habilita esto. Los parámetros de tipo contravariante solo pueden aparecer en posiciones de entrada (parámetros).

Muchas interfaces y delegados integrados ya son variantes: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult>y Action<in T>. Al trabajar con estos tipos, se beneficia automáticamente de la varianza. Para obtener un análisis detallado del diseño de interfaces y delegados variantes, consulte Covarianza y contravarianza.

Cree sus propios tipos genéricos

Puede definir sus propias clases genéricas, estructuras, interfaces y métodos. En el ejemplo siguiente se muestra una lista vinculada genérica simple para la ilustración. En la práctica, use List<T> u otra colección integrada:

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

Los tipos genéricos no se limitan a las clases. Puede definir tipos genéricos interface, structy record . Para obtener más información sobre cómo diseñar algoritmos genéricos y combinaciones de restricciones complejas, vea Generics en .NET.

Consulte también