Generic Math is easy to dismiss as a feature for numerical libraries. Its more important contribution is static abstraction: generic code can require operators, identities, parsing, and other type-level behavior without reflection, dynamic, or a runtime object hierarchy.
This guide builds the feature from first principles, examines the contracts in System.Numerics, and applies the same mechanics to parsing, generated database models, DDD types, architectural boundaries, tensors, and SIMD. It also covers the limitations and alternative designs, because static abstract constraints are powerful enough to become expensive when used casually.
What C# Generic Math Changed
Let’s briefly return to the past.
On November 8, 2022, .NET 7 shipped the first production release of Generic Math.
Later, I ran a poll in my Telegram channel. The results were personally disappointing.
Most developers did not use Generic Math and did not understand what the feature was for.
My goal here is therefore to give you the most complete explanation I could assemble from Microsoft’s public materials.
There is plenty to cover.
Why Generic Arithmetic Was Difficult Before .NET 7
Back in 2020, Kirill Maurin pointed out a frustrating limitation of C#: you could not write even the simplest generic summation function. This code fails to compile:
public static T Sum<T>(this T[] array, T initial)
{
var result = initial;
for (var i = 0; i < array.Length; i++)
result += array[i];
return result;
}
The compiler reports CS0019: “Operator '+=' cannot be applied to operands of type 'T' and 'T'.” The reason is straightforward. T tells the compiler almost nothing, while an overloaded operator is a static member of a type.
Intuition suggests adding a constraint that says the type supports addition:
public static T Sum<T>(this T[] array, T initial)
where T : +
{
var result = initial;
for (var i = 0; i < array.Length; i++)
result += array[i];
return result;
}
Now we get CS1031: “Type expected.” Constraints accept types, not arbitrary symbols, and C# had no way to express the presence of an operator.
Generic Math in C# and .NET
What did we eventually get? In short, an extension to both the language and the standard library.
The first working draft was presented to Microsoft’s Language Design Meeting on June 29, 2020. Miguel de Icaza and Aaron Bockover showed the result of a year’s work. The meeting notes are available on GitHub.
The draft later became a full proposal discussed by the C# community. Reception was generally positive, although commenters raised interesting objections: some feared C++-style complexity, while others could not see a production use case. The full discussion is in dotnet/csharplang issue 4436.
The language solved the problem by introducing static abstract interface members: static abstract. An interface can now define a contract for values that support addition, and a data type can implement it:
interface IAddable<T> where T : IAddable<T>
{
static abstract T Zero { get; }
static abstract T operator +(T t1, T t2);
}
struct Int32 : IAddable<Int32>
{
static int operator +(int x, int y) => x + y;
public static int Zero => 0;
}
We can finally write the sum. Its actual form is slightly different because .NET’s arithmetic abstractions live in System.Numerics:
public static T Sum<T>(this T[] array) where T :
IAdditiveIdentity<T, T>,
IAdditionOperators<T, T, T>
{
var result = T.AdditiveIdentity;
for (var i = 0; i < array.Length; i++)
result += array[i];
return result;
}
Two details are important. First, the generic constraint uses two contracts rather than one; we will return to the reason later. Second, both the feature and the framework interfaces make heavy use of the curiously recurring template pattern (CRTP).
.NET Numerics
This new part of the standard library contains abstractions for implementing numeric types. The interfaces are deliberately fine-grained, allowing you to assemble a custom derived abstraction from small building blocks.
The full architecture is captured in a huge UML diagram. It is too large to display well in an article, but the .svg is available in my Telegram channel.
The raster preview already hints at its size:
Open a built-in type such as int, or Int32, and follow its interfaces upward. You will find INumberBase, which describes an abstract number: an object on which abstract numeric operations can be performed. Surprisingly, the core operations themselves live in separate interfaces.
Core operations
-
IAdditionOperators.cs— addition
public interface IAdditionOperators<TSelf, TOther, TResult>
where TSelf : IAdditionOperators<TSelf, TOther, TResult>?
{
static abstract TResult operator +(TSelf left, TOther right);
static virtual TResult operator checked +(TSelf left, TOther right) => left + right;
}
-
IAdditiveIdentity.cs— additive identity, or zero
public interface IAdditiveIdentity<TSelf, TResult>
where TSelf : IAdditiveIdentity<TSelf, TResult>?
{
static abstract TResult AdditiveIdentity { get; }
}
-
IUnaryNegationOperators.cs— unary negation
public interface IUnaryNegationOperators<TSelf, TResult>
where TSelf : IUnaryNegationOperators<TSelf, TResult>?
{
static abstract TResult operator -(TSelf value);
static virtual TResult operator checked -(TSelf value) => -value;
}
-
ISubtractionOperators.cs— subtraction
public interface ISubtractionOperators<TSelf, TOther, TResult>
where TSelf : ISubtractionOperators<TSelf, TOther, TResult>?
{
static abstract TResult operator -(TSelf left, TOther right);
static virtual TResult operator checked -(TSelf left, TOther right) => left - right;
}
-
IMultiplyOperators.cs— multiplication
public interface IMultiplyOperators<TSelf, TOther, TResult>
where TSelf : IMultiplyOperators<TSelf, TOther, TResult>?
{
static abstract TResult operator *(TSelf left, TOther right);
static virtual TResult operator checked *(TSelf left, TOther right) => left * right;
}
-
IMultiplicativeIdentity.cs— multiplicative identity, or one
public interface IMultiplicativeIdentity<TSelf, TResult>
where TSelf : IMultiplicativeIdentity<TSelf, TResult>?
{
static abstract TResult MultiplicativeIdentity { get; }
}
.NET Numerics arrived as a BCL expansion through a series of pull requests to the .NET runtime: the filtered pull-request list is here.
One of the earliest and most important PRs is dotnet/runtime#54650. It covered several tasks:
- Creating numeric contracts. The interfaces provide abstractions for the operations shown above.
- Applying those contracts to built-in data types. If a type already supported
+, such asDateTime, it implemented the corresponding interface. - Adding runtime support in
System.Runtime.cs.
Notice how few lines the pull request removed: only 27.
How Static Abstract Interface Members Work
The compiler backend required no substantial changes.
The Intermediate Language restriction against using static and abstract together was removed. The platform had been prepared for the feature since at least C# 8, which introduced default interface implementations.
When the compiler generates IL for a call to an abstract static member, it emits a constrained. call sequence.
public static T Sum<T>(this T[] array) where T :
IAdditiveIdentity<T, T>,
IAdditionOperators<T, T, T>
{
var result = T.AdditiveIdentity;
for (var i = 0; i < array.Length; i++)
result += array[i];
return result;
}
Generic Math Uses Beyond Numerical Code
The standard library gained more than algebraic abstractions. It also received useful utility contracts, including a standardized way to turn strings into objects.
IParsable
public interface IParsable<TSelf>
where TSelf : IParsable<TSelf>?
{
static abstract TSelf Parse(string s, IFormatProvider? provider);
static abstract bool TryParse(
[NotNullWhen(true)] string? s,
IFormatProvider? provider,
[MaybeNullWhen(false)] out TSelf result);
}
At the API level, this is effectively a formalized Value Object contract. It is especially useful in ASP.NET Core. Suppose an endpoint accepts a date range encoded as a query-string value:
public class DateRange
{
public DateOnly? From { get; init; }
public DateOnly? To { get; init; }
}
Previously, even Microsoft’s documentation did not make the recommended string-to-object mechanism obvious. Should you use TypeConverter, IModelBinder, or something else? Every solution was fairly verbose and sometimes invasive. Here is my earlier IModelBinder implementation.
Old ASP.NET Core model binding
First, implement the model binder:
internal class DateRangeModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
ArgumentNullException.ThrowIfNull(bindingContext);
var fieldName = bindingContext.FieldName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
if (valueProviderResult == ValueProviderResult.None)
return Task.CompletedTask;
bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);
var value = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var segments = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var dtfi = new DateTimeFormatInfo
{
DateSeparator = "/"
};
if (segments.Length == 2
&& DateOnly.TryParse(segments[0], dtfi, out var fromDate)
&& DateOnly.TryParse(segments[1], dtfi, out var toDate))
{
var dateRange = new DateRange
{
From = fromDate,
To = toDate
};
bindingContext.Result = ModelBindingResult.Success(dateRange);
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
}
Then connect it to the endpoint. It is still unclear how to handle a date range nested inside another request object:
// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange(
[ModelBinder<DateRangeModelBinder>] DateRange range)
{
// ...
}
With the .NET 7 interfaces, implementing IParsable once is enough:
public class DateRange : IParsable<DateRange>
{
public DateOnly? From { get; init; }
public DateOnly? To { get; init; }
public static DateRange Parse(string value, IFormatProvider? provider)
{
if (!TryParse(value, provider, out var result))
{
throw new ArgumentException("Could not parse supplied value.", nameof(value));
}
return result;
}
public static bool TryParse(string? value,
IFormatProvider? provider, out DateRange dateRange)
{
var segments = value?.Split(
',',
StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& DateOnly.TryParse(segments[0], provider, out var fromDate)
&& DateOnly.TryParse(segments[1], provider, out var toDate))
{
dateRange = new DateRange { From = fromDate, To = toDate };
return true;
}
dateRange = new DateRange { From = default, To = default };
return false;
}
}
The endpoint becomes:
// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
// ...
}
Designing a Cache-Like Store
Suppose we need a cache-like store whose key depends on the type of the stored item. Before .NET 7, the options all had drawbacks.
- An instance contract can require every object to expose a key, but it cannot retrieve an object from the cache when no instance exists.
interface IResource
{
string CacheKey { get; }
}
- An attribute can mark cacheable types, but reading its metadata through reflection costs performance.
[CacheKey(nameof(MyResource) + "suffix")]
record MyResource;
class CacheKeyAttribute(string value) : Attribute
{
public string Value { get; } = value;
}
- A separate provider service can hold the logic, but distributing and exposing it cleanly becomes difficult.
abstract class KeyProvider
{
public abstract string Key { get; }
}
abstract class Resource<TKeyProvider>
where TKeyProvider : KeyProvider, new()
{
public string CacheKey =>
new TKeyProvider().Key;
}
- An explicit type-to-key map requires ongoing configuration maintenance and loses type safety.
class Cache(IReadOnlyDictionary<Type, string> resourceTypeToKeyMap);
Static abstract members make the design straightforward: define a contract and put information shared by all instances on the type itself.
interface IResource
{
//...
static abstract string CacheKey { get; }
}
class Cache
{
public TResource Get<TResource>() where TResource : IResource
{
string cacheKey = TResource.CacheKey;
// ...
return //...;
}
public void Set<TResource>(TResource resource) where TResource : IResource
{
string cacheKey = TResource.CacheKey;
// ...
}
}
Static Abstraction Patterns in C
We can now design patterns for algorithms that need information at the type level but have no object instance. The result is type-safe, more general code:
public interface IAsyncFactory<T>
{
static abstract Task<T> CreateAsync();
}
public interface IExampleStrategy
{
static abstract bool IsEnabled(string foo);
void DoStuff(string foo);
}
Integrating with Generic Attributes
As I demonstrated in an article about unit tests, static members on abstract contracts can extend attribute behavior by invoking logic from a constructor’s static context:
public interface IFixtureCustomizer
{
static abstract void Customize(IFixture fixture);
}
public class AutoDataAttribute<TFixtureCustomizer>() :
AutoDataAttribute(
fixtureFactory: () =>
{
var fixture = new Fixture();
TFixtureCustomizer.Customize(fixture);
return fixture;
}) where TFixtureCustomizer : IFixtureCustomizer;
Generic Math for Domain-Driven Design
Static abstract members also let us reconsider how we design entities in C# applications that use Domain-Driven Design. A common practice is for every operation on an entity to emit a domain event that can be handled elsewhere:
interface IDomainEvent;
interface IEntityCreated<T> : IDomainEvent
{
T Entity { get; }
}
record CustomerCreated(Customer Entity) : IEntityCreated<Customer>;
Constructors are often avoided for entity creation because they cannot return an event. Static factory methods can return whatever the domain requires:
record CustomerCreateDto(string Name, DateTimeOffset DateOfBirth);
class Customer(Guid id, string name, DateTimeOffset? dateOfBirth)
{
public static IEntityCreated<Customer> Create(CustomerCreateDto createDto) =>
new CustomerCreated(
new Customer(
id: Guid.NewGuid(),
createDto.Name,
createDto.DateOfBirth));
}
We can generalize creation for Customer, and for any other entity, while expressing the domain’s creation scenarios in the type system—a genuine ubiquitous language:
interface IFactory<TEntity, TEntityCreateDto>
where TEntity : class, IFactory<TEntity, TEntityCreateDto>
where TEntityCreateDto : class
{
static abstract IEntityCreated<TEntity> Create(
TEntityCreateDto createDto);
}
Because a customer’s birth date is optional, we can describe two creation scenarios: one with a date and one without.
record CustomerCreateDto(string Name, DateTimeOffset DateOfBirth);
record CustomerCreateWithoutDateOfBirthDto(string Name);
class Customer(Guid id, string name, DateTimeOffset? dateOfBirth) :
IFactory<Customer, CustomerCreateDto>,
IFactory<Customer, CustomerCreateWithoutDateOfBirthDto>
{
public static IEntityCreated<Customer> Create(
CustomerCreateDto createDto) =>
new CustomerCreated(
new Customer(
id: Guid.NewGuid(),
createDto.Name,
createDto.DateOfBirth));
public static IEntityCreated<Customer> Create(
CustomerCreateWithoutDateOfBirthDto createDto) =>
new CustomerCreated(
new Customer(
id: Guid.NewGuid(),
createDto.Name,
dateOfBirth: null));
}
Contracts for Generated Database Models
Suppose a project uses Database First and generates C# classes from an existing schema. A documents table is represented by a generated Document class:
partial class Document
{
public required string Name { get; set; }
// ...
public static int NameLengthConstraint { get; }
}
We want an interface containing information about the corresponding columns. An instance contract for the columns looks obvious:
interface IDocument
{
// ...
public string Name { get; set; }
}
partial class Document : IDocument;
But the generator also produces static fields describing constraints, such as the maximum string length. Ordinary interfaces could not include those values. static abstract can:
interface IDocumentConstraints<TDocument>
where TDocument : IDocumentConstraints<TDocument>
{
static abstract int NameLengthConstraint { get; }
}
partial class Document :
IDocument,
IDocumentConstraints<Document>;
Static Interfaces as an Architectural Boundary
Consider a DDD and Clean Architecture solution in which subdomains are separated into projects.
A domain entity lives in HydraScript.Domain.FrontEnd. Its implementation needs a regular expression produced by a source generator in HydraScript.Infrastructure.LexeRegexGenerator. How can the implementation cross that boundary cleanly? The same tactical DDD patterns can be applied in a new form. First define a regular-expression container, inject it into the domain entity through a static abstract contract, and wire the closed generic type through dependency injection.
public interface IStructure : IEnumerable<TokenType>
{
public Regex Regex { get; }
public TokenType FindByTag(string tag);
}
public interface IGeneratedRegexContainer
{
public static abstract Regex Regex { get; }
}
public class Structure<TContainer>(ITokenTypesProvider provider) : IStructure
where TContainer : IGeneratedRegexContainer
{
// ...
public Regex Regex { get; } = TContainer.Regex;
}
// ...
services.AddSingleton<IStructure, Structure<GeneratedRegexContainer>>();
A Note on ML.NET
ML.NET brings machine learning to .NET applications in both online and offline scenarios. Its central abstraction is a machine-learning model: a black box that, after training, produces an output for a given input.
You can train a model with ML.NET or import a pretrained model in the ONNX format—the closest thing the neural-network ecosystem has to a widely shared interchange format. In practice, however, models are rarely trained on .NET because the tooling and algorithm implementations remain limited.
Dmitry Soshnikov’s talk referenced in the original article discusses ML.NET’s problems.
After Generic Math arrived, a large-scale framework refactoring opened the door to deep learning on .NET (dotnet/machinelearning#6664).
Spans, Tensors, and SIMD
The AI problem is also being approached through System.Numerics.Tensors, a NuGet package for tensor computations. The broader direction only became clear over time.
.NET 9 introduced the Tensor type and expanded TensorPrimitives, which now provides generic operations over vector data exposed through Span wrappers.
These methods use the CPU’s available SIMD acceleration, including AVX and SSE.
public class ManhattanDistance<T> : IDistanceCalculator<T>
where T : unmanaged, INumberBase<T>
{
public double ComputeDistance(T[] attributesOne, T[] attributesTwo)
{
Span<T> diff = stackalloc T[Math.Min(attributesOne.Length, attributesTwo.Length)];
TensorPrimitives.Subtract(attributesOne, attributesTwo, diff);
var l1Norm = TensorPrimitives.SumOfMagnitudes<T>(diff);
return double.CreateTruncating(l1Norm);
}
}
This looks like a long-running, multi-step strategy involving Generic Math, Span, stackalloc, and more. It addresses several goals at once:
- OOP developers get a powerful architectural tool that replaces singleton workarounds and awkward attempts to simulate ad hoc polymorphism.
- Microsoft moves closer to a competitive machine-learning platform, including deep-model development and the possibility of training neural networks in C# rather than Python.
- Performance-focused developers get accelerated computations over
Span, which the JIT does not optimize in exactly the same way as arrays or lists.
Advantages of C# Generic Math
First, Generic Math solves the long-standing inability to abstract over arithmetic operations. Microsoft is now actively filling the corresponding holes in platform APIs.
Second, it provides a powerful advanced-design tool—possibly one ahead of its time.
Third, as the alternatives and benchmark below demonstrate, static abstract members do not introduce a performance penalty in C# code.
Limitations of C# Generic Math
First, Generic Math works only when the participating type completes the entire contract: you own the contract and the implementation. You cannot retroactively apply an abstraction to third-party code. Suppose I combine two interfaces into a monoid-like IAdditive contract and implement it for a custom string wrapper:
interface IAdditive<TAdditive> :
IAdditiveIdentity<TAdditive, TAdditive>,
IAdditionOperators<TAdditive, TAdditive, TAdditive>
where TAdditive : IAdditive<TAdditive>;
class AdditiveString(string s) : IAdditive<AdditiveString>
{
private readonly string _string = s;
public static AdditiveString AdditiveIdentity => new(string.Empty);
public static AdditiveString operator +(AdditiveString left, AdditiveString right) =>
new(left._string + right._string);
}
The custom type works, but built-in types are excluded even when they appear to satisfy the same requirements—+ and zero.
The two practical workarounds are wrappers and large generic constraint lists. A language feature that might have solved this was implicit interface: adding implicit to the interface declaration would make the code in the screenshot compile. The idea appeared in 2015, but discussion stalled in 2020 and the proposal became “Likely Never” (dotnet/csharplang#110).
Second, an abstract base class cannot defer implementation of abstract static members. This code does not compile:
abstract class AdditiveBase<TAdditive> : IAdditive<TAdditive>
where TAdditive : IAdditive<TAdditive>
{
public abstract static TAdditive AdditiveIdentity { get; }
public abstract static TAdditive operator +(TAdditive left, TAdditive right);
}
The restriction is understandable: abstract static members are not polymorphic in the same way as instance members.
Alternatives to C# Generic Math
We have covered Generic Math, production and exploratory use cases, strengths, and limitations. Now let’s compare it with other language designs and with approaches available inside C# itself. First, a short detour into three kinds of polymorphism.
Three Kinds of Polymorphism
Polymorphism means more than object-oriented inheritance. Many kinds exist; we need three of them.
Suppose we need a printer that can print several kinds of object. The design depends on which form of polymorphism we choose.
- Subtype polymorphism is the familiar OOP model: define a base abstraction and implement it in derived types.
interface IPrintable
{
string Content { get; }
}
interface IPrinter
{
void Print(IPrintable printable);
}
- Parametric polymorphism is implemented in C# through generics. The implementation is parameterized by something—in this case, a type.
interface IPrinter<in T>
{
void Print(T item);
}
- Ad hoc polymorphism appears in C# as method overloading. The compiler selects the required implementation.
interface IPrinter
{
void Print(int i);
void Print(string s);
void Print(bool b);
}
Ad hoc polymorphism comes closest to the static generic abstraction we want, but it raises another question: how can the compiler provide one universal contract instead of many overloads? One answer is the pattern known as...
Type Classes
Type classes separate operations from data. The approach comes from functional programming and is built into some functional languages, including Haskell.
In C#, we can model the pattern with generics and structs. Here is the printer example:
class Printer
{
public void Print<T, TPrintable>(T item)
where TPrintable : struct, IPrintable<T>
{
var content = default(TPrintable).GetContent(item);
Console.WriteLine(content);
}
}
interface IPrintable<T>
{
string GetContent(T item);
}
struct PrintableInt : IPrintable<int>
{
public string GetContent(int item) =>
item.ToString();
}
struct PrintableBool : IPrintable<bool>
{
public string GetContent(bool item) =>
item ? "true" : "false";
}
var printer = new Printer();
printer.Print<int, PrintableInt>(13);
printer.Print<bool, PrintableBool>(false);
Armed with that technique, we can solve the generic-summation problem in a functional style:
interface IAdder<T>
{
T Zero { get; }
T Plus(T left, T right);
}
public static T SumTypeClass<T, TAdder>(
this IReadOnlyList<T> array,
TAdder adder = default)
where TAdder : struct, IAdder<T>
{
var result = adder.Zero;
var count = array.Count;
for (var i = 0; i < count; i++)
result = adder.Plus(result, array[i]);
return result;
}
int[] array = [1, 2, 3];
var sum = array.SumTypeClass<int, IntAdder>();
struct IntAdder : IAdder<int>
{
public int Zero => 0;
public int Plus(int left, int right) =>
left + right;
}
The code is efficient and reasonably clean, but it creates a new problem: callers must explicitly specify both generic type arguments to SumTypeClass. C# inference flows from source to destination and cannot infer the evidence type here. Explicitly typed extension wrappers can hide the extra argument.
Could C# have gained type classes directly? In 2017, a proposal named “Type Classes for the Masses” appeared (discussion).
It would have introduced a new language entity: the concept.
A concept would act as a contract implemented by an instance. The implicit keyword would solve the explicit-type-argument problem by resolving the type from context.
concept Num<A>
{
A operator +(A a, A b);
A operator *(A a, A b);
A operator -(A a, A b);
implicit operator A(int i);
}
instance NumInt
{
int operator +(int a, int b) => a + b;
int operator *(int a, int b) => a * b;
int operator -(int a, int b) => a – b;
implicit operator int(int i) => i;
}
public static A F<A, implicit NumA>(A x)
where NumA : Num<A> =>
x * x + x + 666;
The proposal is “Likely Never” for reasons we can only speculate about. Some believe it was a political decision to follow the OOP route to the end. Others blame the association with Scala’s problematic implicits: perhaps .NET should fix the JVM ecosystem’s mistakes rather than repeat them.
trait Comparator[A]:
def compare(x: A, y: A): Int
object Comparator:
given Comparator[Int] with
def compare(x: Int, y: Int): Int = Integer.compare(x, y)
given Comparator[String] with
def compare(x: String, y: String): Int = x.compareTo(y)
end Comparator
def max[A](x: A, y: A)(using comparator: Comparator[A]): A =
if comparator.compare(x, y) >= 0 then x
else y
println(max(10, 6)) // 10
println(max("hello", "world")) // world
For more about ad hoc polymorphism and type classes:
- Ad Hoc Polymorphism and the Type Class Pattern in C#
- language-ext documentation
- Kirill Maurin’s DotNext 2020 talk
Swift’s Alternative
Let’s briefly visit iOS development and examine another form of generic arithmetic. Swift’s counterpart to interfaces is the protocol:
protocol Animal {
var maxBabiesCount: Int { get }
func getSound() -> String
}
class Cat : Animal {
let maxBabiesCount: Int = 5
func getSound() -> String {
"Meow"
}
}
A protocol can declare an associated type, which is roughly analogous to parameterizing an interface:
protocol Animal {
associatedtype BabyType
var babies: [BabyType] { get set }
}
class Cat : Animal {
var babies = [Kitten]()
}
class Kitten { }
A protocol can also require an associated type to be the implementing type itself. For example, animals may mate only with their own kind: cats with cats and cows with cows.
protocol Animal {
func mate(with: Self)
}
class Cat : Animal {
func mate(with: Cat) {
print("mating with another cat")
}
}
Swift was therefore prepared at the language level for a generic arithmetic library: protocols support both Self and static members. This led to the Swift Numerics proposal, authored by LLVM creator Chris Lattner.
public protocol AdditiveArithmetic : Equatable {
static var zero: Self { get }
static func + (lhs: Self, rhs: Self) -> Self
}
The main difference from .NET is that Swift groups arithmetic properties much like algebraic structures do. The lack of fine-grained interfaces may be related to Swift’s ability to apply abstractions to external code, although I cannot say for certain.
Performance
Now that we have alternatives to compare, let’s benchmark C# Generic Math summation against the type-class implementation, a classic loop, and LINQ.
Benchmark configuration:
- BenchmarkDotNet v0.13.12
- macOS Monterey 12.3
- Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
- .NET SDK 8.0.100
Benchmark code
struct IntAdder : IAdder<int>
{
public int Zero => 0;
public int Plus(int left, int right) =>
left + right;
}
interface IAdder<T>
{
T Zero { get; }
T Plus(T left, T right);
}
static class ArrayExtensions
{
public static T SumTypeClass<T, TAdder>(
this IReadOnlyList<T> array,
TAdder adder = default)
where TAdder : struct, IAdder<T>
{
var result = adder.Zero;
var count = array.Count;
for (var i = 0; i < count; i++)
result = adder.Plus(result, array[i]);
return result;
}
public static T SumGenericMath<T>(
this IReadOnlyList<T> array) where T :
IAdditiveIdentity<T, T>,
IAdditionOperators<T, T, T>
{
var result = T.AdditiveIdentity;
var count = array.Count;
for (var i = 0; i < count; i++)
result += array[i];
return result;
}
}
[SimpleJob]
[SuppressMessage("ReSharper", "UnassignedField.Global")]
#pragma warning disable CA1050
public class SumBenchmarks
#pragma warning restore CA1050
{
private IReadOnlyList<int> _dataSet = [];
private readonly Consumer _consumer = new();
[Params(100_000)]
public int CollectionCount;
[Params(CollectionType.Array, CollectionType.ImmutableArray)]
public CollectionType CollectionType;
[GlobalSetup]
public void GlobalSetup()
{
var enumerable = Enumerable.Range(1, CollectionCount)
.Select(_ => Random.Shared.Next(1, 10001));
_dataSet = CollectionType switch
{
CollectionType.Array => enumerable.ToArray(),
CollectionType.ImmutableArray => enumerable.ToImmutableArray(),
_ => throw new ArgumentOutOfRangeException(nameof(CollectionType))
};
}
[Benchmark(Baseline = true)]
public void SumLinq()
{
var sum = _dataSet.Sum();
_consumer.Consume(sum);
}
[Benchmark]
public void SumClassicFor()
{
var sum = 0;
for (var i = 0; i < CollectionCount; i++)
sum += _dataSet[i];
_consumer.Consume(sum);
}
[Benchmark]
public void SumTypeClass()
{
var sum = _dataSet.SumTypeClass<int, IntAdder>();
_consumer.Consume(sum);
}
[Benchmark]
public void SumGenericMath()
{
var sum = _dataSet.SumGenericMath();
_consumer.Consume(sum);
}
}
#pragma warning disable CA1050
public enum CollectionType
#pragma warning restore CA1050
{
Array,
ImmutableArray
}
The result was interesting: LINQ beat our summation functions. Looking at the source revealed why. The array and list paths use optimizations based on Span and marshalling.
Further Reading
The following resources complement this article.
Two books:
- A. I. Kostrikin, Introduction to Algebra
- Michel Minoux and Michel Gondran, Graphs, Dioids and Semirings: New Models and Algorithms
Two talks about monoids and the use of algebra at Stripe:
- Life After Monoids
- Add ALL the Things
Finally, you can study—and perhaps port to C#—Twitter’s Scala abstract-algebra library, Algebird:
- github.com/twitter/algebird
- www.michael-noll.com/blog/2013/12/02/twitter-algebird-monoid-monad-for-large-scala-data-analytics
Takeaways
Generic Math is worth considering when an algorithm needs capabilities that can be stated as compile-time contracts: operators, identities, parsing, or other static members. It can remove reflection and runtime dispatch while keeping algorithms reusable across built-in and domain-specific types.
The cost is a denser public API, longer constraint lists, and abstractions that many C# developers have not yet internalized. Keep the constraints as small as the algorithm allows, expose domain-focused interfaces when raw numeric contracts leak too far, and benchmark the complete workload rather than assuming generic code is automatically faster.
Related C# Content
Follow StepOne on GitHub for open-source C#/.NET projects, compiler experiments, production-focused examples, and new releases.











Top comments (0)