Tutorial: Exploración de la característica de C# 11: miembros virtuales estáticos en interfaces

C# 11 y .NET 7 incluyen miembros virtuales estáticos en interfaces. Esta característica permite definir interfaces que incluyan operadores sobrecargados u otros miembros estáticos. Una vez que haya definido interfaces con miembros estáticos, puede usar esas interfaces como restricciones para crear tipos genéricos que usen operadores u otros métodos estáticos. Incluso si no crea interfaces con operadores sobrecargados, es probable que se beneficie de esta característica y de las clases matemáticas genéricas habilitadas por la actualización de idioma.

En este tutorial, aprenderá a:

  • Definir interfaces con miembros estáticos.
  • Use interfaces para definir clases que implementan interfaces con operadores definidos.
  • Cree algoritmos genéricos que se basen en métodos de interfaz estática.

Prerrequisitos

Deberá configurar la máquina para ejecutar .NET 7, que admite C# 11. El compilador de C# 11 está disponible a partir de Visual Studio 2022, versión 17.3 o el SDK de .NET 7.

Métodos de interfaz abstracta estática

Comencemos con un ejemplo. El método siguiente devuelve el punto medio de dos double números:

public static double MidPoint(double left, double right) =>
    (left + right) / (2.0);

La misma lógica funcionaría para cualquier tipo numérico: int, short, long, floatdecimalo cualquier tipo que represente un número. Debe tener una manera de usar los + operadores y / y para definir un valor para 2. Puede usar la System.Numerics.INumber<TSelf> interfaz para escribir el método anterior como método genérico siguiente:

public static T MidPoint<T>(T left, T right)
    where T : INumber<T> => (left + right) / T.CreateChecked(2);  // note: the addition of left and right may overflow here; it's just for demonstration purposes

Cualquier tipo que implemente la INumber<TSelf> interfaz debe incluir una definición para operator +y para operator /. El denominador se define mediante T.CreateChecked(2) para crear el valor 2 de cualquier tipo numérico, lo que obliga al denominador a ser el mismo tipo que los dos parámetros. INumberBase<TSelf>.CreateChecked<TOther>(TOther) crea una instancia del tipo a partir del valor especificado y produce un OverflowException si el valor está fuera del intervalo que se puede representar. (Esta implementación tiene la posibilidad de desbordamiento si left y right son valores suficientes grandes. Hay algoritmos alternativos que pueden evitar este posible problema).

Los miembros abstractos estáticos se definen en una interfaz con una sintaxis conocida: se agregan los static modificadores y abstract a cualquier miembro estático que no proporcione una implementación. En el ejemplo siguiente se define una IGetNext<T> interfaz que se puede aplicar a cualquier tipo que invalide operator ++:

public interface IGetNext<T> where T : IGetNext<T>
{
    static abstract T operator ++(T other);
}

La restricción que implementa IGetNext<T> el argumento type, T, garantiza que la firma del operador incluya el tipo contenedor o su argumento type. Muchos operadores aplican que sus parámetros deben coincidir con el tipo o ser el parámetro de tipo restringido para implementar el tipo contenedor. Sin esta restricción, el ++ operador no se pudo definir en la IGetNext<T> interfaz .

Puede crear una estructura que cree una cadena de caracteres "A", donde cada incremento agrega otro carácter a la cadena mediante el código siguiente:

public struct RepeatSequence : IGetNext<RepeatSequence>
{
    private const char Ch = 'A';
    public string Text = new string(Ch, 1);

    public RepeatSequence() {}

    public static RepeatSequence operator ++(RepeatSequence other)
        => other with { Text = other.Text + Ch };

    public override string ToString() => Text;
}

Por lo general, puede crear cualquier algoritmo en el que quiera definir ++ para que "genere el siguiente valor de este tipo". El uso de esta interfaz genera código y resultados claros:

var str = new RepeatSequence();

for (int i = 0; i < 10; i++)
    Console.WriteLine(str++);

En el ejemplo anterior se genera la siguiente salida:

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

En este pequeño ejemplo se muestra la motivación de esta característica. Puede usar la sintaxis natural para operadores, valores constantes y otras operaciones estáticas. Puede explorar estas técnicas al crear varios tipos que dependen de miembros estáticos, incluidos los operadores sobrecargados. Defina las interfaces que coincidan con las funcionalidades de los tipos y, a continuación, declare la compatibilidad de esos tipos con la nueva interfaz.

Matemáticas genéricas

El escenario motivador para permitir métodos estáticos, incluidos los operadores, en interfaces es admitir algoritmos matemáticos genéricos . La biblioteca de clases base de .NET 7 contiene definiciones de interfaz para muchos operadores aritméticos y interfaces derivadas que combinan muchos operadores aritméticos en una INumber<T> interfaz. Vamos a aplicar esos tipos para crear un Point<T> registro que pueda usar cualquier tipo numérico para T. Algunos pueden mover XOffset el punto y YOffset usar el + operador .

Empiece por crear una nueva aplicación de consola, ya sea mediante dotnet new o Visual Studio. Establezca la versión del lenguaje C# en "versión preliminar", que habilita las características en versión preliminar de C# 11. Agregue el siguiente elemento al archivo csproj dentro de un <PropertyGroup> elemento :

<LangVersion>preview</LangVersion>

Nota

Este elemento no se puede establecer mediante la interfaz de usuario de Visual Studio. Debe editar el archivo del proyecto directamente.

La interfaz pública de Translation<T> y Point<T> debe tener un aspecto similar al código siguiente:

// Note: Not complete. This won't compile yet.
public record Translation<T>(T XOffset, T YOffset);

public record Point<T>(T X, T Y)
{
    public static Point<T> operator +(Point<T> left, Translation<T> right);
}

Use el record tipo para los Translation<T> tipos y Point<T> : ambos almacenan dos valores y representan el almacenamiento de datos en lugar de un comportamiento sofisticado. La implementación de operator + tendría el siguiente aspecto:

public static Point<T> operator +(Point<T> left, Translation<T> right) =>
    left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };

Para que el código anterior se compile, deberá declarar que T admite la IAdditionOperators<TSelf, TOther, TResult> interfaz . Esa interfaz incluye el operator + método estático. Declara tres parámetros de tipo: uno para el operando izquierdo, otro para el operando derecho y otro para el resultado. Algunos tipos implementan + para diferentes operandos y tipos de resultado. Agregue una declaración que el argumento type implemente TIAdditionOperators<T, T, T>:

public record Point<T>(T X, T Y) where T : IAdditionOperators<T, T, T>

Después de agregar esa restricción, la Point<T> clase puede usar para + su operador de suma. Agregue la misma restricción en la Translation<T> declaración:

public record Translation<T>(T XOffset, T YOffset) where T : IAdditionOperators<T, T, T>;

La IAdditionOperators<T, T, T> restricción impide que un desarrollador use la clase para crear un Translation mediante un tipo que no cumpla la restricción para la adición a un punto. Ha agregado las restricciones necesarias al parámetro de tipo para Translation<T> y Point<T> , por tanto, este código funciona. Puede probar agregando código como el siguiente en las declaraciones de Translation y Point en el archivo Program.cs :

var pt = new Point<int>(3, 4);

var translate = new Translation<int>(5, 10);

var final = pt + translate;

Console.WriteLine(pt);
Console.WriteLine(translate);
Console.WriteLine(final);

Puede hacer que este código sea más reutilizable declarando que estos tipos implementan las interfaces aritméticas adecuadas. El primer cambio que se va a realizar es declarar que Point<T, T> implementa la IAdditionOperators<Point<T>, Translation<T>, Point<T>> interfaz . El Point tipo usa diferentes tipos para operandos y el resultado. El Point tipo ya implementa un operator + con esa firma, por lo que agregar la interfaz a la declaración es todo lo que necesita:

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>

Por último, al realizar la adición, resulta útil tener una propiedad que defina el valor de identidad aditivo para ese tipo. Hay una nueva interfaz para esa característica: IAdditiveIdentity<TSelf,TResult>. Una traducción de {0, 0} es la identidad de adición: el punto resultante es el mismo que el operando izquierdo. La IAdditiveIdentity<TSelf, TResult> interfaz define una propiedad readonly, AdditiveIdentity, que devuelve el valor de identidad. Necesita Translation<T> algunos cambios para implementar esta interfaz:

using System.Numerics;

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Translation<T> AdditiveIdentity =>
        new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}

Hay algunos cambios aquí, así que vamos a recorrerlos uno por uno. En primer lugar, declara que el Translation tipo implementa la IAdditiveIdentity interfaz :

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>

A continuación, podría intentar implementar el miembro de interfaz tal y como se muestra en el código siguiente:

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: 0, YOffset: 0);

El código anterior no se compilará, ya que 0 depende del tipo . La respuesta: Use IAdditiveIdentity<T>.AdditiveIdentity para 0. Este cambio significa que las restricciones deben incluir ahora que T implementa IAdditiveIdentity<T>. Esto da como resultado la siguiente implementación:

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);

Ahora que ha agregado esa restricción en Translation<T>, debe agregar la misma restricción a Point<T>:

using System.Numerics;

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Point<T> operator +(Point<T> left, Translation<T> right) =>
        left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}

Este ejemplo le ha dado un vistazo a cómo las interfaces de redacción matemática genérica. Ha aprendido a:

  • Escriba un método que dependa de la INumber<T> interfaz para que se pueda usar con cualquier tipo numérico.
  • Cree un tipo que se base en las interfaces de adición para implementar un tipo que solo admita una operación matemática. Ese tipo declara su compatibilidad con esas mismas interfaces para que se pueda componer de otras maneras. Los algoritmos se escriben con la sintaxis más natural de los operadores matemáticos.

Experimente con estas características y registre los comentarios. Puede usar el elemento de menú Enviar comentarios en Visual Studio o crear un nuevo problema en el repositorio roslyn en GitHub. Compile algoritmos genéricos que funcionen con cualquier tipo numérico. Compile algoritmos con estas interfaces en las que el argumento type solo pueda implementar un subconjunto de funcionalidades de tipo numérico. Aunque no cree nuevas interfaces que usen estas funcionalidades, puede experimentar con ellas en los algoritmos.

Vea también