Restricciones de tipos de parámetros (Guía de programación de C#)
Las restricciones informan al compilador sobre las capacidades que debe tener un argumento de tipo. Sin restricciones, el argumento de tipo puede ser cualquier tipo. El compilador solo puede suponer los miembros de System.Object, que es la clase base fundamental de los tipos .NET. Para más información, vea Por qué usar restricciones. Si el código de cliente usa un tipo que no cumple una restricción, el compilador emite un error. Las restricciones se especifican con la palabra clave contextual where. En la tabla siguiente se muestran los distintos tipos de restricciones:
| Restricción | Descripción |
|---|---|
where T : struct |
El argumento de tipo debe ser un tipo de valor que no acepta valores NULL. Para más información sobre los tipos de valor que admiten un valor NULL, consulte Tipos de valor que admiten un valor NULL. Todos los tipos de valor tienen un constructor sin parámetros accesible, por lo que la restricción struct implica la restricción new() y no se puede combinar con la restricción new(). No puede combinar la restricción struct con la restricción unmanaged. |
where T : class |
El argumento de tipo debe ser un tipo de referencia. Esta restricción se aplica también a cualquier clase, interfaz, delegado o tipo de matriz. En un contexto que admite un valor NULL en C# 8.0 o versiones posteriores, T debe ser un tipo de referencia que no acepte valores NULL. |
where T : class? |
El argumento de tipo debe ser un tipo de referencia, que acepte o no valores NULL. Esta restricción se aplica también a cualquier clase, interfaz, delegado o tipo de matriz. |
where T : notnull |
El argumento de tipo debe ser un tipo que no acepta valores NULL. El argumento puede ser un tipo de referencia que no acepta valores NULL en C# 8.0 o posterior, o bien un tipo de valor que no acepta valores NULL. |
where T : default |
Esta restricción resuelve la ambigüedad cuando es necesario especificar un parámetro de tipo sin restricciones al invalidar un método o proporcionar una implementación de interfaz explícita. La restricción default implica el método base sin la restricción class o struct. Para obtener más información, vea la propuesta de especificación de la restricción default. |
where T : unmanaged |
El argumento de tipo debe ser un tipo no administrado que no acepta valores NULL. La restricción unmanaged implica la restricción struct y no se puede combinar con las restricciones struct ni new(). |
where T : new() |
El argumento de tipo debe tener un constructor sin parámetros público. Cuando se usa conjuntamente con otras restricciones, la restricción new() debe especificarse en último lugar. La restricción new() no se puede combinar con las restricciones struct ni unmanaged. |
where T : <base class name> |
El argumento de tipo debe ser o derivarse de la clase base especificada. En un contexto que admite un valor NULL en C# 8.0 y versiones posteriores, T debe ser un tipo de referencia que no acepta valores NULL derivado de la clase base especificada. |
where T : <base class name>? |
El argumento de tipo debe ser o derivarse de la clase base especificada. En un contexto que admite un valor NULL en C# 8.0 y versiones posteriores, T puede ser un tipo que acepta o no acepta valores NULL derivado de la clase base especificada. |
where T : <interface name> |
El argumento de tipo debe ser o implementar la interfaz especificada. Pueden especificarse varias restricciones de interfaz. La interfaz de restricciones también puede ser genérica. En un contexto que admite un valor NULL en C# 8.0 y versiones posteriores, T debe ser un tipo que no acepta valores NULL que implementa la interfaz especificada. |
where T : <interface name>? |
El argumento de tipo debe ser o implementar la interfaz especificada. Pueden especificarse varias restricciones de interfaz. La interfaz de restricciones también puede ser genérica. En un contexto que admite un valor NULL en C# 8.0, T puede ser un tipo de referencia que admite un valor NULL, un tipo de referencia que no acepta valores NULL o un tipo de valor. T no puede ser un tipo de valor que admite un valor NULL. |
where T : U |
El argumento de tipo proporcionado por T debe ser o se debe derivar del argumento proporcionado para U. En un contexto que admite un valor NULL, si U puede ser un tipo de referencia que no acepta valores NULL, T debe ser un tipo de referencia que no acepta valores NULL. Si U es un tipo de referencia que admite un valor NULL, T puede aceptar valores NULL o no. |
Por qué usar restricciones
Las restricciones especifican las funciones y expectativas de un parámetro de tipo. La declaración de esas restricciones significa que puede usar las operaciones y las llamadas de método del tipo de restricción. Si la clase o el método genérico usa cualquier operación en los miembros genéricos más allá de una asignación simple o una llamada a un método que System.Object no admita, se aplicarán restricciones al parámetro de tipo. Por ejemplo, la restricción de clase base indica al compilador que solo los objetos de este tipo o derivados de este tipo se usarán como argumentos de tipo. Una vez que el compilador tenga esta garantía, puede permitir que los métodos de ese tipo se llamen en la clase genérica. En el ejemplo de código siguiente se muestran las funciones que podemos agregar a la clase GenericList<T> (en Introducción a los genéricos) mediante la aplicación de una restricción de clase base.
public class Employee
{
public Employee(string name, int id) => (Name, ID) = (name, id);
public string Name { get; set; }
public int ID { get; set; }
}
public class GenericList<T> where T : Employee
{
private class Node
{
public Node(T t) => (Next, Data) = (null, t);
public Node Next { get; set; }
public T Data { get; set; }
}
private Node head;
public void AddHead(T t)
{
Node n = new Node(t) { Next = head };
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
La restricción permite que la clase genérica use la propiedad Employee.Name. La restricción especifica que está garantizado que todos los elementos de tipo T sean un objeto Employee u objeto que hereda de Employee.
Pueden aplicarse varias restricciones en el mismo parámetro de tipo, y las propias restricciones pueden ser tipos genéricos, de la manera siguiente:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
Al aplicar la restricción where T : class, evite los operadores == y != en el parámetro de tipo porque estos operadores se probarán solo para la identidad de referencia, no para la igualdad de valor. Este comportamiento se produce incluso si estos operadores están sobrecargados en un tipo que se usa como un argumento. En el código siguiente se ilustra este punto; el resultado es False incluso cuando la clase String sobrecarga al operador ==.
public static void OpEqualsTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s == t);
}
private static void TestStringEquality()
{
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpEqualsTest<string>(s1, s2);
}
El compilador solo sabe que T es un tipo de referencia en tiempo de compilación y debe usar los operadores predeterminados que son válidos para todos los tipos de referencia. Si debe probar la igualdad de valor, la manera recomendada también es aplicar la restricción where T : IEquatable<T> o where T : IComparable<T> e implementar esa interfaz en cualquier clase que se usará para construir la clase genérica.
Restringir varios parámetros
Puede aplicar restricciones a varios parámetros, y varias restricciones a un solo parámetro, como se muestra en el siguiente ejemplo:
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
Parámetros de tipo sin enlazar
Los parámetros de tipo que no tienen restricciones, como T en la clase pública SampleClass<T>{}, se denominan parámetros de tipo sin enlazar. Los parámetros de tipo sin enlazar tienen las reglas siguientes:
- Los operadores
!=y==no pueden usarse porque no existe ninguna garantía de que el argumento de tipo concreto admita estos operadores. - Pueden convertirse a y desde
System.Objecto convertirse explícitamente en cualquier tipo de interfaz. - Puede compararlos con NULL. Si un parámetro sin enlazar se compara con
null, la comparación siempre devolverá False si el argumento de tipo es un tipo de valor.
Parámetros de tipo como restricciones
El uso de un parámetro de tipo genérico como una restricción es útil cuando una función de miembro con su propio parámetro de tipo tiene que restringir ese parámetro al parámetro de tipo del tipo contenedor, como se muestra en el ejemplo siguiente:
public class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}
En el ejemplo anterior, T es una restricción de tipo en el contexto del método Add, y un parámetro de tipo sin enlazar en el contexto de la clase List.
Los parámetros de tipo también pueden usarse como restricciones en definiciones de clase genéricas. El parámetro de tipo debe declararse dentro de los corchetes angulares junto con los demás parámetros de tipo:
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
La utilidad de los parámetros de tipo como restricciones con clases genéricas es limitada, ya que el compilador no puede dar por supuesto nada sobre el parámetro de tipo, excepto que deriva de System.Object. Use parámetros de tipo como restricciones en clases genéricas en escenarios en los que quiere aplicar una relación de herencia entre dos parámetros de tipo.
Restricción notnull
A partir C# 8.0, puede usar la restricción notnull para especificar que el argumento de tipo debe ser un tipo de valor que no acepta valores NULL o un tipo de referencia que no acepta valores NULL. A diferencia de la mayoría de las demás restricciones, si un argumento de tipo infringe la restricción notnull, el compilador genera una advertencia en lugar de un error.
La restricción notnull tiene efecto solo cuando se usa en un contexto que admite un valor NULL. Si agrega la restricción notnull en un contexto en el que se desconocen los valores NULL, el compilador no genera advertencias ni errores para las infracciones de la restricción.
Restricción class
A partir de C# 8.0, la restricción class en un contexto que admite un valor NULL especifica que el argumento de tipo debe ser un tipo de referencia que no acepta valores NULL. En un contexto que admite un valor NULL, cuando un argumento de tipo es un tipo de referencia que admite un valor NULL, el compilador genera una advertencia.
Restricción default
La incorporación de tipos de referencia que aceptan valores NULL complica el uso de T? en un método o tipo genérico. Antes de C# 8, T? solo se podía usar cuando la restricción struct se aplicaba a T. En ese contexto, T? hace referencia al tipo Nullable<T> para T. A partir de C# 8, T? se podría usar con la restricción struct o class, pero una de ellas debe estar presente. Cuando se ha usado la restricción class, T? se refiere al tipo de referencia que acepta valores NULL para T. A partir de C# 9, T? se puede usar cuando no se aplica ninguna restricción. En ese caso, T? tiene la misma interpretación que en C# 8 para tipos de valor y tipos de referencia. Sin embargo, si T es una instancia de Nullable<T>, T? es igual que T. En otras palabras, no se convierte en T??.
Dado que T? ahora se puede usar sin las restricciones class o struct, pueden surgir ambigüedades en invalidaciones o implementaciones de interfaz explícitas. En ambos casos, la invalidación no incluye las restricciones, pero las hereda de la clase base. Cuando la clase base no aplica las restricciones class o struct, las clases derivadas deben especificar de algún modo que una invalidación se aplica al método base sin ninguna restricción. Ahí es cuando el método derivado aplica la restricción default. La restricción default no aclara ni la restricción class ni la struct.
Restricción no administrada
A partir de C# 7.3, puede usar la restricción unmanaged para especificar que el parámetro de tipo debe ser un tipo no administrado que no acepta valores NULL. La restricción unmanaged permite escribir rutinas reutilizables para trabajar con tipos que se pueden manipular como bloques de memoria, como se muestra en el ejemplo siguiente:
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
var size = sizeof(T);
var result = new Byte[size];
Byte* p = (byte*)&argument;
for (var i = 0; i < size; i++)
result[i] = *p++;
return result;
}
El método anterior debe compilarse en un contexto unsafe, ya que usa el operador sizeof en un tipo que se desconoce si es integrado. Sin la restricción unmanaged, el operador sizeof no está disponible.
La restricción unmanaged implica la restricción struct y no se puede combinar con ella. Dado que la restricción struct implica la restricción new(), la restricción unmanaged tampoco se puede combinar con la restricción new().
Restricciones de delegado
También a partir de C# 7.3, puede usar System.Delegate o System.MulticastDelegate como una restricción de clase base. CLR siempre permitía esta restricción, pero el lenguaje C# no la permitía. La restricción System.Delegate permite escribir código que funciona con los delegados en un modo con seguridad de tipos. En el código siguiente se define un método de extensión que combina dos delegados siempre y cuando sean del mismo tipo:
public static TDelegate TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;
Puede usar el método anterior para combinar delegados que sean del mismo tipo:
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");
var combined = first.TypeSafeCombine(second);
combined();
Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);
Si quita la marca de comentario de la última línea, no se compilará. Tanto first como test son tipos de delegado, pero son tipos de delegado distintos.
Restricciones de enumeración
A partir de C# 7.3, también puede especificar el tipo System.Enum como una restricción de clase base. CLR siempre permitía esta restricción, pero el lenguaje C# no la permitía. Los genéricos que usan System.Enum proporcionan programación con seguridad de tipos para almacenar en caché los resultados de usar los métodos estáticos en System.Enum. En el ejemplo siguiente se buscan todos los valores válidos para un tipo de enumeración y, después, se compila un diccionario que asigna esos valores a su representación de cadena.
public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));
foreach (int item in values)
result.Add(item, Enum.GetName(typeof(T), item));
return result;
}
Enum.GetValues y Enum.GetName usan reflexión, lo que tiene consecuencias en el rendimiento. Puede llamar a EnumNamedValues para compilar una recopilación que se almacene en caché y se vuelva a usar, en lugar de repetir las llamadas que requieren reflexión.
Podría usarla como se muestra en el ejemplo siguiente para crear una enumeración y compilar un diccionario con sus nombres y valores:
enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
var map = EnumNamedValues<Rainbow>();
foreach (var pair in map)
Console.WriteLine($"{pair.Key}:\t{pair.Value}");