Back to Blog
C#

C# where T : struct: Generic Value Type Constraints

The C# `where T : struct` constraint restricts generic parameters to non-nullable value types. See how it changes `T?` to `Nullable<T>`, when to use it, and where boxing can still happen.

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.

The where T : struct constraint in C# restricts a generic type parameter to non-nullable value types. It is useful when a generic method or type should accept only structs and enums, and when T? should mean Nullable<T>. The compiler enforces the restriction 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 non-nullable 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 these 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.

It rejects every reference type: classes, interfaces, delegates, arrays, and string. It also rejects nullable value types: int? and DateTime? are not valid type arguments for a parameter constrained with struct. The nullable form can appear in declarations as T?, which the compiler treats as Nullable<T>.

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

  • A value of type T is never a null reference and is never itself Nullable<U>. The nullable form is written as T?, and it means Nullable<T>.
  • default(T) produces a zero-initialized value, not null.
  • T? is a valid type and means Nullable<T>.
  • T can be boxed and unboxed, although boxing allocates when the value is converted to object.

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 struct constraint, the meaning depends on the constraint and nullable context: with where T : class, T? is a nullable reference annotation in C# 8 and later; with an unconstrained parameter in C# 9 and later, it can be Nullable<T> for value types or a nullable reference annotation for reference types.

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. Because where T : struct requires T to be a non-nullable value type, T? cannot become a nested Nullable<Nullable<U>>; inside the generic declaration it is always Nullable<T>.

Comparing struct and class Constraints

Aspectwhere T : structwhere T : class
Allowed type argumentsValue types (non-nullable), enumsReference types: classes, interfaces, delegates, arrays, string
Meaning of T?Nullable<T>Nullable reference annotation (C# 8+, in nullable enabled contexts)
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 that the struct constraint also accepts nullable value types. It does not: where T : struct requires a non-nullable value type, so int? is not a valid type argument for T. If the generic code needs to represent "no value" as a return, declare the return type as T?; under the struct constraint that is Nullable<T>.

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 makes the explicit cast from Enum.Parse to T valid.

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; there is no need to add where T : struct, unmanaged.

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: How It Works and When to Use It | RYUSLOG DEV