In November 2022 with the release of .NET 7, new math-related generic interfaces have been added to the .NET Base Class Library (BCL). This is named .NET Generic Math. These interfaces allow you to constrain a type parameter in a generic type or method to behave like a number.
Abstracting Algorithm with Generic Math
.NET Generic Math makes it possible to perform mathematical operations generically, meaning you don’t need to know the exact type you’re working with. For example, this method Middle<T>()
abstracts the computation of the middle of two numbers:
1 2 3 4 5 6 7 8 |
using System.Numerics; Assert.IsTrue(Middle(1, 3) == 2); Assert.IsTrue(Middle(1, 1.5) == 1.25); static T Middle<T>(T a, T b) where T: INumber<T> { return (a + b) / (T.One + T.One); } |
This small method doesn’t look like a valid C# code for two reasons:
- The expression
T.One
is a call on a static member defined on a generic parameter type. - The expression
(a + b) / two
calls the addition and division operators overloaded on the generic parameter typeT
.
How does this work?
Interface static abstract members
This is possible because T
is required to implement the Generic Math interface System.Numerics.INumber<T>
. This interface is special because it has static abstract members. They were introduced in C# 11 and .NET 7 specifically to enable the implementation of Generic Math.
More specifically the interface INumber<TSelf>
extends the interface INumberBase<T>
defined as:
1 2 3 4 5 |
interface INumberBase<TSelf> { /// <summary>Gets the value <c>1</c> for the type.</summary> static abstract TSelf One { get; } ... } |
The interface INumber<TSelf>
extends the interfaces IAdditionOperators<TSelf,TOther,TResult>
and IDivisionOperators
<TSelf,TOther,TResult>
defined as:
1 2 3 4 5 6 7 8 9 10 11 |
public interface IAdditionOperators<TSelf, TOther, TResult> where TSelf : IAdditionOperators<TSelf, TOther, TResult>? { static abstract TResult operator +(TSelf left, TOther right); ... } public interface IDivisionOperators<TSelf, TOther, TResult> where TSelf : IDivisionOperators<TSelf, TOther, TResult>? { static abstract TResult operator /(TSelf left, TOther right); ... } |
Generic Math as a Collection of Fine-Grained Interfaces
We saw that the interface INumber<TSelf>
declared in the namespace System.Numerics
plays a central role in Generic Math. Generic Math introduces many new fine-grained interfaces to implement every aspect of numerics.
Numeric Interfaces
Numeric interfaces abstract numeric representations like INumber<TSelf>
. INumber<TSelf>
mostly represent real number types. It extends INumberBase<TSelf>
that defines the base for complex and real number types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
namespace System.Numerics { /// <summary>Defines the base of other number types.</summary> /// <typeparam name="TSelf">The type that implements the interface.</typeparam> public interface INumberBase<TSelf> : IAdditionOperators<TSelf, TSelf, TSelf>, IAdditiveIdentity<TSelf, TSelf>, IDecrementOperators<TSelf>, IDivisionOperators<TSelf, TSelf, TSelf>, IEquatable<TSelf>, IEqualityOperators<TSelf, TSelf, bool>, IIncrementOperators<TSelf>, IMultiplicativeIdentity<TSelf, TSelf>, IMultiplyOperators<TSelf, TSelf, TSelf>, ISpanFormattable, ISpanParsable<TSelf>, ISubtractionOperators<TSelf, TSelf, TSelf>, IUnaryPlusOperators<TSelf, TSelf>, IUnaryNegationOperators<TSelf, TSelf>, IUtf8SpanFormattable, IUtf8SpanParsable<TSelf> where TSelf : INumberBase<TSelf>? { ... |
With a code query on the .NET BCL, we can see which structures implement INumber<TSelf>
and which interfaces extend it:
Here are all the numeric interfaces defined in the System.Numerics
namespace:
Interface | Description |
---|---|
INumberBase<TSelf> | Abstracts the representation and APIs of all number types, typically real and complex numbers. It exposes properties One and Zero . It exposes the method CreateChecked<TOther>(TOther) that could have been used in our Middle() example above to create the value 2 through T.CreateChecked(2) . It exposes other methods like IsComplexNumber(TSelf) , IsRealNumber(TSelf) , IsInteger(TSelf) , IsPositive(TSelf) , IsZero(TSelf) , IsNan(TSelf) or IsInfinite(TSelf) . It is implemented by all numbers shown in the screenshot above, including also the structure Complex . |
INumber<TSelf> | Abstracts the representation and APIs of comparable number types, typically only real numbers. It proposes methods like MaxNumber(TSelf, TSelf) or Sign(TSelf) . |
IBinaryNumber<TSelf> | Abstracts the representation and APIs of binary numbers. It proposes the property AllBitsSet and methods like IsPow2(TSelf) and Log2(TSelf) . |
IBinaryInteger<TSelf> | Abstracts the representation and APIs of binary integers. It presents methods like GetByteCount() . |
IFloatingPoint<TSelf> | Abstracts the representation and APIs of floating-point types like float , double , decimal , System.Half (16 bits floating numbers) and NFloat . |
IFloatingPointIeee754<TSelf> | Abstracts the representation and APIs of floating-point types that implement the IEEE 754 like the ones above except decimal . |
IBinaryFloatingPointIeee754<TSelf> | Same as above with an emphasis on binary floating-point representation in the IEEE 754 standard. It presents methods like GetExponentByteCount() or GetSignificandBitLength() . |
IFloatingPointConstants<TSelf> | Exposes properties E , Pi and Tau . |
ISignedNumber<TSelf> | Abstracts the representation and APIs of all signed number types through its property T.NegativeOne . |
IUnsignedNumber<TSelf> | Abstracts the representation and APIs of all unsigned number types. |
IAdditiveIdentity<TSelf,TResult> | It only exposes T.AdditiveIdentity useful in (x + T.AdditiveIdentity) == x . |
IMultiplicativeIdentity<TSelf,TResult> | It only exposes T.MultiplicativeIdentity useful in (x * T.MultiplicativeIdentity) == x . |
IMinMaxValue<TSelf> | Exposes T.MinValue and T.MaxValue . |
Operator interfaces
Operator interfaces each propose an operator available to the C# language.
- They intentionally avoid pairing operations like multiplication and division, as this isn’t appropriate for all types. For instance,
Matrix3x3 * Matrix3x3
is valid butMatrix3x3 / Matrix3x3
isn’t valid. - Also, an interface like
IMultiplicationOperators<TSelf,TOther,TResult>
has 3 generic parameter types for inputs and the result. For instance,Matrix3x3 * double
is valid, and multiplying twosbyte
returns anint
:255 * 255 = 65025
Interface | Corresponding C# operators |
---|---|
IAdditionOperators<TSelf,TOther,TResult> | x + y |
ISubtractionOperators<TSelf,TOther,TResult> | x - y |
IMultiplyOperators<TSelf,TOther,TResult> | x * y |
IDivisionOperators<TSelf,TOther,TResult> | x / y |
IEqualityOperators<TSelf,TOther,TResult> | x == y and x != y |
IComparisonOperators<TSelf,TOther,TResult> | x < y , x > y , x <= y , and x >= y |
IIncrementOperators<TSelf> | ++x and x++ |
IDecrementOperators<TSelf> | --x and x-- |
IModulusOperators<TSelf,TOther,TResult> | x % y |
IBitwiseOperators<TSelf,TOther,TResult> | x & y , ‘x | y’, x ^ y , and ~x |
IShiftOperators<TSelf,TOther,TResult> | x << y and x >> y |
IUnaryNegationOperators<TSelf,TResult> | -x |
IUnaryPlusOperators<TSelf,TResult> | +x |
Function interfaces
Function interfaces abstract usual mathematical functions. These interfaces are implemented by floating number structures like float
, double
, decimal
, System.Half
and NFloat
.
Interface | Description |
---|---|
IExponentialFunctions<TSelf> | Abstracts exponential functions: Exp(TSelf) , Exp10(TSelf) , Exp2(TSelf) |
ILogarithmicFunctions<TSelf> | Abstracts logarithmic functions: Log(TSelf), Log10(TSelf), Log2(TSelf) |
ITrigonometricFunctions<TSelf> | Abstracts trigonometric functions: Cos(TSelf) , Sin(TSelf) , Tan(TSelf) , Acos(TSelf) , Asin(TSelf) , Atan(TSelf)
|
IPowerFunctions<TSelf> | Abstracts the power function: Pow(TSelf,TSelf) . |
IRootFunctions<TSelf> | Abstracts root functions: Sqrt(TSelf) , RootN(TSelf,Int32) |
IHyperbolicFunctions<TSelf> | Abstracts hyperbolic functions: Cosh(TSelf) , Sinh(TSelf) , Tanh(TSelf) , Acosh(TSelf) , Asinh(TSelf) , Atanh(TSelf) , |
The methods presented by these interfaces are static abstract methods and can be used through code like this double.Exp(2)
.
Notice that for compatibility reasons these functions are still implemented by the System.Math
class, so we have a doublon: Assert.IsTrue(double.Exp(2) == Math.Exp(2))
.
Parsing and formatting interfaces
Formatting involves converting a number into a human-friendly representation, making it easier to read and understand. Conversely, parsing is the reverse process, where textual data is converted back into a numerical format for computational use.
Notice how generic parsing interfaces propose static abstract members.
Interface | Description |
---|---|
IParsable<TSelf> | Abstracts T.Parse(string, IFormatProvider) and T.TryParse(string, IFormatProvider, out TSelf) . |
ISpanParsable<TSelf> | Abstracts T.Parse(ReadOnlySpan<char>, IFormatProvider) and T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf) . |
IFormattable | Abstracts value.ToString(string, IFormatProvider) . |
ISpanFormattable | Abstracts value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider) . |
Interface Implementation and Boxing
Since the introduction of Generic Math, primitive types such as int
and double
which are structures, now implement some interfaces. But wait, isn’t it generally bad for a structure to implement an interface? This typically leads to boxing. Boxing occurs when the value of a structure is cast to a reference type, such as System.Object
or an interface. To accommodate this, an object must be created to host the value. This additional object puts pressure on the Garbage Collector, which is why boxing negatively impacts performance. The program below shows how casting a structure leads to an IL box
instruction to be emitted by the C# compiler:
Keep in mind that Generic Math is designed to abstract algorithms from number representations. As a result, interfaces in Generic Math are typically used within generic methods and generic types. Fortunately, .NET generics have been carefully designed since their inception to avoid boxing in this context. This is demonstrated in the program below that doesn’t rely on the box
IL instruction (the method DisposeIt<T>()
also does not contain any box
IL instruction):
Conclusion
Generic Math is a remarkable innovation introduced in .NET. It involved deep modifications to core primitive types such as int
and double
, while ensuring that these changes did not negatively impact the performance of existing scenarios. It was made possible thanks to:
- the implementation of generics in .NET that was meticulously thought out from its very beginning with C# 2
- the interface static abstract members that were added to C# 11 and the runtime. They are explained in detail in this blog post.
Initially, Generic Math was an experiment from Miguel de Icaza through the project PartyDonk.
Very interesting.
Minor detail that could be just a blog formatting issue: In the table under “Parsing and formatting interfaces”, IFormattable and ISpanFormattable both have a superscript 1 after their names, like as if there is some relevant footnote, but I couldn’t see any footnote. Did I misinterpret that?
Thanks Mark, this is fixed
Thanks
“and dividing two integers returns a double: 5 / 2 = 2.5” no it does not
❯ dotnet script
> 5 / 2
2
Thanks, Steve, we updated the example by multiplying 2 byte to return an int