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 type T implements addition, equality, and common properties.
  • T.Zero accesses the static property of the interface to retrieve the zero-value representation for whatever type T represents.

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 like Sqrt, Sin, and Log.

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 an OverflowException if the value cannot be represented by T.
  • T.CreateTruncating(value) discards excess bits if the value is too large.
  • T.CreateSaturating(value) clamps the value to the minimum or maximum bounds of T.

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.