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

📚 Interview question

What generic constraints does C# support? Explain class, class?, struct, unmanaged, notnull, new(), base-class, interface, naked type, Enum and Delegate constraints, and the C# 13 allows ref struct anti-constraint, with an example of when you need each.

Every generic constraint in C# 14, what each enables, the order rules, and how constraints affect performance and API design.

Mid-Level Theory Intermediate allows-ref-struct constraints csharp generics unmanaged

🎯 Key points

  • Constraints restrict T so the compiler lets you use members: `where T : IComparable<T>` allows CompareTo
  • Reference and value constraints: `class`, `class?`, `struct`, `unmanaged`, `notnull`
  • `new()` allows `new T()` and must be the last constraint; `struct` implies it
  • Base-class and interface constraints, naked type constraints (`where T : U`), and `System.Enum` / `System.Delegate` since C# 7.3
  • `allows ref struct` (C# 13) lets T be a ref struct such as `Span<T>`, with ref-safety rules applied to T
  • Order: primary (class/struct/unmanaged/notnull/base class) first, then interfaces, then `new()`, and `allows ref struct` last
  • Interface constraints on struct T enable constrained calls with no boxing
  • Do not over-constrain public APIs; each constraint is a breaking change to remove or add

⏱ 30-second 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.

🗣 Full answer (senior)

How I would answer this in an interview

Constraints (where T : ...) restrict which type arguments are allowed and, in return, let generic code use capabilities of T — call interface members, create instances, use null, or treat it as unmanaged memory. I'd walk through each constraint with a real reason to use it.

1. The constraints

Constraint Means Example of when you need it
where T : class Non-nullable reference type (with NRT on) Generic caches storing T? as "missing"; Interlocked.CompareExchange<T>
where T : class? Any reference type, nullable or not Same, when callers may use nullable types
where T : struct Non-nullable value type T? meaning Nullable<T>: T? Parse<T>(string s) where T : struct, IParsable<T>
where T : unmanaged Value type with no references (recursively) stackalloc T[n], MemoryMarshal.Cast<byte, T>, sizeof(T), pointers, binary serialisation
where T : notnull Non-nullable reference or value type Dictionary keys: Dictionary<TKey,TValue> where TKey : notnull
where T : new() Has a public parameterless constructor Factories: T Create<T>() where T : new() => new T();
where T : BaseClass Derives from (or is) a class Generic repositories over AggregateRoot<TId>
where T : IInterface Implements an interface Max<T>(...) where T : IComparable<T>; generic math where T : INumber<T>
where T : U (naked type) T derives from/implements another type parameter AddRange<TDerived>(IEnumerable<TDerived> items) where TDerived : T
where T : Enum (C# 7.3) An enum type Generic enum helpers: GetDisplayName<T>(T value) where T : struct, Enum
where T : Delegate / MulticastDelegate (C# 7.3) A delegate type Combining/removing generic delegates safely
where T : allows ref struct (C# 13) Anti-constraint: permits ref struct type arguments Generic APIs that should accept Span<T>-like types: IEnumerable-like patterns, Func<T, TResult> in .NET 9
where T : default Used in overrides/explicit impls to resolve T? ambiguity Overriding a method with unconstrained T?

Constraints combine: where T : class, IEntity, new(). Order rules: class/struct/unmanaged/notnull/base class first, interfaces next, new() last; allows ref struct with other constraints in the same clause.

2. Examples

Interface constraint — call members without boxing:

public static T Max<T>(ReadOnlySpan<T> items) where T : IComparable<T>
{
    var max = items[0];
    foreach (var i in items[1..]) if (i.CompareTo(max) > 0) max = i;
    return max;
}

struct + IParsable — return Nullable<T>:

public static T? TryParseOrNull<T>(string? s) where T : struct, IParsable<T>
    => T.TryParse(s, CultureInfo.InvariantCulture, out var v) ? v : null;

unmanaged — reinterpret bytes:

public static T Read<T>(ReadOnlySpan<byte> bytes) where T : unmanaged => MemoryMarshal.Read<T>(bytes);

Enum:

public static string Describe<T>(T value) where T : struct, Enum => value.ToString();

allows ref struct (C# 13):

public static TResult Apply<T, TResult>(T value, Func<T, TResult> f) where T : allows ref struct => f(value);
// can now be called with ReadOnlySpan<char>

3. Why constraints matter

  • Compile-time safety: misuse is a compile error, not a runtime cast failure.
  • Capabilities: without constraints, T only has object members.
  • Performance: interface constraints on value types avoid boxing — the JIT specialises code and calls methods directly.
  • Documentation: constraints state assumptions explicitly.

4. A production example

A generic enum helper took object and used reflection to read [Display] attributes; it was called millions of times in report generation and boxed every value. Rewriting it as GetDisplayName<T>(T value) where T : struct, Enum with a static generic cache (static class EnumNames<T>) built once per enum type removed the boxing and reflection per call, and invalid calls (passing a non-enum) became compile errors.

🔍 What the interviewer is checking

Tests breadth (do they know the full set, including newer unmanaged, notnull, allows ref struct) and depth (why constraints matter for boxing and code generation).

Green flags: explains constrained calls and boxing; knows ordering rules; picks unmanaged for binary work; knows new() cost.

Red flags: only knows class and new(); thinks notnull is enforced at run time; cannot explain what a constraint enables inside the method.

📚 Learn in depth

1. How constraints are represented

Constraints are stored in metadata on generic parameters (GenericParamConstraint table plus flags like ReferenceTypeConstraint, NotNullableValueTypeConstraint, DefaultConstructorConstraint). unmanaged is emitted as struct + modreq(UnmanagedType) + an attribute so older compilers can't misuse it. notnull and class? are compiler-level nullable annotations. allows ref struct sets a new flag the runtime (.NET 9+) understands.

2. new() and performance

new T() in generic code historically compiled to Activator.CreateInstance<T>() — reflection-based and slow. Modern runtimes optimise it significantly (and for value types it's just default), but for hot paths a factory delegate (Func<T>) or static abstract static T Create() member is faster and more flexible (supports constructor parameters).

3. Constraints and overload resolution

Constraints aren't part of the method signature, so you can't overload by constraint. C# 7.3 improved resolution: candidates whose constraints aren't satisfied are discarded early in more cases, reducing confusing errors. Still, design APIs with different names rather than relying on constraint-based selection.

4. unmanaged in detail

A type is unmanaged if it's a primitive numeric type, char, bool, an enum, a pointer, or a struct whose fields are all unmanaged (recursively, including generic structs since C# 8). This guarantees no GC references, so the memory can be copied as raw bytes, stack-allocated, pinned trivially, and passed to native code. Generic math types like int, double, Half and Int128 are unmanaged.

5. allows ref struct and what it unlocks

Ref structs couldn't be type arguments because generic code might box them, store them in fields, or capture them. The anti-constraint promises the generic code won't do those things (the compiler enforces ref-safety rules for T inside). .NET 9 annotated many BCL types (Func<>, Action<>, IEnumerable<T>-like interfaces in some cases, SearchValues, comparers) so spans can flow through generic APIs, enabling more zero-allocation code.

6. Nullable interplay

  • Unconstrained T? means "default of T" (nullable for references, unchanged for values).
  • where T : struct makes T? Nullable<T>.
  • where T : class makes T? a nullable reference.
  • where T : notnull warns when nullable types are used as arguments.

Try it yourself

  1. Write Max<T> with and without IComparable<T> constraint (using Comparer<T>.Default) and benchmark with int.
  2. Use where T : unmanaged with stackalloc T[16] and sizeof(T).
  3. Write an enum helper with where T : struct, Enum and a static generic cache.
  4. Write a generic method with allows ref struct and call it with ReadOnlySpan<char>.

💻 Code

using System.Numerics;
using System.Runtime.InteropServices;

namespace Generics.Constraints;

public abstract class EntityBase
{
    public Guid Id { get; init; } = Guid.NewGuid();
}

public sealed class Customer : EntityBase
{
    public string Name { get; set; } = "";
}

public readonly record struct Header(int MessageType, int Length);

public interface IHasLength
{
    int Length { get; }
}

public ref struct Utf8Token(ReadOnlySpan<byte> bytes) : IHasLength
{
    private readonly ReadOnlySpan<byte> _bytes = bytes;
    public int Length => _bytes.Length;
}

public static class Constrained
{
    public static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
    {
        var total = T.Zero;
        foreach (var v in values) total += v;
        return total;
    }

    public static T ReadStruct<T>(ReadOnlySpan<byte> buffer) where T : unmanaged
        => MemoryMarshal.Read<T>(buffer);

    public static T CreateEntity<T>() where T : EntityBase, new() => new T();

    public static IReadOnlyList<string> EnumNames<T>() where T : struct, Enum
        => Enum.GetNames<T>();

    public static void AddAll<T, TDerived>(ICollection<T> target, IEnumerable<TDerived> items) where TDerived : T
    {
        foreach (var item in items) target.Add(item);
    }

    // allows ref struct must be the last item in the constraint clause (C# 13+)
    public static int Length<T>(T text) where T : IHasLength, allows ref struct
        => text.Length;
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(Constrained.Sum<int>([1, 2, 3]));
        Console.WriteLine(Constrained.Sum<decimal>([1.5m, 2.25m]));

        Span<byte> bytes = stackalloc byte[8];
        BitConverter.TryWriteBytes(bytes, 7);
        BitConverter.TryWriteBytes(bytes[4..], 128);
        Console.WriteLine(Constrained.ReadStruct<Header>(bytes));

        Console.WriteLine(Constrained.CreateEntity<Customer>().Id);
        Console.WriteLine(string.Join(",", Constrained.EnumNames<DayOfWeek>()));
        Console.WriteLine(Constrained.Length(new Utf8Token("hello"u8)));
    }
}

⚠ Common mistakes

1. Unconstrained generics with runtime checks

What happens: if (value is IComparable c) inside generic methods. Why it is wrong: boxing and runtime failures. Do instead: add the interface constraint.

2. Overusing new()

What happens: generic factories require parameterless constructors. Why it is wrong: forces public parameterless constructors and bypasses DI/validation. Do instead: accept a Func<T> factory or use static abstract factories.

3. Constraining to base classes unnecessarily

What happens: where T : EntityBase when only an Id property is needed. Why it is wrong: excludes structs and other types; couples to a hierarchy. Do instead: constrain to a small interface.

4. Overloading by constraints

What happens: M<T>() where T : class and M<T>() where T : struct. Why it is wrong: same signature — compile error. Do instead: use different names.

5. Forgetting notnull on keys

What happens: generic caches allow nullable keys. Why it is wrong: Dictionary throws on null keys at run time. Do instead: where TKey : notnull.

6. Misunderstanding T?

What happens: expecting T? to be Nullable<T> without a struct constraint. Why it is wrong: for value types, unconstrained T? is just T. Do instead: add where T : struct or design separate overloads.

7. Using Enum constraint without struct

What happens: where T : Enum alone. Why it is wrong: allows System.Enum itself (a reference type), causing boxing. Do instead: where T : struct, Enum.

8. Using unmanaged for types that contain references

What happens: trying to MemoryMarshal.Cast structs with string fields. Why it is wrong: compile error — and reinterpretation would be unsafe. Do instead: keep binary-serialisable structs free of references.

🏛 Senior insight: trade-offs

Constraints are the tool that turns generics from "object with nicer syntax" into zero-cost abstractions. unmanaged plus spans gives safe binary parsing, interface constraints plus struct T give inlined calls, and INumber<T> gives one numeric algorithm for every type. The flip side is API stability: constraints are part of the signature, so I am conservative in libraries. allows ref struct is still young; I use it in low-level libraries but rarely in application code.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions constrained calls or boxing. Why: Checks whether the candidate knows how constraints change generated code and cost.
    How to answer

    Generics are usually zero-cost, but how depends on T. For value types the JIT generates separate, fully specialised code, so there is no boxing and typeof(T) == typeof(int) folds to a constant. For reference types all instantiations share one canonical body, which looks up type-specific data such as statics and typeof(T) through a runtime dictionary. new T() and generic virtual methods take slower paths. The cost of specialisation is code size and JIT time, which matters for Native AOT, where every instantiation must be known at build time.

  2. When: Candidate mentions INumber<T> or generic math. Why: Static abstract interface members are the modern constraint-based way to call operators on T.
    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.

  3. When: Candidate mentions allows ref struct or Span<T>. Why: allows ref struct only makes sense once you know ref struct rules.
    How to answer

    A struct is a value type: a variable holds the data itself, assignment and parameter passing copy it, and it is stored inline (in a local, a field or an array element) rather than as a separate heap object. Structs cannot inherit (they are sealed) but can implement interfaces. default(T) always gives all-zero fields, even when you write a parameterless constructor (allowed since C# 10). readonly struct guarantees immutability and avoids defensive copies. ref struct (like Span<T>) can live only on the stack; since C# 13 it can implement interfaces and be used with allows ref struct generics. Converting a struct to object or an interface boxes it. Mutable structs cause lost updates because you often change a copy.

🛠 Real-world problems: how you handle them

Corrupted binary reads from a struct containing a reference
Situation
A telemetry ingestion service parsed device packets with a generic `Read<T>() where T : struct` that used Marshal.PtrToStructure on a pinned buffer.
Signals
Occasional garbage values for one message type after a developer added a string Label field to its struct; no exception was thrown.
Investigation
Reproduced with a unit test of the raw bytes, inspected the struct layout with `Unsafe.SizeOf<T>()` and saw the string reference in the middle of the layout; marshalling rules differed from the wire format.
Resolution
Changed the constraint to `where T : unmanaged`, replaced marshalling with `MemoryMarshal.Read<T>`, and moved the label to a separate length-prefixed field parsed explicitly.
Prevention
The compiler now rejects non-blittable message structs. Added round-trip tests for every message type.

How to tell it (STAR)

**S:** Our device telemetry pipeline was storing wrong readings for one packet type. **T:** Find out why and stop it happening again. **A:** I traced it to a struct that gained a reference field, which the generic reader allowed because it only required `struct`. I changed the constraint to `unmanaged`, switched to MemoryMarshal, and added round-trip tests. **R:** The bug class became a compile-time error and the parser got [percentage] faster.

↩ Usually asked after

More in Generics

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me