Static Abstract Members and Generic Math
Interfaces can demand static members, including operators, and the INumber<T> family uses that so one method adds anything numeric.
static abstract TSelf Blank { get; } // reached as T.Blank inside where T : IHasBlank<T>An interface normally lists instance members: things you call on an object. Since C# 11 it can also list static members, which belong to the type itself rather than to any object. static abstract declares one that every implementing type must supply. static virtual declares one with a default body that a type may keep or replace.
These are reachable only through a type parameter. Inside static T Sum<T>(...) where T : INumber<T>, the expression T.Zero resolves at compile time to whatever Zero the type argument declares.
Why it was added
Operators are static members. Before C# 11 an interface could not mention them, so there was no way to say "any type that supports +". A method to add numbers had to be written once per numeric type. .NET 7 shipped interfaces in System.Numerics that close the gap: IAdditionOperators<TSelf, TOther, TResult> for +, IComparisonOperators<TSelf, TOther, TResult> for < and >, and more.
INumber<TSelf> gathers them into one constraint. Every built-in numeric type implements it: int, long, double, decimal, Half, BigInteger and the rest. Constrain T to it and the body gets the arithmetic and comparison operators, plus T.Zero, T.One, T.Abs, T.Min, T.Max and T.CreateChecked<TOther> for turning a plain literal into a T.
What trips people up
Ask for the narrowest interface that fits. A method that only adds should constrain to IAdditionOperators<T, T, T> rather than INumber<T>, so it also serves a point or a money type that supports addition and nothing else.
Example
Console.WriteLine(BlankOf<Metres>().Label);
Console.WriteLine(BlankOf<Seconds>().Label);
static T BlankOf<T>() where T : IHasBlank<T>
{
// T.Blank resolves to the static member on whichever type T turns out
// to be. Writing IHasBlank<T>.Blank instead is error CS8926.
return T.Blank;
}
// A static abstract member is one the interface demands but does not supply.
// The TSelf parameter ties the member's type to the implementing type.
interface IHasBlank<TSelf> where TSelf : IHasBlank<TSelf>
{
static abstract TSelf Blank { get; }
string Label { get; }
}
readonly struct Metres : IHasBlank<Metres>
{
public static Metres Blank => new Metres();
public string Label => "0 m";
}
readonly struct Seconds : IHasBlank<Seconds>
{
public static Seconds Blank => new Seconds();
public string Label => "0 s";
}
// Output:
// 0 m
// 0 sWhen to use it
- Write one Sum or Average that serves int, double and decimal without copies.
- Give a Money or Vector type operator + and declare IAdditionOperators so generic code accepts it.
- Add a static Empty or Parse to a type so generic code can call it as T.Empty.
- Constrain a method to IComparisonOperators<T, T, bool> so it can clamp any orderable type.
More examples
One Sum for every numeric type
This is the payoff for static abstract members: one body, compiled separately for each type argument, with no boxing and no casting. Before .NET 7 this method had to exist once per numeric type, or be written against double and lose precision for decimal. Reach for INumber<T> for totals, averages, running statistics and anything else where the arithmetic is the same whatever the number type is.
using System.Globalization;
using System.Numerics;
int[] whole = [1, 2, 3];
double[] fractional = [0.5, 0.25];
Console.WriteLine(Sum(whole));
// InvariantCulture keeps the decimal point a dot on every machine.
Console.WriteLine(Sum(fractional).ToString(CultureInfo.InvariantCulture));
static T Sum<T>(T[] values) where T : INumber<T>
{
// T.Zero is the only way to write "zero of whatever T is". The literal
// 0 has type int and would not compile here.
T total = T.Zero;
foreach (T value in values)
{
// Shorthand for total = total + value. The two are the same thing,
// and INumber<T> is what promises operator +.
total += value;
}
return total;
}
// Output:
// 6
// 0.75Parsing through the type parameter
INumber<T> brings in more than operators. It requires IParsable<T> and ISpanParsable<T>, so T.Parse and T.TryParse are reachable through the type parameter, and IFormattable, so ToString with a format string is too. This is the shape of a configuration or CSV reader that must turn text into whichever numeric type the caller asked for, with no switch over types.
using System.Globalization;
using System.Numerics;
string[] text = ["1", "2", "3"];
// The same method body, two different type arguments.
Console.WriteLine(SumText<int>(text));
Console.WriteLine(SumText<double>(text));
static T SumText<T>(string[] values) where T : INumber<T>
{
T total = T.Zero;
foreach (string value in values)
{
// Parse comes from IParsable<T>, which INumber<T> requires.
// InvariantCulture fixes the accepted number format instead of
// following the machine's regional settings.
total += T.Parse(value, CultureInfo.InvariantCulture);
}
return total;
}
// Output:
// 6
// 6static virtual: a default a type may replace
static abstract and static virtual differ in one way only: abstract has no body and must be supplied, virtual has a body and may be left alone. Use virtual when most types will want the same answer and a few need their own, which keeps you from breaking every implementing type each time you add a member. The compiler picks the implementation from the type argument, so there is no run-time lookup.
Console.WriteLine(Describe<Widget>());
Console.WriteLine(Describe<Gadget>());
static string Describe<T>() where T : IDescribable<T>
{
return T.Describe();
}
interface IDescribable<TSelf> where TSelf : IDescribable<TSelf>
{
// abstract: every implementing type has to supply this.
static abstract string TypeName { get; }
// virtual: a default body the type may keep or replace.
static virtual string Describe() => TSelf.TypeName;
}
readonly struct Widget : IDescribable<Widget>
{
public static string TypeName => "Widget";
// Keeps the default Describe().
}
readonly struct Gadget : IDescribable<Gadget>
{
public static string TypeName => "Gadget";
public static string Describe() => $"{TypeName} (version 2)";
}
// Output:
// Widget
// Gadget (version 2)
Discussion