Generic math
.NET 7 introduces new math-related generic interfaces to the base class library. The availability of these interfaces means you can constrain a type parameter of a generic type or method to be "number-like". In addition, C# 11 and later lets you define static virtual
interface members. Because operators must be declared as static
, this new C# feature lets operators be declared in the new interfaces for number-like types.
Together, these innovations allow you to perform mathematical operations generically—that is, without having to know the exact type you're working with. For example, if you wanted to write a method that adds two numbers, previously you had to add an overload of the method for each type (for example, static int Add(int first, int second)
and static float Add(float first, float second)
). Now you can write a single, generic method, where the type parameter is constrained to be a number-like type. For example:
static T Add<T>(T left, T right)
where T : INumber<T>
{
return left + right;
}
In this method, the type parameter T
is constrained to be a type that implements the new INumber<TSelf> interface. INumber<TSelf> implements the IAdditionOperators<TSelf,TOther,TResult> interface, which contains the + operator. That allows the method to generically add the two numbers. The method can be used with any of .NET's built-in numeric types, because they've all been updated to implement INumber<TSelf> in .NET 7.
Library authors will benefit most from the generic math interfaces, because they can simplify their code base by removing "redundant" overloads. Other developers will benefit indirectly, because the APIs they consume may start supporting more types.
The interfaces
The interfaces were designed to be both fine-grained enough that users can define their own interfaces on top, while also being granular enough that they're easy to consume. To that extent, there are a few core numeric interfaces that most users will interact with, such as INumber<TSelf> and IBinaryInteger<TSelf>. The more fine-grained interfaces, such as IAdditionOperators<TSelf,TOther,TResult> and ITrigonometricFunctions<TSelf>, support these types and are available for developers who define their own domain-specific numeric interfaces.
Numeric interfaces
This section describes the interfaces in System.Numerics that describe number-like types and the functionality available to them.
Interface name | Description |
---|---|
IBinaryFloatingPointIeee754<TSelf> | Exposes APIs common to binary floating-point types1 that implement the IEEE 754 standard. |
IBinaryInteger<TSelf> | Exposes APIs common to binary integers2. |
IBinaryNumber<TSelf> | Exposes APIs common to binary numbers. |
IFloatingPoint<TSelf> | Exposes APIs common to floating-point types. |
IFloatingPointIeee754<TSelf> | Exposes APIs common to floating-point types that implement the IEEE 754 standard. |
INumber<TSelf> | Exposes APIs common to comparable number types (effectively the "real" number domain). |
INumberBase<TSelf> | Exposes APIs common to all number types (effectively the "complex" number domain). |
ISignedNumber<TSelf> | Exposes APIs common to all signed number types (such as the concept of NegativeOne ). |
IUnsignedNumber<TSelf> | Exposes APIs common to all unsigned number types. |
IAdditiveIdentity<TSelf,TResult> | Exposes the concept of (x + T.AdditiveIdentity) == x . |
IMinMaxValue<TSelf> | Exposes the concept of T.MinValue and T.MaxValue . |
IMultiplicativeIdentity<TSelf,TResult> | Exposes the concept of (x * T.MultiplicativeIdentity) == x . |
1The binary floating-point types are Double (double
), Half, and Single (float
).
2The binary integer types are Byte (byte
), Int16 (short
), Int32 (int
), Int64 (long
), Int128, IntPtr (nint
), SByte (sbyte
), UInt16 (ushort
), UInt32 (uint
), UInt64 (ulong
), UInt128, and UIntPtr (nuint
).
The interface you're most likely to use directly is INumber<TSelf>, which roughly corresponds to a real number. If a type implements this interface, it means that a value has a sign (this includes unsigned
types, which are considered positive) and can be compared to other values of the same type. INumberBase<TSelf> confers more advanced concepts, such as complex and imaginary numbers, for example, the square root of a negative number. Other interfaces, such as IFloatingPointIeee754<TSelf>, were created because not all operations make sense for all number types—for example, calculating the floor of a number only makes sense for floating-point types. In the .NET base class library, the floating-point type Double implements IFloatingPointIeee754<TSelf> but Int32 doesn't.
Several of the interfaces are also implemented by various other types, including Char, DateOnly, DateTime, DateTimeOffset, Decimal, Guid, TimeOnly, and TimeSpan.
The following table shows some of the core APIs exposed by each interface.
Interface | API name | Description |
---|---|---|
IBinaryInteger<TSelf> | DivRem |
Computes the quotient and remainder simultaneously. |
LeadingZeroCount |
Counts the number of leading zero bits in the binary representation. | |
PopCount |
Counts the number of set bits in the binary representation. | |
RotateLeft |
Rotates bits left, sometimes also called a circular left shift. | |
RotateRight |
Rotates bits right, sometimes also called a circular right shift. | |
TrailingZeroCount |
Counts the number of trailing zero bits in the binary representation. | |
IFloatingPoint<TSelf> | Ceiling |
Rounds the value towards positive infinity. +4.5 becomes +5, and -4.5 becomes -4. |
Floor |
Rounds the value towards negative infinity. +4.5 becomes +4, and -4.5 becomes -5. | |
Round |
Rounds the value using the specified rounding mode. | |
Truncate |
Rounds the value towards zero. +4.5 becomes +4, and -4.5 becomes -4. | |
IFloatingPointIeee754<TSelf> | E |
Gets a value representing Euler's number for the type. |
Epsilon |
Gets the smallest representable value that's greater than zero for the type. | |
NaN |
Gets a value representing NaN for the type. |
|
NegativeInfinity |
Gets a value representing -Infinity for the type. |
|
NegativeZero |
Gets a value representing -Zero for the type. |
|
Pi |
Gets a value representing Pi for the type. |
|
PositiveInfinity |
Gets a value representing +Infinity for the type. |
|
Tau |
Gets a value representing Tau (2 * Pi ) for the type. |
|
(Other) | (Implements the full set of interfaces listed under Function interfaces.) | |
INumber<TSelf> | Clamp |
Restricts a value to no more and no less than the specified min and max value. |
CopySign |
Sets the sign of a specified value to the same as another specified value. | |
Max |
Returns the greater of two values, returning NaN if either input is NaN . |
|
MaxNumber |
Returns the greater of two values, returning the number if one input is NaN . |
|
Min |
Returns the lesser of two values, returning NaN if either input is NaN . |
|
MinNumber |
Returns the lesser of two values, returning the number if one input is NaN . |
|
Sign |
Returns -1 for negative values, 0 for zero, and +1 for positive values. | |
INumberBase<TSelf> | One |
Gets the value 1 for the type. |
Radix |
Gets the radix, or base, for the type. Int32 returns 2. Decimal returns 10. | |
Zero |
Gets the value 0 for the type. | |
CreateChecked |
Creates a value, throwing an OverflowException if the input can't fit.1 | |
CreateSaturating |
Creates a value, clamping to T.MinValue or T.MaxValue if the input can't fit.1 |
|
CreateTruncating |
Creates a value from another value, wrapping around if the input can't fit.1 | |
IsComplexNumber |
Returns true if the value has a non-zero real part and a non-zero imaginary part. | |
IsEvenInteger |
Returns true if the value is an even integer. 2.0 returns true , and 2.2 returns false . |
|
IsFinite |
Returns true if the value is not infinite and not NaN . |
|
IsImaginaryNumber |
Returns true if the value has a zero real part. This means 0 is imaginary and 1 + 1i isn't. |
|
IsInfinity |
Returns true if the value represents infinity. | |
IsInteger |
Returns true if the value is an integer. 2.0 and 3.0 return true , and 2.2 and 3.1 return false . |
|
IsNaN |
Returns true if the value represents NaN . |
|
IsNegative |
Returns true if the value is negative. This includes -0.0. | |
IsPositive |
Returns true if the value is positive. This includes 0 and +0.0. | |
IsRealNumber |
Returns true if the value has a zero imaginary part. This means 0 is real as are all INumber<T> types. |
|
IsZero |
Returns true if the value represents zero. This includes 0, +0.0, and -0.0. | |
MaxMagnitude |
Returns the value with a greater absolute value, returning NaN if either input is NaN . |
|
MaxMagnitudeNumber |
Returns the value with a greater absolute value, returning the number if one input is NaN . |
|
MinMagnitude |
Returns the value with a lesser absolute value, returning NaN if either input is NaN . |
|
MinMagnitudeNumber |
Returns the value with a lesser absolute value, returning the number if one input is NaN . |
|
ISignedNumber<TSelf> | NegativeOne |
Gets the value -1 for the type. |
1To help understand the behavior of the three Create*
methods, consider the following examples.
Example when given a value that's too large:
byte.CreateChecked(384)
will throw an OverflowException.byte.CreateSaturating(384)
returns 255 because 384 is greater than Byte.MaxValue (which is 255).byte.CreateTruncating(384)
returns 128 because it takes the lowest 8 bits (384 has a hex representation of0x0180
, and the lowest 8 bits is0x80
, which is 128).
Example when given a value that's too small:
byte.CreateChecked(-384)
will throw an OverflowException.byte.CreateSaturating(-384)
returns 0 because -384 is smaller than Byte.MinValue (which is 0).byte.CreateTruncating(-384)
returns 128 because it takes the lowest 8 bits (384 has a hex representation of0xFE80
, and the lowest 8 bits is0x80
, which is 128).
The Create*
methods also have some special considerations for IEEE 754 floating-point types, like float
and double
, as they have the special values PositiveInfinity
, NegativeInfinity
, and NaN
. All three Create*
APIs behave as CreateSaturating
. Also, while MinValue
and MaxValue
represent the largest negative/positive "normal" number, the actual minimum and maximum values are NegativeInfinity
and PositiveInfinity
, so they clamp to these values instead.
Operator interfaces
The operator interfaces correspond to the various operators available to the C# language.
- They explicitly don't pair operations such as multiplication and division since that isn't correct for all types. For example,
Matrix4x4 * Matrix4x4
is valid, butMatrix4x4 / Matrix4x4
isn't valid. - They typically allow the input and result types to differ to support scenarios such as dividing two integers to obtain a
double
, for example,3 / 2 = 1.5
, or calculating the average of a set of integers.
Interface name | Defined operators |
---|---|
IAdditionOperators<TSelf,TOther,TResult> | x + y |
IBitwiseOperators<TSelf,TOther,TResult> | x & y , 'x | y', x ^ y , and ~x |
IComparisonOperators<TSelf,TOther,TResult> | x < y , x > y , x <= y , and x >= y |
IDecrementOperators<TSelf> | --x and x-- |
IDivisionOperators<TSelf,TOther,TResult> | x / y |
IEqualityOperators<TSelf,TOther,TResult> | x == y and x != y |
IIncrementOperators<TSelf> | ++x and x++ |
IModulusOperators<TSelf,TOther,TResult> | x % y |
IMultiplyOperators<TSelf,TOther,TResult> | x * y |
IShiftOperators<TSelf,TOther,TResult> | x << y and x >> y |
ISubtractionOperators<TSelf,TOther,TResult> | x - y |
IUnaryNegationOperators<TSelf,TResult> | -x |
IUnaryPlusOperators<TSelf,TResult> | +x |
Note
Some of the interfaces define a checked operator in addition to a regular unchecked operator. Checked operators are called in checked contexts and allow a user-defined type to define overflow behavior. If you implement a checked operator, for example, CheckedSubtraction(TSelf, TOther), you must also implement the unchecked operator, for example, Subtraction(TSelf, TOther).
Function interfaces
The function interfaces define common mathematical APIs that apply more broadly than to a specific numeric interface. These interfaces are all implemented by IFloatingPointIeee754<TSelf>, and may get implemented by other relevant types in the future.
Interface name | Description |
---|---|
IExponentialFunctions<TSelf> | Exposes exponential functions supporting e^x , e^x - 1 , 2^x , 2^x - 1 , 10^x , and 10^x - 1 . |
IHyperbolicFunctions<TSelf> | Exposes hyperbolic functions supporting acosh(x) , asinh(x) , atanh(x) , cosh(x) , sinh(x) , and tanh(x) . |
ILogarithmicFunctions<TSelf> | Exposes logarithmic functions supporting ln(x) , ln(x + 1) , log2(x) , log2(x + 1) , log10(x) , and log10(x + 1) . |
IPowerFunctions<TSelf> | Exposes power functions supporting x^y . |
IRootFunctions<TSelf> | Exposes root functions supporting cbrt(x) and sqrt(x) . |
ITrigonometricFunctions<TSelf> | Exposes trigonometric functions supporting acos(x) , asin(x) , atan(x) , cos(x) , sin(x) , and tan(x) . |
Parsing and formatting interfaces
Parsing and formatting are core concepts in programming. They're commonly used when converting user input to a given type or displaying a type to the user. These interfaces are in the System namespace.
Interface name | Description |
---|---|
IParsable<TSelf> | Exposes support for T.Parse(string, IFormatProvider) and T.TryParse(string, IFormatProvider, out TSelf) . |
ISpanParsable<TSelf> | Exposes support for T.Parse(ReadOnlySpan<char>, IFormatProvider) and T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf) . |
IFormattable1 | Exposes support for value.ToString(string, IFormatProvider) . |
ISpanFormattable1 | Exposes support for value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider) . |
1This interface isn't new, nor is it generic. However, it's implemented by all number types and represents the inverse operation of IParsable
.
For example, the following program takes two numbers as input, reading them from the console using a generic method where the type parameter is constrained to be IParsable<TSelf>. It calculates the average using a generic method where the type parameters for the input and result values are constrained to be INumber<TSelf>, and then displays the result to the 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
*/