.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 System.Numerics.INumber interface. System.Numerics.INumber implements the System.Numerics.IAdditionOperators 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 System.Numerics.INumber 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 System.Numerics.INumber and System.Numerics.IBinaryInteger. The more fine-grained interfaces, such as System.Numerics.IAdditionOperators and System.Numerics.ITrigonometricFunctions, 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 |
|---|---|
| System.Numerics.IBinaryFloatingPointIeee754 | Exposes APIs common to binary floating-point types1 that implement the IEEE 754 standard. |
| System.Numerics.IBinaryInteger | Exposes APIs common to binary integers2. |
| System.Numerics.IBinaryNumber | Exposes APIs common to binary numbers. |
| System.Numerics.IFloatingPoint | Exposes APIs common to floating-point types. |
| System.Numerics.IFloatingPointIeee754 | Exposes APIs common to floating-point types that implement the IEEE 754 standard. |
| System.Numerics.INumber | Exposes APIs common to comparable number types (effectively the "real" number domain). |
| System.Numerics.INumberBase | Exposes APIs common to all number types (effectively the "complex" number domain). |
| System.Numerics.ISignedNumber | Exposes APIs common to all signed number types (such as the concept of NegativeOne). |
| System.Numerics.IUnsignedNumber | Exposes APIs common to all unsigned number types. |
| System.Numerics.IAdditiveIdentity | Exposes the concept of (x + T.AdditiveIdentity) == x. |
| System.Numerics.IMinMaxValue | Exposes the concept of T.MinValue and T.MaxValue. |
| System.Numerics.IMultiplicativeIdentity | Exposes the concept of (x * T.MultiplicativeIdentity) == x. |
1The binary floating-point types are System.Double (double), System.Half, and System.Single (float).
2The binary integer types are System.Byte (byte), System.Int16 (short), System.Int32 (int), System.Int64 (long), System.Int128, System.IntPtr (nint), System.SByte (sbyte), System.UInt16 (ushort), System.UInt32 (uint), System.UInt64 (ulong), System.UInt128, and System.UIntPtr (nuint).
The interface you're most likely to use directly is System.Numerics.INumber, 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. System.Numerics.INumberBase confers more advanced concepts, such as complex and imaginary numbers, for example, the square root of a negative number. Other interfaces, such as System.Numerics.IFloatingPointIeee754, 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 System.Double implements System.Numerics.IFloatingPointIeee754 but System.Int32 doesn't.
Several of the interfaces are also implemented by various other types, including System.Char, System.DateOnly, System.DateTime, System.DateTimeOffset, System.Decimal, System.Guid, System.TimeOnly, and System.TimeSpan.
The following table shows some of the core APIs exposed by each interface.
| Interface | API name | Description |
|---|---|---|
| System.Numerics.IBinaryInteger | 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. | |
| System.Numerics.IFloatingPoint | 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. | |
| System.Numerics.IFloatingPointIeee754 | 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.) | |
| System.Numerics.INumber | 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. | |
| System.Numerics.INumberBase | One |
Gets the value 1 for the type. |
Radix |
Gets the radix, or base, for the type. System.Int32 returns 2. System.Decimal returns 10. | |
Zero |
Gets the value 0 for the type. | |
CreateChecked |
Creates a value, throwing an System.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. |
|
| System.Numerics.ISignedNumber | 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 System.OverflowException.byte.CreateSaturating(384)returns 255 because 384 is greater than System.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 System.OverflowException.byte.CreateSaturating(-384)returns 0 because -384 is smaller than System.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 * Matrix4x4is valid, butMatrix4x4 / Matrix4x4isn'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 |
|---|---|
| System.Numerics.IAdditionOperators | x + y |
| System.Numerics.IBitwiseOperators | x & y, 'x | y', x ^ y, and ~x |
| System.Numerics.IComparisonOperators | x < y, x > y, x <= y, and x >= y |
| System.Numerics.IDecrementOperators | --x and x-- |
| System.Numerics.IDivisionOperators | x / y |
| System.Numerics.IEqualityOperators | x == y and x != y |
| System.Numerics.IIncrementOperators | ++x and x++ |
| System.Numerics.IModulusOperators | x % y |
| System.Numerics.IMultiplyOperators | x * y |
| System.Numerics.IShiftOperators | x << y and x >> y |
| System.Numerics.ISubtractionOperators | x - y |
| System.Numerics.IUnaryNegationOperators | -x |
| System.Numerics.IUnaryPlusOperators | +x |
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 System.Numerics.IFloatingPointIeee754, and may get implemented by other relevant types in the future.
| Interface name | Description |
|---|---|
| System.Numerics.IExponentialFunctions | Exposes exponential functions supporting e^x, e^x - 1, 2^x, 2^x - 1, 10^x, and 10^x - 1. |
| System.Numerics.IHyperbolicFunctions | Exposes hyperbolic functions supporting acosh(x), asinh(x), atanh(x), cosh(x), sinh(x), and tanh(x). |
| System.Numerics.ILogarithmicFunctions | Exposes logarithmic functions supporting ln(x), ln(x + 1), log2(x), log2(x + 1), log10(x), and log10(x + 1). |
| System.Numerics.IPowerFunctions | Exposes power functions supporting x^y. |
| System.Numerics.IRootFunctions | Exposes root functions supporting cbrt(x) and sqrt(x). |
| System.Numerics.ITrigonometricFunctions | 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 |
|---|---|
| System.IParsable | Exposes support for T.Parse(string, IFormatProvider) and T.TryParse(string, IFormatProvider, out TSelf). |
| System.ISpanParsable | Exposes support for T.Parse(ReadOnlySpan<char>, IFormatProvider) and T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf). |
| System.IFormattable1 | Exposes support for value.ToString(string, IFormatProvider). |
| System.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 System.IParsable. It calculates the average using a generic method where the type parameters for the input and result values are constrained to be System.Numerics.INumber, 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
*/