Using Generic Math in C# for Numeric Algorithms
Generic math, introduced in .NET 7 and C# 11, allows developers to
write generalized numeric algorithms that work across multiple data
types without duplicating code or sacrificing performance. Built upon
static virtual interface members, generic math abstracts arithmetic
operations, constants, and numeric capabilities behind interfaces such
as INumber<T> and
IFloatingPoint<T>. This guide explains how to define
type constraints, utilize built-in numeric interfaces, handle constants,
and implement efficient numeric algorithms in C#.
The Foundation: Static Abstract Interface Members
Prior to .NET 7, you could not use standard mathematical operators
(+, -, *, /) on
generic type parameters because operators are static methods. Generic
math solves this using static abstract members in interfaces. The
runtime delegates operator calls to the static methods implemented by
the concrete type.
The base interface for most algorithms is
INumber<T>, which provides standard arithmetic
operations, comparison capabilities, and common numeric constants.
Constraining Generic
Types with INumber<T>
To create a function that operates over any numeric type (such as
int, float, double, or
decimal), constrain your generic type parameter to
INumber<T>.
Here is an implementation of an algorithm that calculates the sum of a collection:
using System.Numerics;
public static class MathUtilities
{
public static T Sum<T>(IEnumerable<T> numbers) where T : INumber<T>
{
T total = T.Zero;
foreach (var number in numbers)
{
total += number;
}
return total;
}
}In this example:
where T : INumber<T>guarantees that typeTimplements addition, equality, and common properties.T.Zeroaccesses the static property of the interface to retrieve the zero-value representation for whatever typeTrepresents.
Choosing the Right Generic Math Interface
The System.Numerics namespace provides a hierarchy of
interfaces tailored to specific capabilities:
INumber<T>: Represents numbers supporting basic arithmetic, comparison, parsing, and formatting.IBinaryInteger<T>: For integer types (int,long,BigInteger), exposing bitwise operations, shifts, and integer division.IFloatingPoint<T>: For real numbers (float,double,decimal,Half), exposing rounding, IEEE 754 compliance, and sign manipulation.IRootFunctions<T>,ITrigonometricFunctions<T>,ILogarithmicFunctions<T>: Expose specialized mathematical operations likeSqrt,Sin, andLog.
Example: Advanced
Math with IFloatingPoint<T>
If an algorithm requires non-integer capabilities, such as
calculating the Euclidean distance between two points, constrain the
type to IFloatingPointI<T> (which includes
IRootFunctions<T>):
using System.Numerics;
public static class Geometry
{
public static T Distance<T>(T x1, T y1, T x2, T y2) where T : IFloatingPoint<T>, IRootFunctions<T>
{
T deltaX = x2 - x1;
T deltaY = y2 - y1;
return T.Sqrt((deltaX * deltaX) + (deltaY * deltaY));
}
}This method works interchangeably with float,
double, or custom high-precision floating-point types
without casting or code duplication.
Type Conversions and Constants
Numeric algorithms frequently require numeric literals (like
2 or 0.5) or conversions between different
types. Because generic types cannot be directly assigned integer
literals, use T.CreateChecked,
T.CreateTruncating, or T.CreateSaturating.
Here is a generic calculation of the arithmetic mean:
using System.Numerics;
public static class Statistics
{
public static T Mean<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
if (values.IsEmpty)
{
throw new ArgumentException("Collection must not be empty.", nameof(values));
}
T sum = T.Zero;
foreach (var value in values)
{
sum += value;
}
// Convert the count (int) into type T safely
T count = T.CreateChecked(values.Length);
return sum / count;
}
}T.CreateChecked(value)throws anOverflowExceptionif the value cannot be represented byT.T.CreateTruncating(value)discards excess bits if the value is too large.T.CreateSaturating(value)clamps the value to the minimum or maximum bounds ofT.
Benefits and Performance
Generic math algorithms compile down to highly optimized machine code
through JIT specialization. For value types like int and
double, the .NET JIT compiler eliminates virtual dispatch
overhead, inlining the static calls directly. This yields performance
equivalent to hand-written, type-specific overloads while maintaining a
single, reusable codebase.