Matemática genérica

O .NET 7 apresenta novas interfaces genéricas relacionadas à matemática à biblioteca de classes base. A disponibilidade dessas interfaces significa que você pode restringir um parâmetro de tipo de um método ou tipo genérico para ser "semelhante a um número". Além disso, o C# 11 e posterior permite definir membros da interface static virtual. Como os operadores devem ser declarados como static, esse novo recurso de C# permite que os operadores sejam declarados nas novas interfaces para tipos semelhantes a números.

Juntas, essas inovações permitem executar operações matemáticas genericamente, ou seja, sem precisar saber o tipo exato com o qual você está trabalhando. Por exemplo, se você quisesse escrever um método que adicionasse dois números, anteriormente você precisava adicionar uma sobrecarga do método para cada tipo (por exemplo, static int Add(int first, int second) e static float Add(float first, float second)). Agora você pode escrever um único método genérico, em que o parâmetro de tipo é restrito a ser um tipo semelhante a um número. Por exemplo:

static T Add<T>(T left, T right)
    where T : INumber<T>
{
    return left + right;
}

Nesse método, o parâmetro de tipo T é restrito a ser um tipo que implementa a nova interface INumber<TSelf>. INumber<TSelf> implementa a interface IAdditionOperators<TSelf,TOther,TResult>, que contém o operador +. Isso permite que o método adicione genericamente os dois números. O método pode ser usado com qualquer um dos tipos numéricos internos do NET, pois todos eles foram atualizados para implementar INumber<TSelf> no .NET 7.

Os autores da biblioteca se beneficiarão mais das interfaces matemáticas genéricas, pois podem simplificar sua base de código removendo sobrecargas "redundantes". Outros desenvolvedores se beneficiarão indiretamente, pois as APIs que consomem podem começar a dar suporte a mais tipos.

As interfaces

As interfaces foram projetadas para serem refinadas o suficiente para que os usuários possam definir as próprias interfaces sobre elas, além de serem granulares o suficiente para serem fáceis de consumir. Nessa medida, há algumas interfaces numéricas principais com as quais a maioria dos usuários interagirá, como INumber<TSelf> e IBinaryInteger<TSelf>. As interfaces mais refinadas, como IAdditionOperators<TSelf,TOther,TResult> e ITrigonometricFunctions<TSelf>, dão suporte a esses tipos e estão disponíveis para desenvolvedores que definem as próprias interfaces numéricas específicas do domínio.

Interfaces numéricas

Esta seção descreve as interfaces em System.Numerics que descrevem tipos semelhantes a números e a funcionalidade disponível para eles.

Nome da interface Descrição
IBinaryFloatingPointIeee754<TSelf> Expõe APIs comuns aos tipos de ponto flutuante binário1 que implementam o padrão IEEE 754.
IBinaryInteger<TSelf> Expõe APIs comuns a inteiros binários2.
IBinaryNumber<TSelf> Expõe APIs comuns a números binários.
IFloatingPoint<TSelf> Expõe APIs comuns a tipos de ponto flutuante.
IFloatingPointIeee754<TSelf> Expõe APIs comuns aos tipos de ponto flutuante que implementam o padrão IEEE 754.
INumber<TSelf> Expõe APIs comuns a tipos de número comparáveis (efetivamente, o domínio numérico "real").
INumberBase<TSelf> Expõe APIs comuns a todos os tipos de número (efetivamente, o domínio numérico "complexo").
ISignedNumber<TSelf> Expõe APIs comuns a todos os tipos de número com sinal (como o conceito de NegativeOne).
IUnsignedNumber<TSelf> Expõe APIs comuns a todos os tipos de número sem sinal.
IAdditiveIdentity<TSelf,TResult> Expõe o conceito de (x + T.AdditiveIdentity) == x.
IMinMaxValue<TSelf> Expõe o conceito de T.MinValue e T.MaxValue.
IMultiplicativeIdentity<TSelf,TResult> Expõe o conceito de (x * T.MultiplicativeIdentity) == x.

1Os tipos de ponto flutuante binários são Double (double), Half e Single (float).

2Os tipos inteiros binários são Byte (byte), Int16 (short), Int32 (int), Int64 (long), Int128, IntPtr (nint), SByte (sbyte), UInt16 (ushort), UInt32 (uint), UInt64 (ulong), UInt128 e UIntPtr (nuint).

A interface que você provavelmente usará diretamente é INumber<TSelf>, que corresponde aproximadamente a um número real. Se um tipo implementar essa interface, isso significa que um valor tem um sinal (isso inclui tipos unsigned, que são considerados positivos) e pode ser comparado a outros valores do mesmo tipo. INumberBase<TSelf> confere conceitos mais avançados, como números complexos e imaginários, por exemplo, a raiz quadrada de um número negativo. Outras interfaces, como IFloatingPointIeee754<TSelf>, foram criadas porque nem todas as operações fazem sentido para todos os tipos de número — por exemplo, calcular o piso de um número só faz sentido para tipos de ponto flutuante. Na biblioteca de classes base do .NET, o tipo de ponto flutuante Double implementa IFloatingPointIeee754<TSelf>, mas Int32 não implementa.

Várias das interfaces também são implementadas por vários outros tipos, incluindo Char, DateOnly, DateTime, DateTimeOffset, Decimal, Guid, TimeOnly e TimeSpan.

A tabela a seguir mostra algumas das APIs principais expostas por cada interface.

Interface Nome da API Descrição
IBinaryInteger<TSelf> DivRem Calcula o quociente e o resto simultaneamente.
LeadingZeroCount Conta o número de bits zero à esquerda na representação binária.
PopCount Conta o número de bits definidos na representação binária.
RotateLeft Gira bits para a esquerda, às vezes também chamado de deslocamento circular à esquerda.
RotateRight Gira bits para a direita, às vezes também chamado de deslocamento circular para a direita.
TrailingZeroCount Conta o número de bits zero à direita na representação binária.
IFloatingPoint<TSelf> Ceiling Arredonda o valor para o infinito positivo. +4,5 torna-se +5 e -4,5 torna-se -4.
Floor Arredonda o valor para o infinito negativo. +4,5 torna-se +4 e -4,5 torna-se -5.
Round Arredonda o valor usando o modo de arredondamento especificado.
Truncate Arredonda o valor para zero. +4,5 torna-se +4 e -4,5 torna-se -4.
IFloatingPointIeee754<TSelf> E Obtém um valor que representa o número de Euler para o tipo.
Epsilon Obtém o menor valor representável que é maior que zero para o tipo.
NaN Obtém um valor que representa NaN para o tipo.
NegativeInfinity Obtém um valor que representa -Infinity para o tipo.
NegativeZero Obtém um valor que representa -Zero para o tipo.
Pi Obtém um valor que representa Pi para o tipo.
PositiveInfinity Obtém um valor que representa +Infinity para o tipo.
Tau Obtém um valor que representa Tau (2 * Pi) para o tipo.
(Outros) (Implementa o conjunto completo de interfaces listadas em Interfaces de função.)
INumber<TSelf> Clamp Restringe um valor a nada mais e nada menos que o valor mínimo e máximo especificados.
CopySign Define o sinal de um valor especificado como o mesmo que de outro valor especificado.
Max Retorna o maior de dois valores, retornando NaN se uma das entradas for NaN.
MaxNumber Retorna o maior de dois valores, retornando o número se uma entrada for NaN.
Min Retorna o menor de dois valores, retornando NaN se uma das entradas for NaN.
MinNumber Retorna o menor de dois valores, retornando o número se uma entrada for NaN.
Sign Retorna -1 para valores negativos, 0 para zero e +1 para valores positivos.
INumberBase<TSelf> One Obtém o valor 1 para o tipo.
Radix Obtém a base para o tipo. Int32 retorna 2. Decimal retorna 10.
Zero Obtém o valor 0 para o tipo.
CreateChecked Cria um valor, gerando um OverflowException se a entrada não puder se encaixar.1
CreateSaturating Cria um valor, fixando a T.MinValue ou T.MaxValue se a entrada não puder se encaixar.1
CreateTruncating Cria um valor com base em outro valor, encapsulando se a entrada não puder se encaixar. 1
IsComplexNumber Retornará true se o valor tiver uma parte real diferente de zero e uma parte imaginária diferente de zero.
IsEvenInteger Retornará true se um valor for um inteiro par. 2.0 retorna true e 2.2 retorna false.
IsFinite Retornará true se o valor não for infinito e não for NaN.
IsImaginaryNumber Retornará true se o valor tiver uma parte real zero. Isso significa que 0 é imaginário e 1 + 1i não é.
IsInfinity Retornará true se o valor representar infinito.
IsInteger Retornará true se um valor for um inteiro. 2.0 e 3.0 retornam true e 2.2 e 3.1 retornam false.
IsNaN Retornará true se o valor representar NaN.
IsNegative Retornará true se o valor for negativo. Isso inclui -0.0.
IsPositive Retornará true se o valor for positivo. Isso inclui 0 e +0.0.
IsRealNumber Retornará true se o valor tiver uma parte imaginária zero. Isso significa que 0 é real, assim como todos os tipos INumber<T>.
IsZero Retornará true se o valor representar zero. Isso inclui 0, +0.0 e -0.0.
MaxMagnitude Retorna o valor com um valor absoluto maior, retornando NaN se uma das entradas for NaN.
MaxMagnitudeNumber Retorna o valor com um valor absoluto maior, retornando o número se uma entrada for NaN.
MinMagnitude Retorna o valor com um valor absoluto menor, retornando NaN se uma das entradas for NaN.
MinMagnitudeNumber Retorna o valor com um valor absoluto menor, retornando o número se uma entrada for NaN.
ISignedNumber<TSelf> NegativeOne Obtém o valor -1 para o tipo.

1Para ajudar a entender o comportamento dos três métodos de Create*, considere os exemplos a seguir.

Exemplo quando dado um valor muito grande:

  • byte.CreateChecked(384) gerará um OverflowException.
  • byte.CreateSaturating(384) retorna 255 porque 384 é maior que Byte.MaxValue (que é 255).
  • byte.CreateTruncating(384) retorna 128 porque leva os 8 bits mais baixos (384 tem uma representação hexadecimal de 0x0180, e os 8 bits mais baixos são 0x80, que é 128).

Exemplo quando dado um valor muito pequeno:

  • byte.CreateChecked(-384) gerará um OverflowException.
  • byte.CreateSaturating(-384) retorna 0 porque -384 é menor que Byte.MinValue (que é 0).
  • byte.CreateTruncating(-384) retorna 128 porque leva os 8 bits mais baixos (384 tem uma representação hexadecimal de 0xFE80, e os 8 bits mais baixos são 0x80, que é 128).

Os métodos Create* também têm algumas considerações especiais para tipos de ponto flutuante IEEE 754, como float e double, pois eles têm os valores especiais PositiveInfinity, NegativeInfinity e NaN. Todas as três APIs Create* se comportam como CreateSaturating. Além disso, enquanto MinValue e MaxValue representam o maior número "normal" negativo/positivo, os valores mínimo e máximo reais são NegativeInfinity e PositiveInfinity, portanto, eles se prendem a esses valores.

Interfaces de operador

As interfaces do operador correspondem aos vários operadores disponíveis para a linguagem C#.

  • Eles não emparelham explicitamente operações como multiplicação e divisão, pois isso não é correto para todos os tipos. Por exemplo, Matrix4x4 * Matrix4x4 é válido, mas Matrix4x4 / Matrix4x4 não é válido.
  • Normalmente, eles permitem que os tipos de entrada e de resultado sejam diferentes para dar suporte a cenários como dividir dois inteiros para obter um double, por exemplo, 3 / 2 = 1.5, ou calcular a média de um conjunto de inteiros.
Nome da interface Operadores definidos
IAdditionOperators<TSelf,TOther,TResult> x + y
IBitwiseOperators<TSelf,TOther,TResult> x & y, "x | y", x ^ y e ~x
IComparisonOperators<TSelf,TOther,TResult> x < y, x > y, x <= y e x >= y
IDecrementOperators<TSelf> --x e x--
IDivisionOperators<TSelf,TOther,TResult> x / y
IEqualityOperators<TSelf,TOther,TResult> x == y e x != y
IIncrementOperators<TSelf> ++x e x++
IModulusOperators<TSelf,TOther,TResult> x % y
IMultiplyOperators<TSelf,TOther,TResult> x * y
IShiftOperators<TSelf,TOther,TResult> x << y e x >> y
ISubtractionOperators<TSelf,TOther,TResult> x - y
IUnaryNegationOperators<TSelf,TResult> -x
IUnaryPlusOperators<TSelf,TResult> +x

Observação

Algumas das interfaces definem um operador verificado, além de um operador não verificado regular. Os operadores verificados são chamados em contextos verificados e permitem que um tipo definido pelo usuário defina o comportamento de estouro. Se você implementar um operador verificado, por exemplo, CheckedSubtraction(TSelf, TOther), também deverá implementar o operador não verificado, por exemplo, Subtraction(TSelf, TOther).

Interfaces de função

As interfaces de função definem APIs matemáticas comuns que se aplicam mais amplamente do que a uma interface numérica específica. Essas interfaces são implementadas por IFloatingPointIeee754<TSelf> e podem ser implementadas por outros tipos relevantes no futuro.

Nome da interface Descrição
IExponentialFunctions<TSelf> Expõe funções exponenciais que dão suporte a e^x, e^x - 1, 2^x, 2^x - 1, 10^x e 10^x - 1.
IHyperbolicFunctions<TSelf> Expõe funções hiperbólicas que dão suporte a acosh(x), asinh(x), atanh(x), cosh(x), sinh(x) e tanh(x).
ILogarithmicFunctions<TSelf> Expõe funções logarítmicas com suporte a ln(x), ln(x + 1), log2(x), log2(x + 1), log10(x) e log10(x + 1).
IPowerFunctions<TSelf> Expõe funções de potência que dão suporte a x^y.
IRootFunctions<TSelf> Expõe funções raiz que dão suporte a cbrt(x) e sqrt(x).
ITrigonometricFunctions<TSelf> Expõe funções trigonométricas que dão suporte a acos(x), asin(x), atan(x), cos(x), sin(x) e tan(x).

Interfaces de análise e formatação

Análise e formatação são conceitos fundamentais na programação. Eles são comumente usados ao converter a entrada do usuário em um determinado tipo ou exibir um tipo para o usuário. Essas interfaces estão no namespace System.

Nome da interface Descrição
IParsable<TSelf> Expõe o suporte para T.Parse(string, IFormatProvider) e T.TryParse(string, IFormatProvider, out TSelf).
ISpanParsable<TSelf> Expõe o suporte para T.Parse(ReadOnlySpan<char>, IFormatProvider) e T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf).
IFormattable1 Expõe o suporte para value.ToString(string, IFormatProvider).
ISpanFormattable1 Expõe o suporte para value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider).

1 Essa interface não é nova nem genérica. No entanto, ela é implementada por todos os tipos de número e representa a operação inversa de IParsable.

Por exemplo, o programa a seguir usa dois números como entrada, lendo-os do console usando um método genérico em que o parâmetro de tipo é restrito a ser IParsable<TSelf>. Ele calcula a média usando um método genérico em que os parâmetros de tipo para os valores de entrada e resultado são restritos a ser INumber<TSelf> e, em seguida, exibe o resultado para o console.

using System.Globalization;
using System.Numerics;

static TResult Average<T, TResult>(T first, T second)
    where T : INumber<T>
    where TResult : INumber<TResult>
{
    return TResult.CreateChecked( (first + second) / T.CreateChecked(2) );
}

static T ParseInvariant<T>(string s)
    where T : IParsable<T>
{
    return T.Parse(s, CultureInfo.InvariantCulture);
}

Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());

Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());

Console.WriteLine($"Result: {Average<float, float>(left, right)}");

/* This code displays output similar to:

First number: 5.0
Second number: 6
Result: 5.5
*/

Confira também