Generic Math: Writing Numeric Algorithms That Work Across Types
Before C# 11, writing a generic method that adds two numbers was surprisingly difficult. You could not constrain a type parameter to "things that support addition" because operators were not part of any interface. The result was either code duplication across numeric types or awkward workarounds involving dynamic or expression trees. Generic math, powered by static abstract interface members, changes this fundamentally.
The Core Idea
.NET 7 introduced a set of interfaces in System.Numerics that define arithmetic operations as static abstract members. The built-in numeric types — int, double, decimal, float, and others — all implement these interfaces. This means you can now constrain generic type parameters to types that support specific mathematical operations.
public static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
T result = T.Zero;
foreach (var value in values)
{
result += value;
}
return result;
}
This single method works with int, double, decimal, long, Half, and any other type implementing INumber<T>:
int intSum = Sum<int>(new[] { 1, 2, 3, 4, 5 });
double doubleSum = Sum<double>(new[] { 1.5, 2.7, 3.14 });
decimal decimalSum = Sum<decimal>(new[] { 10.0m, 20.0m, 30.0m });
The Interface Hierarchy
The generic math interfaces form a hierarchy, so you can constrain to exactly the operations you need:
INumber<T>— general numeric operations including comparisonIAdditionOperators<T, T, T>— just additionIMultiplyOperators<T, T, T>— just multiplicationIFloatingPoint<T>— floating-point specific operationsIInteger<T>— integer-specific operations (available from .NET 8)IMinMaxValue<T>— access toMinValueandMaxValue
For example, an average function needs both addition and division:
public static T Average<T>(ReadOnlySpan<T> values)
where T : INumber<T>
{
T sum = T.Zero;
foreach (var value in values)
{
sum += value;
}
return sum / T.CreateChecked(values.Length);
}
T.CreateChecked converts the integer length to whatever T is, with overflow checking.
Static Abstract Members
The mechanism that makes this work is static abstract interface members, introduced in C# 11. Operators in C# are static methods, so allowing interfaces to declare static abstract members means operators can finally participate in generic constraints:
public interface IAdditionOperators<TSelf, TOther, TResult>
where TSelf : IAdditionOperators<TSelf, TOther, TResult>?
{
static abstract TResult operator +(TSelf left, TOther right);
}
This is a significant language evolution. It means interfaces can now define factory methods, operators, and other static contracts.
Practical Example: A Generic Statistics Helper
Here is a more complete example — a statistics utility that works across numeric types:
public static class Stats
{
public static T Min<T>(ReadOnlySpan<T> values) where T : INumber<T>, IMinMaxValue<T>
{
T min = T.MaxValue;
foreach (var value in values)
{
if (value < min)
min = value;
}
return min;
}
public static T Clamp<T>(T value, T min, T max) where T : INumber<T>
{
return T.Clamp(value, min, max);
}
public static bool IsInRange<T>(T value, T lower, T upper) where T : INumber<T>
{
return value >= lower && value <= upper;
}
}
Creating Your Own Numeric Types
You can implement the generic math interfaces on your own types. A Percentage type, for instance:
public readonly struct Percentage :
IAdditionOperators<Percentage, Percentage, Percentage>,
IComparisonOperators<Percentage, Percentage, bool>
{
public decimal Value { get; }
public Percentage(decimal value) => Value = value;
public static Percentage operator +(Percentage left, Percentage right)
=> new(left.Value + right.Value);
public static bool operator >(Percentage left, Percentage right)
=> left.Value > right.Value;
public static bool operator >=(Percentage left, Percentage right)
=> left.Value >= right.Value;
public static bool operator <(Percentage left, Percentage right)
=> left.Value < right.Value;
public static bool operator <=(Percentage left, Percentage right)
=> left.Value <= right.Value;
public static bool operator ==(Percentage left, Percentage right)
=> left.Value == right.Value;
public static bool operator !=(Percentage left, Percentage right)
=> left.Value != right.Value;
}
Performance Considerations
Generic math does not introduce runtime overhead compared to concrete implementations. The JIT compiler specialises each generic instantiation for value types, so Sum<int> compiles to the same machine code as a hand-written integer sum. There is no boxing, no virtual dispatch, and no indirection.
When to Use It
Generic math is most valuable when you are writing library code or utilities that logically apply to multiple numeric types. If you only ever work with double, there is no need to make your method generic. But if you find yourself duplicating arithmetic logic across int, long, and decimal variants, generic math is the right tool to collapse that duplication into a single, type-safe implementation.