Compartir vía


Tutorial: Exploración de miembros virtuales estáticos en interfaces

Los miembros virtuales estáticos de interfaz permiten definir interfaces que incluyen operadores sobrecargados u otros miembros estáticos. Una vez definidas las 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 del lenguaje.

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

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 el 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 de que el argumento de tipo T implemente IGetNext<T> garantiza que la firma del operador incluya el tipo contenedor o su argumento de tipo. Muchos operadores imponen que sus parámetros deben coincidir con el tipo, o ser el parámetro de tipo que esté restringido a 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 construir cualquier algoritmo en el que desee definir ++ para que signifique "generar el siguiente valor de este tipo". El uso de esta interfaz produce 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 se basan en 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 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. Puede mover el punto mediante algunos XOffset y YOffset usando el + operador.

Empiece por crear una nueva aplicación de consola, ya sea mediante dotnet new o Visual Studio.

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

Usas el record tipo para los tipos Translation<T> y Point<T>: ya que ambos almacenan dos valores y representan más el almacenamiento de datos que 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, debe 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 distintos tipos de operando y de resultado. Agregue una declaración de que el argumento de tipo, T implementa IAdditionOperators<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 el + 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 anterior a 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. Translation<T> necesita 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, puede intentar implementar el miembro de interfaz 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 compila, 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 agregó 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 dio un vistazo a cómo se componen las interfaces de matemáticas genéricas. 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 comentarios. Puede usar el elemento de menú Enviar comentarios en Visual Studio o crear un nuevo problema en el repositorio roslyn en GitHub. Cree algoritmos genéricos que funcionen con cualquier tipo numérico. Compile algoritmos con estas interfaces en las que el argumento type solo implemente un subconjunto de funcionalidades de tipo numérico. Incluso si no crea nuevas interfaces que usan estas funcionalidades, puede experimentar con ellas en los algoritmos.

Consulte también