Partilhar via


Tutorial: Explore membros virtuais estáticos em interfaces

Os membros virtuais estáticos de interface permitem-lhe definir interfaces que incluem operadores sobrecarregados ou outros membros estáticos. Depois de definir interfaces com membros estáticos, pode usar essas interfaces como restrições para criar tipos genéricos que utilizam operadores ou outros métodos estáticos. Mesmo que não crie interfaces com operadores sobrecarregados, provavelmente beneficia desta funcionalidade e das classes genéricas de matemática permitidas pela atualização da linguagem.

Neste tutorial, aprenderás como:

  • Defina interfaces com membros estáticos.
  • Use interfaces para definir classes que implementem interfaces com operadores definidos.
  • Criar algoritmos genéricos que dependam de métodos de interface estática.

Pré-requisitos

Métodos estáticos de interface abstrata

Vamos começar com um exemplo. O método seguinte devolve o ponto médio de dois double números:

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

A mesma lógica funcionaria para qualquer tipo numérico: int, short, long, floatdecimal, ou qualquer tipo que represente um número. É necessário ter uma forma de usar os operadores + e /, e de definir um valor para 2. Pode usar a System.Numerics.INumber<TSelf> interface para escrever o método anterior como o seguinte método genérico:

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

Qualquer tipo que implemente a INumber<TSelf> interface deve incluir uma definição para operator +, e para operator /. O denominador é definido por T.CreateChecked(2) para criar o valor 2 de qualquer tipo numérico, o que força o denominador a ser o mesmo tipo dos dois parâmetros. INumberBase<TSelf>.CreateChecked<TOther>(TOther) cria uma instância do tipo a partir do valor especificado e lança um OverflowException se o valor estiver fora do intervalo representável. (Esta implementação tem potencial para overflow se left e right forem ambos valores suficientemente grandes. Existem algoritmos alternativos que podem evitar este potencial problema.)

Define-se membros abstratos estáticos numa interface usando sintaxe familiar: Adiciona-se os static modificadores e abstract a qualquer membro estático que não forneça uma implementação. O exemplo seguinte define uma interface IGetNext<T> que pode ser aplicada a qualquer tipo que sobrescreva operator ++.

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

A restrição de que o argumento de tipo, T, implementa IGetNext<T> garante que a assinatura do operador inclui o tipo envolvente, ou o seu argumento de tipo. Muitos operadores exigem que os seus parâmetros correspondam ao tipo ou que o parâmetro de tipo esteja restringido a implementar o tipo que os contém. Sem esta restrição, o ++ operador não poderia ser definido na IGetNext<T> interface.

Pode criar uma estrutura que crie uma cadeia de caracteres 'A' onde cada incremento adiciona mais um carácter à cadeia usando o seguinte código:

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

Mais geralmente, pode construir qualquer algoritmo onde queira definir ++ que significa "produzir o próximo valor deste tipo." Usar esta interface produz código claro e resultados:

var str = new RepeatSequence();

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

O exemplo anterior produz a seguinte saída:

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

Este pequeno exemplo demonstra a motivação para esta funcionalidade. Pode usar sintaxe natural para operadores, valores constantes e outras operações estáticas. Pode explorar estas técnicas ao criar múltiplos tipos que dependem de elementos estáticos, incluindo operadores sobrecarregados. Defina as interfaces que correspondam às capacidades dos seus tipos e depois declare o suporte desses tipos para a nova interface.

Matemática genérica

O cenário motivador para permitir métodos estáticos, incluindo operadores, em interfaces é suportar algoritmos matemáticos genéricos . A biblioteca base de classes .NET 7 contém definições de interface para muitos operadores aritméticos e interfaces derivadas que combinam muitos operadores aritméticos numa INumber<T> interface. Vamos aplicar esses tipos para construir um Point<T> registo que possa usar qualquer tipo numérico para T. Podes mover o ponto por alguns XOffset e YOffset usando o + operador.

Comece por criar uma nova aplicação para Consola, seja usando dotnet new ou usando o Visual Studio.

A interface pública para o Translation<T> e Point<T> deve assemelhar-se ao seguinte código:

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

Utilizas o tipo record tanto para os tipos Translation<T> e Point<T>: ambos armazenam dois valores e representam armazenamento de dados em vez de comportamentos sofisticados. A implementação de operator + seria o seguinte código:

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

Para compilar o código anterior, tens de declarar que T suporta a IAdditionOperators<TSelf, TOther, TResult> interface. Essa interface inclui o operator + método estático. Declara três parâmetros de tipo: um para o operando esquerdo, um para o operando direito e um para o resultado. Alguns tipos implementam + para diferentes tipos de operandos e resultados. Adicione uma declaração de que o argumento de tipo T implementa IAdditionOperators<T, T, T>:

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

Depois de adicionares essa restrição, a tua classe Point<T> pode usar o + como operador de adição. Adicione a mesma restrição na Translation<T> declaração:

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

A IAdditionOperators<T, T, T> restrição impede um programador que utiliza a sua classe de criar um Translation utilizando um tipo que não cumpra a restrição para adicionar a um ponto. Adicionaste as restrições necessárias ao parâmetro de tipo para Translation<T> e Point<T> assim este código funciona. Pode testar adicionando um código como o seguinte, acima das declarações de Translation e Point no seu ficheiro 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);

Pode tornar este código mais reutilizável declarando que estes tipos implementam as interfaces aritméticas apropriadas. A primeira alteração a fazer é declarar que Point<T, T> implementa a IAdditionOperators<Point<T>, Translation<T>, Point<T>> interface. O Point tipo utiliza diferentes tipos para operandos e o resultado. O Point tipo já implementa um operator + com essa assinatura, por isso adicionar a interface à declaração é tudo o que precisas:

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

Finalmente, quando se faz adição, é útil ter uma propriedade que defina o valor de identidade aditivo para esse tipo. Há uma nova interface para essa funcionalidade: IAdditiveIdentity<TSelf,TResult>. Uma translação de {0, 0} é a identidade aditiva: O ponto resultante é o mesmo que o operando esquerdo. A IAdditiveIdentity<TSelf, TResult> interface define uma propriedade apenas de leitura, AdditiveIdentity, que devolve o valor de identidade. O Translation<T> precisa de algumas alterações para implementar esta interface.

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

Há algumas alterações aqui, por isso vamos analisá-las uma a uma. Primeiro, declara que o Translation tipo implementa a IAdditiveIdentity interface:

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

A seguir, pode tentar implementar o membro da interface como mostrado no seguinte código:

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

O código anterior não compila, porque 0 depende do tipo. A resposta: Use IAdditiveIdentity<T>.AdditiveIdentity para 0. Essa alteração significa que as suas restrições devem agora incluir que T implementa IAdditiveIdentity<T>. Isso resulta na seguinte implementação:

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

Agora que adicionou essa restrição em Translation<T>, precisa de adicionar a mesma restrição 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 exemplo deu-te uma visão de como as interfaces para matemática genérica se compõem. Você aprendeu a:

  • Escreve um método que dependesse da INumber<T> interface para que esse método pudesse ser usado com qualquer tipo numérico.
  • Constrói um tipo que dependa das interfaces de adição para implementar um tipo que só suporta uma operação matemática. Esse tipo declara o seu suporte para essas mesmas interfaces para poder ser composto de outras formas. Os algoritmos são escritos usando a sintaxe mais natural dos operadores matemáticos.

Experimenta estas funcionalidades e regista feedback. Pode usar o elemento do menu Enviar Feedback no Visual Studio ou criar um novo problema no repositório Roslyn no GitHub. Constrói algoritmos genéricos que funcionem com qualquer tipo numérico. Construir algoritmos usando estas interfaces onde o argumento do tipo apenas implementa um subconjunto de capacidades semelhantes a números. Mesmo que não construas novas interfaces que usem estas capacidades, podes experimentar usá-las nos teus algoritmos.

Consulte também