C# & .NET (C# 14, .NET 10 LTS) Module 11 – Generics
Generic math and static abstract members
Write one C# method for int, long, decimal and double with INumber<T>, declare static abstract interface members, and convert and add numbers without overflow.
What you will learn
- Write numeric methods once for int, long, decimal and double with INumber<T>
- Declare and implement static abstract interface members, and call them on a type parameter
- Choose between CreateChecked, CreateSaturating and CreateTruncating, and keep generic sums from overflowing
- Implement operator interfaces and IParsable<T> so that a type of your own works with generic math
- Explain what generic math costs when it runs, and where its interfaces cannot be used
Before you start
What you’ll learn · Before you start
What you will learn
- Write numeric methods once for int, long, decimal and double with INumber<T>
- Declare and implement static abstract interface members, and call them on a type parameter
- Choose between CreateChecked, CreateSaturating and CreateTruncating, and keep generic sums from overflowing
- Implement operator interfaces and IParsable<T> so that a type of your own works with generic math
- Explain what generic math costs when it runs, and where its interfaces cannot be used
Before you start
On this page
A sales report adds int units, decimal rupees and double kilograms, and before .NET 7 that meant three Sum
methods with the same loop, three Average methods, and three more for every new numeric type. With generic math one
Sum<T> serves them all: on .NET 10 it added this lesson’s data to 445 units, 4749.49 rupees and 15.25 kg.
You will write numeric methods once for every number type, see the static abstract members that make it possible, and declare one of your own.
One method adds every numeric type when T is an INumber<T>
generic math: Numeric code written once for every number type through interfaces of System.Numerics, such as INumber<T>, which the built-in numeric types implement since .NET 7. Generic code then uses +, T.Zero and other members on its type parameter. is a set of interfaces in System.Numerics, with INumber<TSelf> at its
centre, that every built-in numeric type implements since .NET 7. A method constrained with
where T : INumber<T> may use +, -, *, / and comparisons on T, and start from T.Zero. sum.cs writes
Sum and Average once and calls them with four types:
// sum.cs: one Sum and one Average for every numeric type. where T : INumber<T> gives T the operators + and /, and
// static members such as T.Zero and T.CreateChecked, which the methods call on the type itself.
using System.Numerics;
int[] units = [120, 85, 240];
decimal[] rupees = [1250.50m, 499.00m, 2999.99m];
double[] kilos = [2.5, 0.75, 12.0];
long[] views = [4_000_000_000, 3_500_000_000];
Console.WriteLine($"int sum {Sum<int>(units)}, average {Average<int>(units)}");
Console.WriteLine($"decimal sum {Sum<decimal>(rupees)}, average {Average<decimal>(rupees)}");
Console.WriteLine($"double sum {Sum<double>(kilos)}, average {Average<double>(kilos)}");
Console.WriteLine($"long sum {Sum<long>(views)}, average {Average<long>(views)}");
static T Sum<T>(ReadOnlySpan<T> values)
where T : INumber<T>
{
T total = T.Zero;
foreach (T value in values)
{
total += value;
}
return total;
}
static T Average<T>(ReadOnlySpan<T> values)
where T : INumber<T> => Sum(values) / T.CreateChecked(values.Length); Output
int sum 445, average 148 decimal sum 4749.49, average 1583.1633333333333333333333333 double sum 15.25, average 5.083333333333333 long sum 7500000000, average 3750000000
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run sum.cs
Every type kept its own arithmetic, because inside Sum<int> the + is the int addition:
| Type | Sum | Average | What decided the average |
|---|---|---|---|
int |
445 | 148 | integer division drops the fraction of 148.33 |
decimal |
4749.49 | 1583.1633… | 29 significant digits in the output, decimal’s precision |
double |
15.25 | 5.083333333333333 | binary floating point, about 16 digits |
long |
7500000000 | 3750000000 | values past int.MaxValue add without trouble |
Average divides by T.CreateChecked(values.Length), which turns the int count into a T: 3 becomes 3m for
decimal and 3.0 for double. CreateChecked throws OverflowException when a value does not fit the target type,
which for a count of items never happens here.
Expecting Average<int> to keep the fraction
Average<int>([120, 85, 240]) is 148, not 148.33, because generic math uses the type’s own division, and int
division drops the fraction. When the result needs another type than the inputs, give the method a second type
parameter for the result, or call it with decimal or double values.
Checkpoint
Check: Numeric methods written once with INumber<T>
Static abstract members put operators and constants on the type, not on an object
T.Zero is a static member read on a type parameter, which C# could not do before C# 11. The feature that makes it
possible is static abstract member: A static member that an interface declares and every implementing type must supply, allowed since C# 11. Generic code calls it on a type parameter, as T.Parse(text). More in “Interfaces, explicit implementation and defaults”: an interface may declare static members
without a body, and every type that implements the interface must supply them. Static abstract members are how
INumber<T> carries operators such as +, constants such as Zero and One, and factories such as CreateChecked.
members.cs reads the static members of five types through one method, and declares an interface of its own:
// members.cs: INumber<T> and IMinMaxValue<T> declare static abstract members. Describe reads them on the type
// parameter itself, with no value of the type at all. IQuantity<TSelf> declares static abstract members of its own,
// an operator among them, which is why it needs the constraint where TSelf : IQuantity<TSelf>.
using System.Numerics;
Console.WriteLine(Describe<byte>());
Console.WriteLine(Describe<int>());
Console.WriteLine(Describe<long>());
Console.WriteLine(Describe<decimal>());
Console.WriteLine(Describe<double>());
Console.WriteLine(Add(new Kilograms(2.5), new Kilograms(0.75)));
Console.WriteLine(Add(new Litres(0.75), new Litres(1.5)));
static string Describe<T>()
where T : INumber<T>, IMinMaxValue<T> =>
$"{typeof(T).Name,-8} zero {T.Zero}, one {T.One}, max {T.MaxValue}";
static string Add<T>(T first, T second)
where T : IQuantity<T> => $"{(first + second).Value} {T.Symbol}";
interface IQuantity<TSelf>
where TSelf : IQuantity<TSelf>
{
static abstract string Symbol { get; }
double Value { get; }
static abstract TSelf operator +(TSelf left, TSelf right);
}
readonly record struct Kilograms(double Value) : IQuantity<Kilograms>
{
public static string Symbol => "kg";
public static Kilograms operator +(Kilograms left, Kilograms right) => new(left.Value + right.Value);
}
readonly record struct Litres(double Value) : IQuantity<Litres>
{
public static string Symbol => "L";
public static Litres operator +(Litres left, Litres right) => new(left.Value + right.Value);
} Output
Byte zero 0, one 1, max 255 Int32 zero 0, one 1, max 2147483647 Int64 zero 0, one 1, max 9223372036854775807 Decimal zero 0, one 1, max 79228162514264337593543950335 Double zero 0, one 1, max 1.7976931348623157E+308 3.25 kg 2.25 L
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run members.cs
Describe<T>() has no argument and no object of type T, yet it printed the zero, one and maximum of byte, int,
long, decimal and double. Step through how it reaches T.MaxValue:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- No object at all. Describe<T>() takes no arguments, so there is no value to call a member on. It reads T.MaxValue on the type parameter itself.
- The constraint allows it. IMinMaxValue<T> declares MaxValue as a static abstract member, so every type that implements the interface must supply it, and the compiler lets generic code read T.MaxValue.
- Describe<int>(). The first call supplies int as T. int implements IMinMaxValue<int>, so T.MaxValue is int.MaxValue, and members.cs printed 2147483647.
- Describe<decimal>(). A call with decimal reads decimal.MaxValue through the same line of code, 79228162514264337593543950335, a number with 29 digits.
- Chosen by the type argument. Which MaxValue runs is decided by the type argument of each call, not by an object, so the same method reads the members of byte, long and double as well.
IQuantity<TSelf> shows the declaring side. Its + takes and returns the implementing type, and an interface may
declare such an operator only when its type parameter is constrained to the interface itself,
where TSelf : IQuantity<TSelf>, a self-referencing constraint: A constraint such as where TSelf : IQuantity<TSelf>, in which a type parameter must implement the interface that declares it. It lets the interface's static abstract members, operators among them, take and return the implementing type.;
Microsoft’s tutorial on static virtual members explains that without it the operator could not be declared.
Kilograms and Litres each supply Symbol and +, and Add turned 2.5 kg and 0.75 kg into 3.25 kg.
Generic math offers more than Zero and the operators:
CreateCheckedconverts a number intoTand throwsOverflowExceptionwhen it does not fit.CreateSaturatingconverts and clamps a value that does not fit toT.MinValueorT.MaxValue.CreateTruncatingconverts and keeps only the low bits of an integer that does not fit.- Inside
checked(…),+onTcalls the type’s checked operator: A user-defined operator declared with the checked keyword, such as operator checked +. A checked context calls it and an unchecked context calls the ordinary operator, which a type with a checked operator must also declare., so anintsum that passesint.MaxValuethrowsOverflowExceptioninstead of wrapping. A type that declares a checked operator must declare the ordinary one too. IParsable<TSelf>declares staticParseandTryParse, so generic code can read text into any parsable type.
Key idea
Static abstract members make the type itself keep a promise: generic code calls T.Zero, T.MaxValue or + on the
type parameter, and each type argument supplies its own.
Checkpoint
Check: Static abstract members on a type parameter
Converting numbers and summing without overflow, measured Pro: opens with a Pro, Premium or Ultimate pass
Every generic numeric algorithm converts numbers and adds them, and both can lose information without a word. The run below converts the same eight values three ways and reproduces an average that comes out wrong by more than a billion.
In this part:
- Three ways to convert the same eight values
- An average that comes out wrong by more than a billion
1 runnable example (C#) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Your own type in generic math: Money with operators, IParsable and checked + Pro: opens with a Pro, Premium or Ultimate pass
A type of your own joins generic math by implementing the operator interfaces that generic code asks for. The run below adds and parses amounts in paise, and shows the overflow that slips through when a checked operator is missing.
In this part:
- Ask for the operations you use, not for INumber<T>
- A missing checked operator lets overflow through
1 runnable example (C#) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
What generic math costs when it runs, and where its interfaces cannot go Pro: opens with a Pro, Premium or Ultimate pass
Generic code is often suspected of being slow, and generic math of being slower still. The run below measures what
Sum<int> allocates, and the build after it shows the two places where these interfaces cannot be used.
In this part:
- No allocation for Sum<int>, 24 bytes per value for a sum over objects
- Two uses the compiler refuses
3 runnable examples (C#, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: statistics for every numeric type
stats.cs checks a Statistics<T> with Mean, Range and Percentile on int, long, decimal and double.
The starter’s Mean wraps on overflow and divides by zero for an empty input, and Percentile is not written yet, so
it passes 5 of the 10 checks:
// stats.cs: practice. Statistics<T> works for every numeric type through INumber<T>. Mean must refuse an empty input
// and throw OverflowException instead of wrapping; Percentile sorts the values and takes the one at rank
// ceil(percent × count / 100), the nearest-rank method. Rewrite Statistics<T> at the end of this file until all ten
// checks pass.
using System.Numerics;
(string Check, Func<string> Run, string Expected)[] checks =
[
("Mean of ints", () => $"{Statistics<int>.Mean([2, 4, 9])}", "5"),
("Mean of decimals", () => $"{Statistics<decimal>.Mean([10.5m, 20.25m])}", "15.375"),
("Mean of doubles", () => $"{Statistics<double>.Mean([0.5, 0.25])}", "0.375"),
("Mean of longs past int.MaxValue", () => $"{Statistics<long>.Mean([3_000_000_000, 5_000_000_000])}", "4000000000"),
("Mean of nothing throws", () => Throws(() => Statistics<int>.Mean([])), "InvalidOperationException"),
("an int sum that overflows throws", () => Throws(() => Statistics<int>.Mean([int.MaxValue, int.MaxValue])), "OverflowException"),
("Range of ints", () => $"{Statistics<int>.Range([3, 9, 4])}", "6"),
("Percentile 50 by nearest rank", () => $"{Statistics<int>.Percentile([15, 20, 35, 40, 50], 50)}", "35"),
("Percentile 90 of unsorted values", () => $"{Statistics<int>.Percentile([50, 15, 40, 20, 35], 90)}", "50"),
("Percentile of decimals", () => $"{Statistics<decimal>.Percentile([2.5m, 1.5m], 50)}", "1.5"),
];
int passed = 0;
foreach (var (check, run, expected) in checks)
{
string got;
try
{
got = run();
}
catch (Exception e)
{
got = $"throws {e.GetType().Name}";
}
bool ok = got == expected;
if (ok)
{
passed++;
}
Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, got {got}, expected {expected}");
}
Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;
static string Throws(Func<object> action)
{
try
{
return $"returned {action()}";
}
catch (Exception e)
{
return e.GetType().Name;
}
}
// The starter's Mean wraps on overflow and divides by zero on an empty input, and Percentile is not written yet.
static class Statistics<T>
where T : INumber<T>
{
public static T Mean(ReadOnlySpan<T> values)
{
T total = T.Zero;
foreach (T value in values)
{
total += value;
}
return total / T.CreateChecked(values.Length);
}
public static T Range(ReadOnlySpan<T> values)
{
T min = values[0];
T max = values[0];
foreach (T value in values)
{
min = T.Min(min, value);
max = T.Max(max, value);
}
return max - min;
}
public static T Percentile(ReadOnlySpan<T> values, int percent) => throw new NotImplementedException();
} Output (exit status 1)
Mean of ints: ok Mean of decimals: ok Mean of doubles: ok Mean of longs past int.MaxValue: ok Mean of nothing throws: FAIL, got DivideByZeroException, expected InvalidOperationException an int sum that overflows throws: FAIL, got returned -1, expected OverflowException Range of ints: ok Percentile 50 by nearest rank: FAIL, got throws NotImplementedException, expected 35 Percentile 90 of unsorted values: FAIL, got throws NotImplementedException, expected 50 Percentile of decimals: FAIL, got throws NotImplementedException, expected 1.5 5 of 10 checks pass
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run stats.cs
Make Mean safe and write Percentile
Copy stats.cs. In Mean, throw InvalidOperationException for an empty span and add with
checked(total + value). Write Percentile by the nearest-rank method: copy the values to an array, sort it, and
return the item at rank ceil(percent × count / 100), counting from 1. Run dotnet run stats.cs until it prints
10 of 10 checks pass.
Then check the ideas of the first two sections:
Practice that adapts to you
Practise this lesson
7 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
2-5 years · What is generic math in .NET, and what made it possible?
Generic math lets one method work with every numeric type, written once with a constraint such as
where T : INumber<T>. Before .NET 7 a Sum for int, long, decimal and double meant four copies, because C#
had no way to say “a type with a +”: operators are static, and an interface could not require a static member of
the types that implement it. C# 11 added static abstract members, which let an interface require static members, operators included, and .NET 7 shipped
interfaces such as INumber<T>, IAdditionOperators<T, T, T> and IParsable<T> that the built-in numbers implement.
Generic code then calls T.Zero, + or T.CreateChecked(n) on the type parameter, and each type supplies its own.
-
Mid to senior
A generic Average<T> where T : INumber<T> returns 568,344,234 for three readings of 2,000,000,000 stored as int, and no exception is thrown. Explain the result exactly, and give two fixes with the case each one suits.
-
Mid to senior
A Money type kept in paise must work with a generic Total<T> used across the codebase and with a generic ParseAll<T>. Which interfaces do you implement, which do you avoid, and how do you make sure that an overflow is never silent?
-
Senior to staff
A teammate argues that generic math is slow because every + goes through an interface, and wants hand-written Sum methods for int, long and double. How do you answer, and what would you measure before deciding?
Key takeaways
where T : INumber<T>lets oneSumorAverageserveint,long,decimalanddouble, and each type keeps its own arithmetic, so anintaverage drops its fraction.- Static abstract members, added in C# 11, let an interface require static members such as
Zero,MaxValueand operators, which generic code calls on the type parameter itself. - An interface whose static abstract operators take the implementing type needs a self-referencing constraint, such
as
where TSelf : IQuantity<TSelf>.
Terms in this lesson
- checked operator
- A user-defined operator declared with the checked keyword, such as operator checked +. A checked context calls it and an unchecked context calls the ordinary operator, which a type with a checked operator must also declare.
- generic math
- Numeric code written once for every number type through interfaces of System.Numerics, such as INumber<T>, which the built-in numeric types implement since .NET 7. Generic code then uses +, T.Zero and other members on its type parameter.
- self-referencing constraint
- A constraint such as where TSelf : IQuantity<TSelf>, in which a type parameter must implement the interface that declares it. It lets the interface's static abstract members, operators among them, take and return the implementing type.
References
- Generic math (Microsoft Learn)
- Explore static virtual members in C# interfaces (Microsoft Learn)
- INumber<TSelf> interface (Microsoft Learn)
- INumberBase<TSelf>.CreateChecked method (Microsoft Learn)
- INumberBase<TSelf>.CreateSaturating method (Microsoft Learn)
- IParsable<TSelf> interface (Microsoft Learn)
- Resolve errors and warnings related to generic type parameters and type arguments (Microsoft Learn)
- The history of C# (Microsoft Learn)
- What's new in .NET 11 (Microsoft Learn)
- GC.GetAllocatedBytesForCurrentThread method (Microsoft Learn)
- Arithmetic operators (C# reference) (Microsoft Learn)
Report a problem with this lesson
Kept only in this browser. Your Learn progress