Back to Blog
C#

C# where T : struct: Generic Value Type Constraints

c# where t struct: Learn how the C# where T : struct constraint restricts generic parameters to value types, enables Nullable<T> return patterns, and prevents boxing.

C#GenericsType ConstraintsValue Types.NET
Illustration of a C# generic type parameter T constrained to value types, with solid blocks representing int, DateTime, and enum passing through a filter gate while a reference type is blocked.

c# where t struct requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The where T : struct constraint in C# restricts a generic type parameter to value types. When a generic method or type declares this constraint, callers can only supply struct types, enum types, or nullable value types as the type argument. The compiler enforces this at compile time, and it also changes what the generic body is allowed to do with T.

public static T Clamp<T>(T value, T min, T max) where T : struct, IComparable<T> { if (value.CompareTo(min) < 0) return min; if (value.CompareTo(max) > 0) return max; return value; }

This method works with any value type that implements IComparable<T>: int, double, DateTime, Guid, or a custom struct. It rejects string, object, and every other reference type at compile time.

What the struct Constraint Actually Restricts

The constraint accepts three categories of types:

  • Primitive and built-in value types: int, double, bool, char, DateTime, Guid, and similar.
  • Custom struct types declared with the struct keyword.
  • Enum types and nullable value types such as int? and DateTime?.

It rejects every reference type: classes, interfaces, delegates, arrays, and string. The compiler produces an error at the call site when a reference type is used as the type argument.

Inside the generic body, the constraint gives the compiler specific knowledge about T:

  • T is never a null reference, unless T is itself Nullable<U>.
  • default(T) produces a zero-initialized value, not null.
  • T? is a valid type and means Nullable<T>.
  • T can be boxed and unboxed without runtime type checks.

Basic Usage in Generic Methods and Types

The constraint appears on generic methods and generic types. A generic class with the constraint:

public sealed class Result<T> where T : struct { public bool IsSuccess { get; } public T Value { get; } public string? Error { get; } private Result(bool isSuccess, T value, string? error) { IsSuccess = isSuccess; Value = value; Error = error; } public static Result<T> Ok(T value) => new(true, value, null); public static Result<T> Fail(string error) => new(false, default, error); }

Result<T> can hold an int, a Guid, a DateTime, or any enum, but it cannot hold a string or a class instance. This is a deliberate design choice when the success value is always a value type and the failure path carries only an error message.

How T? Changes Meaning Under the struct Constraint

The most important consequence of the struct constraint is that T? becomes Nullable<T>. Without the constraint, T? is either invalid syntax or, with an unconstrained parameter in C# 9 and later, a nullable annotation with different semantics.

public static T? FindFirst<T>(IEnumerable<T> source, Func<T, bool> predicate) where T : struct { foreach (var item in source) { if (predicate(item)) { return item; } } return null; }

The return type is Nullable<T>, so callers can check HasValue to detect "not found":

var numbers = new List<int> { 1, 2, 3, 4, 5 }; var even = FindFirst(numbers, n => n % 2 == 0); if (even.HasValue) { Console.WriteLine($"Found: {even.Value}"); }

This pattern is the main reason developers reach for the struct constraint. Without it, returning "not found" from a generic method requires an out parameter or a sentinel value. When T is instantiated with a nullable value type such as int?, the expression T? resolves to T itself, because Nullable<Nullable<int>> is not a valid type.

Comparing struct and class Constraints

Aspectwhere T : structwhere T : class
Allowed type argumentsValue types, enums, Nullable<T>Reference types: classes, interfaces, delegates, arrays, string
Meaning of T?Nullable<T>Nullable reference annotation (C# 8+)
default(T)Zero-initialized valuenull
Null checks neededNot for T itselfYes, T can be null
BoxingAvoided by JIT specializationNot applicable
Typical useValue semantics, Nullable<T> patternsReference semantics, null handling

The choice is not about which constraint is better. It depends on what the generic code must do. If the code must return a "not found" state through null, the struct constraint with Nullable<T> is the natural fit. If the code must accept null and work with reference identity, the class constraint is appropriate.

Boxing, Performance, and Runtime Behavior

Generics with value types avoid boxing because the JIT compiler generates specialized code for each value type used as T. When Clamp(7, 1, 10) is called, the runtime creates a specialized version of Clamp where T is int, and the values flow through registers or the stack without heap allocation.

This differs from non-generic APIs that accept object:

// Boxes the int - heap allocation ArrayList list = new ArrayList(); list.Add(42); // No boxing - specialized code for int List<int> list2 = new List<int>(); list2.Add(42);

The struct constraint does not automatically prevent all boxing. If the generic body casts T to object or passes T to a parameter of type object, boxing still occurs. The constraint only guarantees that T is a value type; it does not change how the value is used inside the body.

One cost to weigh: value types are copied by value. Every assignment, method call, and return copies the entire struct. For small structs like int or DateTime this is negligible. For large custom structs with many fields, the copy cost can become significant. If profiling shows a large struct being copied frequently in generic code, consider whether a class or a readonly struct is a better fit.

Common Mistakes and Edge Cases

A common mistake is assuming the struct constraint excludes nullable value types. It does not. where T : struct accepts int?, and inside the generic body T is then Nullable<int>. If the code relies on T being a non-nullable value type, this can produce surprising behavior.

Another mistake is using the struct constraint when the code actually needs to support both value and reference types. With an unconstrained type parameter in C# 9 and later, T? is valid for both categories, with different meanings. If the generic code does not specifically need Nullable<T>, leaving T unconstrained is often the better choice.

Enums satisfy the struct constraint. This is frequently useful, but it also means the generic body cannot assume T is a numeric type. Arithmetic and comparison operators are not available on T unless an additional constraint is added.

public static T ParseEnum<T>(string value) where T : struct, Enum { return (T)Enum.Parse(typeof(T), value, ignoreCase: true); }

The where T : Enum constraint, available since C# 7.3, is more specific than where T : struct. It restricts T to enum types and lets the compiler treat the result of Enum.Parse as type T.

Combining Constraints and Advanced Scenarios

The struct constraint combines with other constraints. A common combination adds an interface constraint so the generic body can call methods on T:

public static T Max<T>(T left, T right) where T : struct, IComparable<T> { return left.CompareTo(right) >= 0 ? left : right; }

The unmanaged constraint implies struct, so where T : unmanaged is sufficient when the code requires a value type with no reference fields. Writing where T : struct, unmanaged is redundant but harmless.

When a generic type has multiple type parameters, each can carry its own constraint:

public static TKey GetKey<TKey, TValue>(TValue value, Func<TValue, TKey> selector) where TKey : struct where TValue : class { return selector(value); }

A production consideration: when the struct constraint is used on a widely called generic method, the JIT generates one specialized version per value type. This is usually desirable for performance, but it does increase the amount of JIT-compiled code. In most applications the effect is not measurable, but in very large generic-heavy codebases it is worth keeping in mind.

c# where t struct: Practical Usage and Code Examples | RYUSLOG DEV