Modern C# · Lesson 33

Generic math through static contracts

Call numeric identities and operators through INumber<T> constraints.

The concept and environment

Generic math combines numeric interfaces from .NET 7 with C# support for static abstract interface members introduced in C# 11. It is available in the current .NET 9 environment, even though it predates C# 12 and 13. Sum constrains its type parameter with INumber so that a numeric zero and addition are available through the type's contract. The implementation can then sum integers and decimals without dynamic binding, boxing every value into object, or maintaining an unrelated overload for every participating numeric type. The constraint describes capabilities rather than a single representation.

Walk through the portable program

Each call explicitly supplies the numeric type and a target ReadOnlySpan. The integer input contains two, three, and four, producing nine. The decimal input contains exact decimal values one point two five and two point seven five, producing four, formatted with two fractional digits using invariant culture. The empty integer span contributes no additions, leaving T.Zero unchanged. Output is int:9, decimal:4.00, and empty:0. The formatting decision is explicit and separate from the generic arithmetic, so the result does not depend on a device's current regional decimal separator.

A deeper design boundary

T.Zero is a static member reached through a constrained type parameter, and the addition expression is permitted because the numeric interface hierarchy supplies the required operator contract. An unconstrained T would not establish those operations. Choose the narrowest useful contract for a real library: INumber offers a broad numeric surface, while a specialized algorithm may need only an additive identity and an addition operator. A static interface requirement lets the compiler establish available operations; it does not prove that every user-defined implementation has the mathematical properties your algorithm assumes.

Pitfalls and limits

Different numeric types have different overflow, precision, and special-value behavior. Do not infer that one generic implementation makes integer wrapping, decimal overflow, and floating-point NaN equivalent. This bounded sample avoids those edge cases and uses ordinary addition. If a production sum requires checked arithmetic, saturation, or exact accumulation, specify and test that policy for its supported types. Also keep span lifetime separate from numeric generality: this synchronous function consumes its view during the call and does not retain it. Returning a computed scalar does not transfer ownership of the input collection.

Practice one: isolate a contract

First add an empty decimal call and predict a formatted 0.00, then replace the integer values with negative two, three, and four and predict five. Hint: write the expected identity and sum independently instead of computing the expectation by calling Sum again. Keep invariant formatting on decimal output. Next add a single-element case to distinguish the identity initialization from a mistaken implementation that skips the first element. These small examples challenge the loop and its starting state without relying on runtime-specific generic-code optimization measurements.

Practice two: challenge an assumption

For a second exercise, design a checked integer aggregation with a clearly documented overflow outcome, and add an input that exceeds the accepted range. Hint: do not assume a generic capability constraint chooses the domain's arithmetic policy. Compare that function with a bounded decimal use case whose scale is part of presentation rather than equality. If you narrow the generic constraint to only addition and identity, verify which syntax remains available and which INumber operations no longer compile. Keep compiler-contract experiments separate from the program's independently expected numeric results.

Review the resulting contract

Before adopting generic math, identify the required operations, the numeric types you promise to support, the empty-input identity, and the exceptional or special-value policy. Confirm that the selected target framework contains the relevant interfaces and that the compiler supports their static member contracts. The sample demonstrates genuine constrained numeric behavior on .NET 9, not a runtime type switch disguised as generic code. Its deterministic results establish the bounded example only; they do not justify claiming arbitrary numeric algorithms are correct merely because their operations share interface names.

A working example

using System;
using System.Globalization;
using System.Numerics;
T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
    T total = T.Zero;
    foreach (T value in values) total += value;
    return total;
}
Console.WriteLine($"int:{Sum<int>([2, 3, 4])}");
Console.WriteLine("decimal:" + Sum<decimal>([1.25m, 2.75m]).ToString("0.00", CultureInfo.InvariantCulture));
Console.WriteLine($"empty:{Sum<int>([])}");

Expected output

int:9
decimal:4.00
empty:0
Try this
Compare empty and nonempty input with explicit type arguments. Numeric capability constraints do not establish an overflow policy.

Further reading