📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

Write generic numeric code in C# using static abstract interface members and generic math (.NET 7+). Explain INumber<T> and the smaller interfaces, T.Zero and T.CreateChecked, IParsable<T>, how to make your own type participate (operators as static abstract members, the self-referencing constraint), checked operators, and the performance and design trade-offs.

Generic math with static abstract interface members: writing and consuming numeric generic code.

Senior Coding Advanced csharp-11 generic-math interfaces performance static-abstract

🎯 Key points

  • C# 11 static abstract interface members allow interfaces to require static methods and operators
  • .NET 7 generic math interfaces such as INumber<T>, IAdditionOperators and IParsable<T> are implemented by built-in numeric types
  • Use T.Zero, T.AdditiveIdentity and T.CreateChecked inside generic algorithms
  • The self-referencing constraint lets interfaces describe the implementing type
  • Interfaces with static abstract members can only be used as constraints
  • Custom types participate by implementing only the operators that are meaningful
  • Checked user-defined operators support overflow detection in checked contexts
  • The JIT specialises value-type instantiations, so performance matches hand-written code

⏱ 30-second answer

C# 11 lets interfaces declare static abstract members (including operators), and .NET 7 used that to add generic math: interfaces such as INumber<T>, IAdditionOperators<TSelf, TOther, TResult>, IAdditiveIdentity<TSelf, TResult>, IMinMaxValue<T> and IParsable<T>, implemented by all built-in numeric types. You write static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T> and use T.Zero, + and T.CreateChecked(count). Your own types join by implementing the interfaces with the self-referencing constraint pattern (struct Money : IAdditionOperators<Money, Money, Money>). The JIT specialises generics for value types, so this code runs as fast as hand-written int or double versions. Prefer the smallest interface that expresses what you need, and use checked conversions (CreateChecked) unless you have a reason not to.

🗣 Full answer (senior)

## How I would answer this in an interview

**Generic math** (.NET 7, C# 11) lets you write one algorithm that works for **any numeric type** — `int`, `long`, `decimal`, `double`, `Half`, `Int128`, `BigInteger` or your own — by constraining type parameters to interfaces with **static abstract members** (operators, constants, parsing). The JIT specialises the code per value type, so it's as fast as hand-written versions. I'd write an example, explain the interface hierarchy, `T.Zero`/`CreateChecked`, `IParsable

🔍 What the interviewer is checking

The interviewer checks whether you can write and reason about generic code using static abstract members.

Green flags

  • Writes a correct generic Sum or Average with T.Zero and CreateChecked.
  • Explains the self-referencing constraint and that these interfaces are constraint-only.
  • Picks minimal constraints and makes a custom type participate sensibly.
  • Knows the JIT specialises value-type generics.

Red flags

  • Falls back to dynamic or overloads for each numeric type.
  • Uses unchecked conversions without thinking about overflow.
  • Implements nonsensical operators on domain types.

📚 Learn in depth

1. Static abstract interface members

public interface IAdditiveIdentity<TSelf, TResult> where TSelf : IAdditiveIdentity<TSelf, TResult>
{
    static abstract TResult AdditiveIdentity { get; }
}

Implementations provide static members; generic code calls T.AdditiveIdentity. In IL, calls use constrained. !!T call ..., resolved per instantiation. They're not virtual dispatch at run time for value types.

2. Why the self-referencing constraint

Operators must be declared in terms of the implementing type (static TSelf operator +(TSelf l, TOther r)). Without TSelf, the interface couldn't express "the result is the same type as the implementer". The constraint where TSelf : IFoo<TSelf> ensures TSelf is the type implementing the interface (by convention; C# can't strictly enforce "self").

3. Conversion APIs

CreateChecked/Saturating/Truncating<TOther>(TOther value) are implemented via TryConvertFrom... and TryConvertTo... static abstract members, allowing conversions between arbitrary numeric types, including user-defined ones, in both directions.

4. Default interface methods in generic math

Some interfaces provide default implementations (e.g. INumber<T>.Clamp, Max, Min) built from other members, so custom types get them for free but can override for performance.

5. Interop with existing APIs

  • LINQ's Sum/Average have generic-math-aware implementations in recent .NET versions.
  • Vector<T>/Vector128<T> and TensorPrimitives work with numeric generic constraints for SIMD.
  • Minimal API binding uses IParsable<T>/ISpanParsable<T> for custom route parameter types.

6. Pitfalls in numeric generic code

  • Floating-point accumulation errors (use decimal where exactness matters, or compensated summation).
  • Integer overflow in Sum for large inputs (use a wider accumulator type via CreateChecked into long/Int128).
  • T.Zero vs default(T): for built-ins they're the same, but custom types may differ — always use T.Zero.

Try it yourself

  1. Implement Sum<T> and benchmark with int, double, decimal vs hand-written loops.
  2. Implement Money with addition interfaces and use it in Sum<T>.
  3. Compare CreateChecked, CreateSaturating, CreateTruncating converting 300 to byte.
  4. Add a checked operator and call it inside checked { }.

💻 Code

using System.Globalization;
using System.Numerics;

public readonly record struct Money(decimal Amount)
    : IAdditionOperators<Money, Money, Money>,
      IAdditiveIdentity<Money, Money>,
      IComparisonOperators<Money, Money, bool>
{
    public static Money AdditiveIdentity => new(0m);
    public static Money operator +(Money left, Money right) => new(left.Amount + right.Amount);
    public static bool operator <(Money left, Money right) => left.Amount < right.Amount;
    public static bool operator >(Money left, Money right) => left.Amount > right.Amount;
    public static bool operator <=(Money left, Money right) => left.Amount <= right.Amount;
    public static bool operator >=(Money left, Money right) => left.Amount >= right.Amount;
    public override string ToString() => Amount.ToString("C", CultureInfo.GetCultureInfo("en-AU"));
}

public static class Stats
{
    // Smallest constraint that expresses the algorithm
    public static T Sum<T>(ReadOnlySpan<T> values)
        where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
    {
        T total = T.AdditiveIdentity;
        foreach (T value in values) total += value;
        return total;
    }

    public static T Average<T>(ReadOnlySpan<T> values) where T : INumber<T>
    {
        if (values.IsEmpty) throw new ArgumentException("No values", nameof(values));
        T total = T.Zero;
        foreach (T value in values) total += value;
        return total / T.CreateChecked(values.Length);   // throws if Length does not fit in T
    }

    public static T Max<T>(ReadOnlySpan<T> values) where T : IComparisonOperators<T, T, bool>
    {
        T max = values[0];
        foreach (T value in values[1..]) if (value > max) max = value;
        return max;
    }

    public static T Setting<T>(IReadOnlyDictionary<string, string> settings, string key) where T : IParsable<T>
        => T.Parse(settings[key], CultureInfo.InvariantCulture);
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(Stats.Sum<int>([1, 2, 3]));                     // 6
        Console.WriteLine(Stats.Average<double>([1.5, 2.5, 3.5]));        // 2.5
        Console.WriteLine(Stats.Average<decimal>([10m, 20m, 25m]));       // 18.333...
        Console.WriteLine(Stats.Sum<Money>([new(10m), new(5.5m)]));       // $15.50
        Console.WriteLine(Stats.Max<Money>([new(10m), new(25m), new(5m)])); // $25.00

        var settings = new Dictionary<string, string> { ["Retries"] = "3", ["Timeout"] = "00:00:30", ["Launch"] = "2026-11-01" };
        Console.WriteLine(Stats.Setting<int>(settings, "Retries"));
        Console.WriteLine(Stats.Setting<TimeSpan>(settings, "Timeout"));
        Console.WriteLine(Stats.Setting<DateOnly>(settings, "Launch"));

        try { Console.WriteLine(Stats.Average<byte>(new byte[300])); }
        catch (OverflowException) { Console.WriteLine("300 does not fit in a byte"); }
    }
}

⚠ Common mistakes

1. Over-constraining with INumber<T>

What happens: requiring INumber<T> when only addition is needed. Why it is wrong: excludes types that don't support full numeric semantics (e.g. Money, complex numbers). Do instead: constrain to the minimal interfaces.

2. Implementing INumber<T> on domain types

What happens: Money : INumber<Money>. Why it is wrong: forces nonsensical operations (money × money, parsing semantics). Do instead: implement only meaningful operator interfaces.

3. Using default(T) instead of T.Zero

What happens: generic code initialises accumulators with default. Why it is wrong: custom types may define zero differently. Do instead: T.Zero or T.AdditiveIdentity.

4. Ignoring overflow

What happens: summing many ints into an int accumulator. Why it is wrong: silent wraparound. Do instead: wider accumulator types or checked conversions.

5. Assuming identical semantics across types

What happens: generic code expects / to produce fractions for all types. Why it is wrong: integer division truncates; floating has NaN; decimal has limited range. Do instead: constrain to IFloatingPoint<T> when fractional semantics are required.

6. Generic math over reference types in hot paths

What happens: custom numeric classes in generic loops. Why it is wrong: shared code and runtime lookups reduce performance. Do instead: use structs for numeric types.

7. Unreadable constraint lists

What happens: methods with five or six numeric interface constraints. Why it is wrong: hard to read and reuse. Do instead: define a composite interface for your domain.

8. Using generic math where a single type suffices

What happens: generic numeric code for logic only ever used with decimal. Why it is wrong: complexity without benefit. Do instead: keep it concrete until reuse is real.

🏛 Senior insight: trade-offs

Generic math is a library author's tool that lets one correct, tested algorithm serve every numeric type, and it is the main practical reason static abstract interface members exist.

Context Use generic math?
Shared numeric or statistics library yes, removes per-type duplication
Units of measure and domain value types selectively, with only valid operators
Generic parsing of configuration and inputs yes, via IParsable<T>
Single-type business logic usually no; plain decimal is clearer

The senior trade-off is readability versus reuse. Long constraint lists scare readers, so name helper interfaces or document constraints, and keep generic math at the edges of the system where reuse pays.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate explains static abstract members. Why: Checks static abstract and default interface members basics.
    How to answer

    An interface is a contract: a set of members a type promises to provide, with no instance state. A class or struct can implement many interfaces. Since C# 8 an interface can carry default implementations (used only through the interface, and they need .NET Core 3.0+), plus static, private and protected members. Since C# 11 it can declare static abstract / static virtual members, which power generic math (INumber<T>, T.Zero). Explicit implementation (void IDisposable.Dispose()) hides a member unless you cast. Calls through an interface use interface dispatch, which the JIT can often devirtualise. Casting a struct to an interface boxes it; a generic constraint (where T : IComparable<T>) avoids the box.

  2. When: Candidate writes where T : INumber<T>. Why: Generic constraints are central to generic math.
    How to answer

    Constraints tell the compiler what T can do so you can use it safely. C# has class and class? for reference types, struct for non-nullable value types, unmanaged for blittable structs, notnull, new() for a public parameterless constructor, base-class and interface constraints, where T : U, and Enum and Delegate constraints. C# 13 added the allows ref struct anti-constraint so T can be a Span<T>-like type. Interface constraints on struct T also avoid boxing because the JIT emits constrained calls.

  3. When: Candidate implements operators on Money. Why: Operator overloading rules apply to custom numeric types.
    How to answer

    You overload an operator with a public static method named operator + (or ==, <, implicit operator and so on), where at least one parameter is the containing type. Some operators must be overloaded in pairs: ==/!=, </>, <=/>=, true/false. If you overload ==, also override Equals and GetHashCode so all equality paths agree. Conversion operators are implicit (safe, no data loss) or explicit (may fail or lose data). C# 11 added checked operators and static abstract operators in interfaces for generic math (IAdditionOperators<,,>). C# 14 lets you declare compound assignment (+=) and ++/-- as instance operators that update the object in place. Overload only when the meaning is obvious, as with Money, Vector or BigInteger.

🛠 Real-world problems: how you handle them

Four drifting copies of statistics code merged into one generic implementation
Situation
An internal [telemetry library] had Average, Percentile and Histogram implemented separately for int, long, float and double.
Signals
A bug report that p95 latency in ms (long) differed from the same data as double; code review showed one copy rounded differently.
Investigation
Diffed the four implementations and found [three] behavioural differences introduced by separate fixes over two years.
Resolution
Replaced them with single generic methods constrained on INumber<T> with CreateChecked conversions, plus property-based tests across all four types.
Prevention
BenchmarkDotNet suite in CI proving no regression, and a rule that new numeric helpers are written generically.

How to tell it (STAR)

**Situation:** our [telemetry library] produced different p95 values depending on the numeric type. **Task:** I owned the library and wanted to fix the bug and the cause. **Action:** I found four copies of each algorithm that had drifted apart, rewrote them once using generic math with INumber<T>, added tests that ran every type through the same cases, and benchmarked to prove speed was unchanged. **Result:** [about 600] lines removed, the inconsistency fixed, and future fixes now apply to every type at once.

More in Modern C#

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me