Tutorial: Explore o recurso C# 11 - membros virtuais estáticos em interfaces
C# 11 e .NET 7 incluem membros virtuais estáticos em interfaces. Esse recurso permite definir interfaces que incluem operadores sobrecarregados ou outros membros estáticos. Depois de definir interfaces com membros estáticos, você pode usar essas interfaces como restrições para criar tipos genéricos que usam operadores ou outros métodos estáticos . Mesmo que você não crie interfaces com operadores sobrecarregados, provavelmente se beneficiará desse recurso e das classes de matemática genéricas habilitadas pela atualização de idioma.
Neste tutorial, irá aprender a:
- Defina interfaces com membros estáticos.
- Use interfaces para definir classes que implementam interfaces com operadores definidos.
- Crie algoritmos genéricos que dependem de métodos de interface estática.
Pré-requisitos
Você precisará configurar sua máquina para executar o .NET 7, que suporta C# 11. O compilador C# 11 está disponível a partir do Visual Studio 2022, versão 17.3 ou do SDK do .NET 7.
Métodos de interface abstrata estática
Comecemos por um exemplo. O método a seguir retorna 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
, float
decimal
, ou qualquer tipo que represente um número. Você precisa ter uma maneira de usar os +
operadores e /
e definir um valor para 2
. Você 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
para qualquer tipo numérico, o que força o denominador a ser do mesmo tipo que os 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 o potencial de estouro se left
e right
são valores grandes o suficiente. Existem algoritmos alternativos que podem evitar esse problema potencial.)
Você define membros abstratos estáticos em uma interface usando sintaxe familiar: você adiciona os static
modificadores e abstract
a qualquer membro estático que não forneça uma implementação. O exemplo a seguir define uma IGetNext<T>
interface que pode ser aplicada a qualquer tipo que substitua operator ++
:
public interface IGetNext<T> where T : IGetNext<T>
{
static abstract T operator ++(T other);
}
A restrição que o argumento type, T
, implementa IGetNext<T>
garante que a assinatura do operador inclua o tipo que contém ou seu argumento type. Muitos operadores impõem que seus parâmetros devem corresponder ao tipo, ou ser o parâmetro de tipo restrito para implementar o tipo que contém. Sem essa restrição, o ++
operador não poderia ser definido na IGetNext<T>
interface.
Você pode criar uma estrutura que cria uma cadeia de caracteres 'A' onde cada incremento adiciona outro caractere à cadeia de caracteres 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, você pode construir qualquer algoritmo onde você queira definir ++
como "produzir o próximo valor desse tipo". A utilização desta interface produz um código e resultados claros:
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 este recurso. Você pode usar sintaxe natural para operadores, valores constantes e outras operações estáticas. Você pode explorar essas técnicas ao criar vários tipos que dependem de membros estáticos, incluindo operadores sobrecarregados. Defina as interfaces que correspondem aos recursos dos seus tipos e, em seguida, 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 de classes base do .NET 7 contém definições de interface para muitos operadores aritméticos e interfaces derivadas que combinam muitos operadores aritméticos em uma INumber<T>
interface. Vamos aplicar esses tipos para criar um Point<T>
registro que possa usar qualquer tipo numérico para T
. O ponto pode ser movido por alguns XOffset
e YOffset
usando o +
operador.
Comece criando um novo aplicativo de console, usando ou o dotnet new
Visual Studio.
A interface pública para o Translation<T>
e Point<T>
deve ser semelhante 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);
}
Você usa o record
tipo para os Translation<T>
tipos e Point<T>
: ambos armazenam dois valores e representam armazenamento de dados em vez de comportamento sofisticado. A implementação do operator +
seria semelhante ao 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 que o código anterior seja compilado, você precisará declarar que T
suporta a IAdditionOperators<TSelf, TOther, TResult>
interface. Essa interface inclui o operator +
método estático. Ele 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 operando e resultados. Adicione uma declaração que o argumento type, T
implementa IAdditionOperators<T, T, T>
:
public record Point<T>(T X, T Y) where T : IAdditionOperators<T, T, T>
Depois de adicionar essa restrição, sua Point<T>
classe pode usar o +
para seu 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 que um desenvolvedor usando sua classe crie um Translation
usando um tipo que não atenda à restrição para a adição a um ponto. Você adicionou as restrições necessárias ao parâmetro type para Translation<T>
e Point<T>
assim esse código funciona. Você pode testar adicionando código como o seguinte acima as declarações de e em seu arquivo Program.cs:Translation
Point
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);
Você pode tornar esse código mais reutilizável declarando que esses 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 faz uso de diferentes tipos para operandos e o resultado. O Point
tipo já implementa um operator +
com essa assinatura, portanto, adicionar a interface à declaração é tudo o que você precisa:
public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
where T : IAdditionOperators<T, T, T>
Finalmente, quando você estiver executando a adição, é útil ter uma propriedade que defina o valor de identidade aditiva para esse tipo. Há uma nova interface para esse recurso: IAdditiveIdentity<TSelf,TResult>. Uma tradução de {0, 0}
é a identidade aditiva: O ponto resultante é o mesmo que o operando esquerdo. A IAdditiveIdentity<TSelf, TResult>
interface define uma propriedade somente leitura, AdditiveIdentity
, que retorna o valor de identidade. São Translation<T>
necessárias 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 mudanças aqui, então vamos percorrê-las uma a uma. Primeiro, você declara que o Translation
tipo implementa a IAdditiveIdentity
interface:
public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
Em seguida, você pode tentar implementar o membro da interface, conforme mostrado no código a seguir:
public static Translation<T> AdditiveIdentity =>
new Translation<T>(XOffset: 0, YOffset: 0);
O código anterior não será compilado, porque 0
depende do tipo. A resposta: Use IAdditiveIdentity<T>.AdditiveIdentity
para 0
. Essa alteração significa que suas restrições agora devem 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 você adicionou essa restrição ao Translation<T>
, você precisa 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 uma olhada em como as interfaces para matemática genérica se compõem. Aprendeu a:
- Escreva um método que dependesse da interface para que esse
INumber<T>
método pudesse ser usado com qualquer tipo numérico. - Crie um tipo que dependa das interfaces de adição para implementar um tipo que suporte apenas uma operação matemática. Esse tipo declara seu suporte para essas mesmas interfaces para que possa ser composto de outras maneiras. Os algoritmos são escritos usando a sintaxe mais natural dos operadores matemáticos.
Experimente esses recursos e registre comentários. Você pode usar o item de menu Enviar Comentários no Visual Studio ou criar um novo problema no repositório roslyn no GitHub. Crie algoritmos genéricos que funcionem com qualquer tipo numérico. Crie algoritmos usando essas interfaces onde o argumento type só pode implementar um subconjunto de recursos semelhantes a números. Mesmo que você não crie novas interfaces que usem esses recursos, você pode experimentar usá-los em seus algoritmos.