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 type debe ser un tipo de valor que no acepta valores NULL, que incluye record struct tipos. Para más información sobre los tipos de valor que admiten un valor NULL, consulte Tipos de valor que admiten un valor NULL. Dado que todos los tipos de valor tienen un constructor sin parámetros accesible, declarado o implícito, 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 acepta valores NULL, T debe ser un tipo de referencia que no acepta valores NULL.
where T : class? El argumento de tipo debe ser un tipo de referencia, que acepte o no valores NULL. Esta restricción también se aplica a cualquier clase, interfaz, delegado o tipo de matriz, incluidos los registros.
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, o bien un tipo de valor que no acepta valores NULL.
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 :<nombre de clase base> El argumento de tipo debe ser o derivarse de la clase base especificada. En un contexto que acepta valores NULL, T debe ser un tipo de referencia que no acepta valores NULL derivado de la clase base especificada.
where T :<nombre de clase base>? El argumento de tipo debe ser o derivarse de la clase base especificada. En un contexto que acepta valores NULL, Tpuede ser un tipo que acepta valores NULL o que no acepta valores NULL derivados de la clase base especificada.
where T :<nombre de interfaz> 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 acepta valores NULL, T debe ser un tipo que no acepta valores NULL que implementa la interfaz especificada.
where T :<nombre de interfaz>? 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 acepta valores NULL, Tpuede ser un tipo de referencia que acepta valores NULL, un tipo de referencia que no acepta valores NULL o un tipo de valor. T no puede ser un tipo de valor que acepta valores 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 acepta valores NULL, si U es 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 acepta valores NULL, T puede ser que acepta valores NULL o que no aceptan 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.

Algunas restricciones son mutuamente excluyentes y algunas restricciones deben estar en un orden especificado:

  • Puede aplicar como máximo una de las restricciones struct, class, class?, notnull, y unmanaged. Si proporciona alguna de estas restricciones, debe ser la primera restricción especificada para ese parámetro de tipo.
  • La restricción de clase base, (where T : Base o where T : Base?), no se puede combinar con ninguna de las restricciones struct, class, class?, notnull, o unmanaged.
  • Puede aplicar como máximo una restricción de clase base, de cualquier forma. Si desea admitir el tipo base que acepta valores NULL, use Base?.
  • No se puede asignar un nombre a la forma que no acepta valores NULL y que aceptan valores NULL de una interfaz como restricción.
  • La restricción new() no se puede combinar con las restricciones struct o unmanaged. Si especifica la restricción new(), debe ser la última restricción para ese parámetro de tipo.
  • La restricción default solo se puede aplicar en implementaciones de interfaz explícitas o invalidaciones. No se puede combinar con las restricciones struct o class.

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. Las restricciones se aplican al parámetro de tipo cuando la clase o el método genéricos usan cualquier operación en los miembros genéricos más allá de la asignación simple, lo que incluye llamar a los métodos no admitidos por System.Object. Por ejemplo, la restricción de clase base indica al compilador que solo los objetos de este tipo o derivados de este tipo pueden reemplazar ese argumento 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, System.Collections.Generic.IList<T>, IDisposable, new()
{
    // ...
}

Al aplicar la restricción where T : class, evite los operadores == y != en el parámetro de tipo porque estos operadores solo prueban la identidad de referencia, no para la igualdad de valores. 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 valores, aplique la restricción where T : IEquatable<T> o where T : IComparable<T> e implemente la interfaz en cualquier clase usada 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 se pueden usar porque no hay ninguna garantía de que el argumento de tipo concreto admita estos operadores.
  • Pueden convertirse a y desde System.Object o convertirse explícitamente en cualquier tipo de interfaz.
  • Puede compararlos con NULL. Si se compara un parámetro sin enlazar con null, la comparación siempre devuelve false si el argumento 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

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

La restricción class en un contexto que acepta valores 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. T? se puede 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. T? se puede usar cuando no se aplica ninguna restricción. En ese caso, T? se interpreta como T? para tipos de valor y los 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. El método derivado aplica la restricción default. La restricción defaultno aclara ni la restricción class ni la struct.

Restricción no administrada

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

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

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

Los argumentos de tipo implementan la interfaz declarada

Algunos escenarios requieren que un argumento proporcionado para un parámetro de tipo implemente esa interfaz. Por ejemplo:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static T operator +(T left, T right);
    public abstract static T operator -(T left, T right);
}

Este patrón permite al compilador de C# determinar el tipo contenedor para los operadores sobrecargados, o algún método static virtual o static abstract. Proporciona la sintaxis para que los operadores de suma y resta se puedan definir en un tipo contenedor. Sin esta restricción, los parámetros y argumentos deben declararse como interfaz, en lugar de parámetro de tipo:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    public abstract static IAdditionSubtraction<T> operator -(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);
}

La sintaxis anterior requeriría que los implementadores usaran la implementación de interfaz explícita para esos métodos. Proporcionar la restricción adicional permite a la interfaz definir los operadores en términos de los parámetros de tipo. Los tipos que implementan la interfaz pueden implementar implícitamente los métodos de interfaz.

Consulte también