Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: One method adds every numeric type when T is an · Static abstract members put operators and constants on the type, not on an object. With a pass: 3 in-depth parts and 3 interview answers, about 10 minutes.

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 int, decimal, double and long C# · sum/sum.cs
// 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>

  1. Question 1 of 2 Average<int>([120, 85, 240]) printed 148, not 148.33. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Generic math uses the type's own division, and int division drops the fraction

    Each type keeps its own arithmetic inside generic code. For a fractional average of int inputs, compute in another type, such as decimal or double.

  2. Question 2 of 2 Why does Sum<T> start from T.Zero instead of the literal 0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 0 is an int, while T may be decimal or double; T.Zero is the zero of whatever T is

    A literal has one fixed type. Generic math gives each value it needs as a static member of T: Zero, One and the conversions such as CreateChecked.

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: static abstract members read on a type parameter C# · members/members.cs
// 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:

Interactive · step by step How Describe<T> finds T.MaxValue Step through two calls of Describe. The left column is the generic method and its constraint, the right column the type that each call supplies.
How Describe<T> finds T.MaxValue, step 5 of 5. Showing Describe<T>(), T : IMinMaxValue<T>, int, decimal. In focus: T : IMinMaxValue<T>.promised byT = intT = decimalDescribe<T>()reads T.MaxValueT : IMinMaxValue<T>static abstract MaxValueintMaxValue 2147483647decimalMaxValue 7.9 × 10^28

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. Describe<int>(). The first call supplies int as T. int implements IMinMaxValue<int>, so T.MaxValue is int.MaxValue, and members.cs printed 2147483647.
  4. Describe<decimal>(). A call with decimal reads decimal.MaxValue through the same line of code, 79228162514264337593543950335, a number with 29 digits.
  5. 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:

  • CreateChecked converts a number into T and throws OverflowException when it does not fit.
  • CreateSaturating converts and clamps a value that does not fit to T.MinValue or T.MaxValue.
  • CreateTruncating converts and keeps only the low bits of an integer that does not fit.
  • Inside checked(…), + on T calls 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 an int sum that passes int.MaxValue throws OverflowException instead of wrapping. A type that declares a checked operator must declare the ordinary one too.
  • IParsable<TSelf> declares static Parse and TryParse, 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

  1. Question 1 of 2 Describe<T>() in members.cs takes no arguments, so it has no value of type T. How can it print T.MaxValue?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: MaxValue is a static abstract member of IMinMaxValue<T>, read on the type parameter, and each type argument supplies its own

    A static abstract member is a contract on the type itself. Generic code calls it on T, and the type argument of each call decides which implementation runs.

  2. Question 2 of 2 Why does IQuantity<TSelf> in members.cs declare where TSelf : IQuantity<TSelf>?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Its + operator takes and returns the implementing type, which an interface may declare only when TSelf is constrained to the interface itself

    The self-referencing constraint lets members of the interface name the implementing type: operators, factories such as Parse, and constants such as Zero.

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.

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.

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.

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: ten checks of Statistics<T> C# · practice/stats.cs
// 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.

  1. Question 1 of 7 Which of these can be the type argument of Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: decimal

    The constraint admits the types that implement INumber<T>: int, long, decimal, double and the other built-in numbers, plus any type of your own that implements it.

  2. Question 2 of 7 Average<T> in sum.cs divides the sum by the count in T's own arithmetic. What does Average<int>([7, 8]) return?

    Type a number.

    Show the answer: Question 2 of 7

    Answer: 7

    The sum is 15 and the count 2; in int arithmetic 15 / 2 is 7, because integer division drops the fraction.

  3. Question 3 of 7 Average<T> divides by T.CreateChecked(values.Length). For which call could that conversion throw OverflowException?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: Average<byte> over 300 values

    CreateChecked throws when the value does not fit the target type. A byte holds at most 255, so the count 300 cannot become a byte, even though every value in the span is a byte.

  4. Question 4 of 7 What does a static abstract member of an interface require?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: Every type that implements the interface must supply that static member

    Static abstract members, added in C# 11, move a contract from objects to types, which is what operators, constants and factories need.

  5. Question 5 of 7 Inside Describe<T>() where T : INumber<T>, IMinMaxValue<T>, which line compiles?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: T limit = T.MaxValue - T.One;

    In generic math code, constants, factories and operators all come from the type parameter: T.Zero, T.One, T.MaxValue and the operators of its interfaces.

  6. Question 6 of 7 Sum<int> adds with checked(total + value), and the sum passes int.MaxValue. What happens?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: OverflowException, because + on T inside checked(…) calls int's checked operator

    checked(…) works in generic code as it does elsewhere: for int it throws on overflow, and for a type of your own it calls the type's checked operator.

  7. Question 7 of 7 A file reader must turn each column into its type: int, decimal, DateOnly or a type of your own. Which constraint lets one generic method parse them all?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: where T : IParsable<T>

    Ask for the ability you use. Parsing is IParsable<T>, which numbers, dates, Guid and your own types can all implement.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • where T : INumber<T> lets one Sum or Average serve int, long, decimal and double, and each type keeps its own arithmetic, so an int average drops its fraction.
  • Static abstract members, added in C# 11, let an interface require static members such as Zero, MaxValue and 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.