Dominio de los genéricos de C#
Los genéricos en C# se han convertido en una parte integral del lenguaje desde su introducción, proporcionando numerosos beneficios incluso si su funcionamiento no es completamente entendido por todos los desarrolladores. En su vídeo "Cómo crear genéricos en C#, incluidas las nuevas funciones", Tim Corey explica por qué son importantes los genéricos, cómo crearlos y muestra sus aplicaciones prácticas.
Este artículo proporciona una guía completa sobre los genéricos de C# y ofrece información valiosa extraída del vídeo de Tim Corey sobre el tema. Abarca los fundamentos de los genéricos, incluida la seguridad de tipos, las ventajas de rendimiento y las aplicaciones prácticas. El artículo también explora la creación de métodos genéricos, clases, interfaces y la aplicación de restricciones a los genéricos. Además, destaca casos de uso del mundo real y la importancia de usar genéricos para escribir código eficiente y seguro.
Introducción
Los genéricos de C# ofrecen una potente forma de crear código flexible, reutilizable y seguro desde el punto de vista tipográfico, ya que permiten a los desarrolladores definir clases, métodos, interfaces y colecciones que funcionan con cualquier tipo de datos. Al utilizar una clase genérica o un método genérico, los desarrolladores pueden definir un parámetro de tipo genérico (por ejemplo, T) que puede representar cualquier tipo de datos. Esto elimina la necesidad de duplicar código y mejora la reutilización de código mientras asegura la seguridad de tipos en tiempo de compilación. Las clases de colección genéricas como List y Dictionary<TKey, TValue> permiten manejar de manera eficiente diferentes tipos de datos, mientras que las interfaces genéricas y los delegados genéricos permiten la creación de tipos genéricos personalizados que pueden trabajar con múltiples parámetros de tipo. Al aprovechar los genéricos, los desarrolladores pueden maximizar la eficiencia del código y minimizar los inconvenientes de las clases no genéricas. Esta flexibilidad permite crear más código reutilizable sin sacrificar el rendimiento ni la seguridad tipográfica.
Tim at (0:00) introduce el tema haciendo hincapié en el uso generalizado de los genéricos en C#. Su objetivo es explicar por qué los genéricos son esenciales y demostrar cómo crearlos y utilizarlos con eficacia.
Creación del proyecto
Tim comienza creando una nueva aplicación de consola llamada "GenericsDemoApp" para centrarse únicamente en demostrar los genéricos sin ninguna distracción de la interfaz de usuario. Configura el proyecto utilizando .NET 8 y Visual Studio 2022.
Básicos de los genéricos
Tim a las (2:22) comienza con un ejemplo utilizando la clase List para ilustrar el concepto de genéricos. Los genéricos permiten especificar el tipo de elementos que puede contener una colección, proporcionando seguridad de tipo y evitando errores en tiempo de ejecución.
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };
La List asegura que solo se pueden agregar enteros a la lista de números y solo cadenas a la lista de cadenas.
Seguridad tipográfica y eficiencia
Tim destaca la importancia de la seguridad de tipos que proporcionan los genéricos. El compilador comprueba los tipos en tiempo de diseño, evitando desajustes de tipo y garantizando la ejecución segura del código. Los genéricos también evitan la necesidad de hacer boxing y unboxing, lo que da lugar a un código más eficiente.
Ineficiencia de las colecciones no genéricas
Para demostrar la ineficacia del uso de colecciones no genéricas, Tim crea una lista y muestra cómo puede contener distintos tipos de objetos.
List<object> objects = new List<object> { "Tim", 4, 3.6m };
List<object> objects = new List<object> { "Tim", 4, 3.6m };
Explica que el uso de colecciones no genéricas puede dar lugar a desajustes de tipos e ineficiencias debidas al boxing y unboxing.
Comparación de prestaciones
Tim en (6:15) realiza una comparación de rendimiento entre el uso de una List
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");
Los resultados muestran que añadir enteros a una List

Creación de un método de comprobación de tipos
Tim en (10:14) demuestra cómo crear un método genérico llamado TypeChecker. Este método comprueba el tipo de un valor dado y lo imprime, ilustrando la flexibilidad y la potencia de los genéricos.
public static void TypeChecker<t>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}
public static void TypeChecker<t>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}
El método TypeChecker utiliza el operador typeof para determinar el tipo del parámetro genérico T e imprime tanto el tipo como el valor.
Utilización del método Type Checker
Tim proporciona ejemplos de llamada al método TypeChecker con diferentes tipos de argumentos.
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1
Al pasar varios tipos al método TypeChecker, Tim muestra cómo los genéricos pueden manejar diferentes tipos de datos sin problemas.
Creación de una clase genérica: Mejor lista
Tim en (16:25) pasa a crear una clase genérica llamada BetterList. Esta clase encapsula una lista de un tipo específico y proporciona funcionalidad adicional.
public class BetterList<t>
{
private List<t> data = new List<t>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
public class BetterList<t>
{
private List<t> data = new List<t>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
La clase BetterList incluye una List privada y un método AddToList que añade un valor a la lista e imprime un mensaje indicando la adición.
Uso de la clase Better List
Tim proporciona ejemplos de uso de la clase BetterList con diferentes tipos.
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));
En estos ejemplos, BetterList
Creación de una interfaz genérica
En el minuto 21:48, Tim presenta la idea de una interfaz genérica denominada IImportance. Esta interfaz define un método para determinar cuál de dos valores es más importante.
public interface IImportance<t>
{
T MostImportant(T a, T b);
}
public interface IImportance<t>
{
T MostImportant(T a, T b);
}
Implementación de la interfaz genérica
Tim muestra cómo implementar esta interfaz para diferentes tipos. Comienza con una implementación de enteros.
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}
A continuación, implementa la interfaz para cadenas, utilizando la longitud de las cadenas para determinar la importancia.
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}
Estas implementaciones demuestran cómo se puede aplicar la misma interfaz a diferentes tipos con una lógica específica para cada tipo.
Aplicación de restricciones a los genéricos
Tim explica en (25:21) cómo aplicar restricciones a los genéricos para garantizar que cumplen determinadas condiciones. Por ejemplo, un tipo genérico puede limitarse a tener un constructor vacío o a implementar una interfaz concreta.
public class SampleClass<t> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<t> where T : IImportance<t>
{
// Class implementation
}
public class SampleClass<t> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<t> where T : IImportance<t>
{
// Class implementation
}
Estas restricciones ayudan a garantizar que el tipo genérico cumpla los criterios necesarios, evitando errores en tiempo de ejecución y mejorando la seguridad de tipo.
Implementación de INumber por parte de Microsoft
Tim explica cómo Microsoft utiliza la interfaz INumber para restringir las operaciones numéricas. Esto permite realizar operaciones aritméticas como sumas y restas en tipos genéricos.
public class MathOperations<t> where T : INumber<t>
{
public T Add(T x, T y)
{
return x + y;
}
}
public class MathOperations<t> where T : INumber<t>
{
public T Add(T x, T y)
{
return x + y;
}
}
Al constreñir el tipo genérico T a INumber, se asegura que el tipo soporte operaciones numéricas.
Uso de genéricos con diferentes tipos numéricos
Tim (33:55) amplía la clase MathOperations para demostrar cómo se pueden utilizar los genéricos con diferentes tipos numéricos, como dobles y decimales.
Tim muestra cómo crear instancias de MathOperations para enteros y dobles:
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8
Esto demuestra la flexibilidad de los genéricos, que permiten manejar sin problemas diferentes tipos numéricos dentro de la misma clase.
Manejo de diferentes tipos numéricos
Tim subraya la importancia de la seguridad de tipos mostrando que no se pueden mezclar tipos numéricos diferentes. Por ejemplo, intentar añadir un doble a un entero dará lugar a un error de compilación.
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));
Evitar la sobrecarga de la conversión tipográfica
Tim explica las ventajas de utilizar genéricos para evitar la sobrecarga asociada a la conversión de tipos. Por ejemplo, la conversión de números enteros a dobles para operaciones matemáticas y su posterior conversión a números enteros puede resultar costosa. El uso de genéricos permite realizar operaciones directas sobre los tipos nativos, preservando el rendimiento y la precisión.
Los genéricos en la práctica
Tim aconseja precaución a la hora de utilizar genéricos, recomendando a los desarrolladores que los utilicen de forma adecuada y eviten su uso excesivo. Destaca las ventajas de los genéricos, como la seguridad de tipos, la reducción de boxing y unboxing, la comprobación en tiempo de compilación y la mejora de la legibilidad del código.
También señala que los genéricos son prevalentes en colecciones como List y Dictionary<TKey, TValue>, así como en marcos de registro que pueden manejar varios tipos sin necesidad de conocer los detalles por adelantado.
Conclusión
La detallada exploración de Tim Corey de los genéricos avanzados en C# ofrece una valiosa perspectiva de sus aplicaciones y ventajas prácticas. Si quieres profundizar en el conocimiento de los genéricos y ver ejemplos reales en acción, no dejes de ver el detallado vídeo de Tim Corey sobre C# Generics. Sus claras explicaciones y demostraciones prácticas le ayudarán a comprender plenamente los conceptos y a aplicarlos eficazmente en sus propios proyectos.
