Matemáticas genéricas

.NET 7 incorpora nuevas interfaces genéricas relacionadas con las matemáticas a la biblioteca de clases base. La disponibilidad de estas interfaces significa que puede restringir un parámetro de un tipo o un método genérico para que sea "similar a un número". Además, C# 11 y versiones posteriores permite definir miembros de interfaz static virtual. Dado que los operadores deben declararse como static, esta nueva característica de C# permite declarar operadores en las nuevas interfaces para los tipos similares a números.

Juntas, estas innovaciones permiten realizar operaciones matemáticas de forma genérica, es decir, sin tener que conocer el tipo exacto con el que se está trabajando. Por ejemplo, si desea escribir un método que sume dos números, antes tenía que agregar una sobrecarga del método para cada tipo (por ejemplo, static int Add(int first, int second) y static float Add(float first, float second)). Ahora puede escribir un único método genérico, donde el parámetro de tipo está restringido para ser un tipo similar a un número. Por ejemplo:

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

En este método, el parámetro de tipo T está restringido para que sea un tipo que implemente la nueva interfaz INumber<TSelf>. INumber<TSelf> implementa la interfaz IAdditionOperators<TSelf,TOther,TResult>, que contiene el operador +. Esto permite que el método sume los dos números genéricamente. El método se puede usar con cualquiera de los tipos numéricos integrados de .NET, ya que todos se han actualizado para implementar INumber<TSelf> en .NET 7.

Los autores de bibliotecas serán los que más se beneficien de las interfaces de matemáticas genéricas, ya que pueden simplificar su base de código eliminando sobrecargas "redundantes". Otros desarrolladores se beneficiarán indirectamente, ya que las API que consumen pueden empezar a admitir más tipos.

Las interfaces

Las interfaces se han diseñado para ser lo suficientemente específicas como para que los usuarios puedan definir sus propias interfaces a partir de ellas, al tiempo que son lo suficientemente pormenorizadas como para que sean fáciles de consumir. En esa medida, hay algunas interfaces numéricas básicas con las que la mayoría de los usuarios interactuarán, como INumber<TSelf> y IBinaryInteger<TSelf>. Las interfaces más específicas, como IAdditionOperators<TSelf,TOther,TResult> y ITrigonometricFunctions<TSelf>, admiten estos tipos y están disponibles para los desarrolladores que definen sus propias interfaces numéricas específicas del dominio.

Interfaces numéricas

En esta sección se tratan las interfaces de System.Numerics que describen los tipos de tipo numérico y la funcionalidad disponible para ellos.

Nombre de la interfaz Descripción
IBinaryFloatingPointIeee754<TSelf> Expone las API comunes a los tipos de punto flotante binarios1 que implementan el estándar IEEE 754.
IBinaryInteger<TSelf> Expone las API comunes a los enteros binarios2.
IBinaryNumber<TSelf> Expone las API comunes a los números binarios.
IFloatingPoint<TSelf> Expone las API comunes a los tipos de punto flotante.
IFloatingPointIeee754<TSelf> Expone las API comunes a los tipos de punto flotante que implementan el estándar IEEE 754.
INumber<TSelf> Expone las API comunes a los tipos de números comparables (en realidad, el dominio del número "real").
INumberBase<TSelf> Expone las API comunes a todos los tipos de números (en realidad, el dominio del número "complejo").
ISignedNumber<TSelf> Expone las API comunes a todos los tipos de números firmados (por ejemplo, el concepto de NegativeOne).
IUnsignedNumber<TSelf> Expone las API comunes a todos los tipos de números sin firmar.
IAdditiveIdentity<TSelf,TResult> Expone el concepto de (x + T.AdditiveIdentity) == x.
IMinMaxValue<TSelf> Expone el concepto de T.MinValue y T.MaxValue.
IMultiplicativeIdentity<TSelf,TResult> Expone el concepto de (x * T.MultiplicativeIdentity) == x.

1 Los tipos de punto flotante binarios son Double (double), Halfy Single (float).

2 Los tipos enteros binarios son Byte (byte), Int16 (short), Int32 (int), Int64 (long), Int128, IntPtr (nint), SByte (sbyte), UInt16 (ushort), UInt32 (uint), UInt64 (ulong), UInt128 y UIntPtr (nuint).

La interfaz que es más probable que use directamente es INumber<TSelf>, que se corresponde aproximadamente con un número real. Si un tipo implementa esta interfaz, significa que un valor tiene un signo (esto incluye los tipos unsigned, que se consideran positivos) y se puede comparar con otros valores del mismo tipo. INumberBase<TSelf> confiere conceptos más avanzados, como números complejos e imaginarios, por ejemplo, la raíz cuadrada de un número negativo. Se han creado otras interfaces, como IFloatingPointIeee754<TSelf>, porque no todas las operaciones tienen sentido para todos los tipos de números; por ejemplo, calcular el suelo de un número solo tiene sentido para los tipos de punto flotante. En la biblioteca de clases base de .NET, el tipo Double de punto flotante implementa IFloatingPointIeee754<TSelf>, pero Int32 no.

Varias de las interfaces también se implementan con otros tipos, como Char, DateOnly, DateTime, DateTimeOffset, Decimal, Guid, TimeOnly y TimeSpan.

En la tabla siguiente se muestran algunas de las principales API que expone cada interfaz.

Interfaz Nombre de la API Descripción
IBinaryInteger<TSelf> DivRem Calcula el cociente y el resto simultáneamente.
LeadingZeroCount Cuenta el número de bits cero iniciales en la representación binaria.
PopCount Cuenta el número de bits establecidos en la representación binaria.
RotateLeft Gira los bits a la izquierda, a veces también denominado desplazamiento circular a la izquierda.
RotateRight Gira los bits a la derecha, a veces también denominado desplazamiento circular a la derecha.
TrailingZeroCount Cuenta el número de bits cero finales en la representación binaria.
IFloatingPoint<TSelf> Ceiling Redondea el valor hacia el infinito positivo. +4,5 se convierte en +5 y -4,5 se convierte en -4.
Floor Redondea el valor hacia el infinito negativo. +4,5 se convierte en +4 y -4,5 se convierte en -5.
Round Redondea el valor con el modo de redondeo especificado.
Truncate Redondea el valor hacia cero. +4,5 se convierte en +4 y -4,5 se convierte en -4.
IFloatingPointIeee754<TSelf> E Obtiene un valor que representa el número de Euler para el tipo.
Epsilon Obtiene el valor representable más pequeño que es mayor que cero para el tipo.
NaN Obtiene un valor que representa NaN para el tipo.
NegativeInfinity Obtiene un valor que representa -Infinity para el tipo.
NegativeZero Obtiene un valor que representa -Zero para el tipo.
Pi Obtiene un valor que representa Pi para el tipo.
PositiveInfinity Obtiene un valor que representa +Infinity para el tipo.
Tau Obtiene un valor que representa Tau (2 * Pi) para el tipo.
(Otro) (Implementa todo el conjunto de interfaces que se enumeran en Interfaces de función)
INumber<TSelf> Clamp Restringe un valor a no más ni menos que los valores mínimo y máximo especificados.
CopySign Establece el signo de un valor especificado igual que el de otro valor especificado.
Max Devuelve el mayor de dos valores y devuelve NaN si alguna de las entradas es NaN.
MaxNumber Devuelve el mayor de dos valores y devuelve el número si una entrada es NaN.
Min Devuelve el menor de dos valores y devuelve NaN si alguna de las entradas es NaN.
MinNumber Devuelve el menor de dos valores y devuelve el número si una entrada es NaN.
Sign Devuelve -1 para los valores negativos, 0 para cero y +1 para los valores positivos.
INumberBase<TSelf> One Obtiene el valor 1 para el tipo.
Radix Obtiene la raíz, o base, para el tipo. Int32 devuelve 2. Decimal devuelve 10.
Zero Obtiene el valor 0 para el tipo.
CreateChecked Crea un valor y produce OverflowException si la entrada no cabe1.
CreateSaturating Crea un valor sujeto a T.MinValue o T.MaxValue si la entrada no cabe1.
CreateTruncating Crea un valor a partir de otro valor y lo ajusta si la entrada no cabe 1.
IsComplexNumber Devuelve true si el valor tiene una parte real distinta de cero y una parte imaginaria distinta de cero.
IsEvenInteger Devuelve true si el valor es un entero par. 2.0 devuelve true y 2.2 devuelve false.
IsFinite Devuelve true si el valor no es infinito ni NaN.
IsImaginaryNumber Devuelve true si el valor tiene una parte real cero. Esto significa que 0 es imaginario y 1 + 1i no lo es.
IsInfinity Devuelve true si el valor representa el infinito.
IsInteger Devuelve true si el valor es un entero. 2.0 y 3.0 devuelven true, y 2.2 y 3.1 devuelven false.
IsNaN Devuelve true si el valor representa NaN.
IsNegative Devuelve true si el valor es negativo. Esto incluye -0.0.
IsPositive Devuelve true si el valor es positivo. Esto incluye 0 y +0.0.
IsRealNumber Devuelve true si el valor tiene una parte imaginaria cero. Esto significa que 0 es real, como todos los tipos INumber<T>.
IsZero Devuelve true si el valor representa cero. Esto incluye 0, +0.0 y -0.0.
MaxMagnitude Devuelve el valor con un valor absoluto mayor y devuelve NaN si alguna de las entradas es NaN.
MaxMagnitudeNumber Devuelve el valor con un valor absoluto mayor y devuelve el número si una entrada es NaN.
MinMagnitude Devuelve el valor con un valor absoluto menor y devuelve NaN si alguna de las entradas es NaN.
MinMagnitudeNumber Devuelve el valor con un valor absoluto menor y devuelve el número si una entrada es NaN.
ISignedNumber<TSelf> NegativeOne Obtiene el valor -1 para el tipo.

1 Para facilitar la comprensión del comportamiento de los tres métodos Create*, tenga en cuenta los ejemplos siguientes.

Ejemplo de cuando se da un valor demasiado grande:

  • byte.CreateChecked(384) produce una excepción OverflowException.
  • byte.CreateSaturating(384) devuelve 255 porque 384 es mayor que Byte.MaxValue (que es 255).
  • byte.CreateTruncating(384) devuelve 128 porque toma los 8 bits más bajos (384 tiene una representación hexadecimal de 0x0180 y el mínimo de 8 bits es 0x80, que es 128).

Ejemplo de cuando se da un valor demasiado pequeño:

  • byte.CreateChecked(-384) produce una excepción OverflowException.
  • byte.CreateSaturating(-384) devuelve 0 porque -384 es menor que Byte.MinValue (que es 0).
  • byte.CreateTruncating(-384) devuelve 128 porque toma los 8 bits más bajos (384 tiene una representación hexadecimal de 0xFE80 y el mínimo de 8 bits es 0x80, que es 128).

Los métodos Create* también tienen algunas consideraciones especiales para los tipos de punto flotante IEEE 754, como float y double, porque tienen los valores especiales PositiveInfinity, NegativeInfinity y NaN. Las tres API Create* se comportan como CreateSaturating. Además, mientras MinValue y MaxValue representan el número "normal" negativo o positivo más grande, los valores mínimo y máximo reales son NegativeInfinity y PositiveInfinity, por lo que dependen de estos valores en su lugar.

Interfaces de operador

Las interfaces de operador corresponden a los distintos operadores disponibles para el lenguaje C#.

  • No emparejan explícitamente operaciones como la multiplicación y la división, ya que eso no es correcto para todos los tipos. Por ejemplo, Matrix4x4 * Matrix4x4 es válido, pero Matrix4x4 / Matrix4x4 no es válido.
  • Normalmente, permiten que los tipos de entrada y de resultado sean diferentes para admitir escenarios como dividir dos enteros para obtener un double (por ejemplo, 3 / 2 = 1.5) o calcular el promedio de un conjunto de enteros.
Nombre de la interfaz Operadores definidos
IAdditionOperators<TSelf,TOther,TResult> x + y
IBitwiseOperators<TSelf,TOther,TResult> x & y, 'x | y', x ^ y, y ~x
IComparisonOperators<TSelf,TOther,TResult> x < y, x > y, x <= y y x >= y.
IDecrementOperators<TSelf> --x y x--
IDivisionOperators<TSelf,TOther,TResult> x / y
IEqualityOperators<TSelf,TOther,TResult> x == y y x != y
IIncrementOperators<TSelf> ++x y x++
IModulusOperators<TSelf,TOther,TResult> x % y
IMultiplyOperators<TSelf,TOther,TResult> x * y
IShiftOperators<TSelf,TOther,TResult> x << y y x >> y
ISubtractionOperators<TSelf,TOther,TResult> x - y
IUnaryNegationOperators<TSelf,TResult> -x
IUnaryPlusOperators<TSelf,TResult> +x

Nota:

Algunas de las interfaces definen un operador comprobado además de un operador no comprobado normal. Los operadores comprobados se llaman en contextos comprobados y permiten que un tipo definido por el usuario defina el comportamiento de desbordamiento. Si implementa un operador comprobado, por ejemplo, CheckedSubtraction(TSelf, TOther), también debe implementar el operador no comprobado, por ejemplo, Subtraction(TSelf, TOther).

Interfaces de función

Las interfaces de función definen API matemáticas comunes que se aplican más ampliamente que a una interfaz numérica específica. Estas interfaces se implementan con IFloatingPointIeee754<TSelf> y puede que se implementen con otros tipos relevantes en el futuro.

Nombre de la interfaz Descripción
IExponentialFunctions<TSelf> Expone funciones exponenciales que admiten e^x, e^x - 1, 2^x, 2^x - 1, 10^x y 10^x - 1.
IHyperbolicFunctions<TSelf> Expone funciones hiperbólicas que admiten acosh(x), asinh(x), atanh(x), cosh(x), sinh(x) y tanh(x).
ILogarithmicFunctions<TSelf> Expone funciones logarítmicas que admiten ln(x), ln(x + 1), log2(x), log2(x + 1), log10(x) y log10(x + 1).
IPowerFunctions<TSelf> Expone funciones de potencia que admiten x^y.
IRootFunctions<TSelf> Expone funciones de raíz que admiten cbrt(x) y sqrt(x).
ITrigonometricFunctions<TSelf> Expone funciones trigonométricas que admiten acos(x), asin(x), atan(x), cos(x), sin(x) y tan(x).

Interfaces de análisis y formato

El análisis y el formato son conceptos básicos de programación. Normalmente, se usan cuando se convierte la entrada del usuario en un tipo determinado o se muestra un tipo al usuario. Estas interfaces están en el espacio de nombres System.

Nombre de la interfaz Descripción
IParsable<TSelf> Expone la compatibilidad con T.Parse(string, IFormatProvider) y T.TryParse(string, IFormatProvider, out TSelf).
ISpanParsable<TSelf> Expone la compatibilidad con T.Parse(ReadOnlySpan<char>, IFormatProvider) y T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf).
IFormattable1 Expone la compatibilidad con value.ToString(string, IFormatProvider).
ISpanFormattable1 Expone la compatibilidad con value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider).

1 Esta interfaz no es nueva ni es genérica. Sin embargo, la implementan todos los tipos numéricos y representa la operación inversa de IParsable.

Por ejemplo, el siguiente programa toma dos números como entrada, leyéndolos de la consola con un método genérico donde el parámetro de tipo está restringido a ser IParsable<TSelf>. Calcula el promedio con un método genérico donde los parámetros de tipo de los valores de entrada y resultado están restringidos a ser INumber<TSelf> y, a continuación, muestra el resultado en la consola.

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
*/

Vea también