C# Generics — Boxing-Induced OutOfMemory in Trading Engines
ArrayList boxing in a trading engine caused OutOfMemory every 2 hours (30% GC CPU).
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Generics let you parameterize types with placeholders (T) for compile-time safety.
- List
replaces ArrayList — no runtime casts, no boxing for value types. - Constraints (where T : IInterface) unlock member access on T.
- Covariance (out) and contravariance (in) enable type-safe substitution on interfaces.
- Performance: value types avoid boxing, cutting GC pressure by up to 90% in hot paths.
- Biggest mistake: assuming T can do anything without constraints — expect compile errors.
C# generics let you write code that works with any type while preserving compile-time type safety — no casting, no boxing, no runtime surprises. Before generics (C# 1.0), you'd use object or ArrayList, which meant value types like int or double got boxed into heap objects every time you stored or retrieved them.
In a trading engine processing millions of price ticks per second, that boxing creates garbage collection pressure that can spike latency or, in extreme cases, trigger OutOfMemoryException when the GC can't keep up. Generics eliminate boxing entirely by letting the runtime generate specialized code for each value type — List<int> stores ints directly on the stack or inline in the array, never touching the heap.
Generics solve the fundamental tension between reusability and performance. Without them, you either duplicate code for every type (maintenance nightmare) or use object-based collections that kill throughput. Trading engines care because they operate under strict latency budgets — sub-millisecond order execution, real-time risk checks, market data normalization.
A single boxing allocation on the hot path can blow your p99 latency. Generics also enable patterns like Result<T> or Option<T> that let you return success/failure without throwing exceptions (which are expensive) or using out parameters. The compiler enforces that you handle both paths, eliminating entire classes of null-reference and type-cast bugs.
In the .NET ecosystem, generics are the foundation for everything from List<T> and Dictionary<TKey, TValue> to LINQ's IEnumerable<T> and Task<T>. Alternatives like C++ templates are more powerful but compile per instantiation, while Java's generics use erasure and still box value types.
C# generics are reified — the runtime knows the actual type at execution, enabling optimizations like shared code for reference types and specialized code for value types. You shouldn't use generics when you truly need runtime type flexibility (e.g., serialization of unknown types) or when working with legacy APIs that expect object.
But for any performance-sensitive code path — especially in finance, gaming, or real-time systems — generics are non-negotiable.
Imagine you own a vending machine that only accepts one type of coin — you'd need a separate machine for quarters, dimes, and nickels. That's what non-generic code feels like. Generics let you build ONE vending machine with a adjustable slot that you lock to a specific coin type when you need it. The machine's logic stays the same; you just tell it upfront what type of coin it'll be dealing with. No guessing, no fumbling, no wrong coins jamming the mechanism.
Generics are everywhere in C#. List<T>, Dictionary<TKey, TValue>, Task<T> — you use them daily without thinking about what's happening under the hood. That's fine until something breaks. A cast fails. Boxing spikes your GC. A constraint you didn't add forces a runtime workaround. That's when knowing how generics actually work saves you a late-night debug session.
Before C# 2.0, developers used ArrayList and cast everything to and from object. The compiler couldn't catch type mismatches — a bug that should fail at compile time exploded as InvalidCastException at runtime. Generics fix this by letting you parameterise a class with a type placeholder. The compiler fills it in when you use the code, giving you full type checking without any runtime casting overhead.
By the end of this article you'll understand why generics exist at a language-design level, write your own generic classes with constraints, combine generics with interfaces for real architectural patterns, and avoid the three mistakes that trip up even experienced developers. You'll also walk away with sharp answers to the interview questions that separate juniors from mid-level engineers.
What Generics Actually Prevent — and Why Trading Engines Care
Generics in C# let you define classes, methods, and interfaces with a placeholder type that is specified at compile time. The core mechanic: the compiler generates specialized code for each value type used, eliminating boxing and unboxing overhead. For reference types, it shares a single internal instantiation via runtime trickery, but for structs — like decimal, int, or custom value types — each gets its own native code path.
In practice, this means a List<int> stores integers directly in a contiguous array, not as boxed objects on the heap. A List<object> storing ints boxes each value: allocation, pointer dereference, GC pressure. The difference is measurable: a tight loop over 10 million ints in a generic list runs at native speed; the boxed version allocates 80 MB of heap objects and triggers multiple Gen-2 collections.
Use generics whenever you need type-safe collections, algorithms, or data structures that work with value types. In trading engines, where latency is measured in microseconds and memory churn kills determinism, generics are not optional — they are the difference between a predictable 10μs path and a GC pause that blows your P99.
ArrayList for price levels. Each decimal addition boxed to object, allocating 16 bytes per level. With 10,000 levels updated 100 times per second, the process consumed 1.5 GB/min of heap — causing OOM crashes every 90 seconds.object in the type signature, you are paying for boxing. Switch to a generic collection immediately.List<T>, Dictionary<TKey, TValue>, and custom generic structs over ArrayList or Hashtable.The Problem Generics Solve — Why object-Based Code Is a Time Bomb
Before you can appreciate generics, you need to feel the pain they eliminate. The classic approach before C# 2.0 was to write everything against the object type — the root of all C# types. It seemed clever: one method handles everything. In practice, it was a maintenance nightmare.
Every value you pulled out had to be cast back to its real type. The compiler had no idea what was actually in your collection. You could put a string in a list of integers and the code would compile fine — it would just blow up at runtime when some unsuspecting method tried to call .ToString() on what it assumed was an int.
There's also a performance cost. Value types like int and double must be 'boxed' — wrapped in a heap-allocated object — to be stored as object, then 'unboxed' when retrieved. In tight loops processing thousands of items, this garbage pressure is measurable.
Generics eliminate both problems. You declare the type once at the use site, the compiler enforces it everywhere, and value types are stored directly without boxing. You get the flexibility of writing reusable code AND the safety of a strongly-typed language — not a trade-off between them.
using System; using System.Collections; using System.Collections.Generic; class BeforeAndAfterGenerics { static void Main() { // BEFORE GENERICS var legacyScores = new ArrayList(); legacyScores.Add(95); // int gets BOXED onto the heap legacyScores.Add(87); legacyScores.Add("oops"); // compiler is fine with this string! try { foreach (object item in legacyScores) { int score = (int)item; // UNBOXING — risky cast every time Console.WriteLine($"Legacy score: {score}"); } } catch (InvalidCastException ex) { Console.WriteLine($"Runtime crash: {ex.Message}"); } // AFTER GENERICS var modernScores = new List<int>(); modernScores.Add(95); // stored directly, no boxing modernScores.Add(87); // modernScores.Add("oops"); // COMPILE ERROR: cannot convert string to int foreach (int score in modernScores) // no cast needed { Console.WriteLine($"Modern score: {score}"); } } }
Writing Your Own Generic Class — Building a Type-Safe Result Wrapper
The best way to deeply understand generics is to build something you'd actually use in production. A Result<T> wrapper is a perfect example — it represents either a successful value or an error, without throwing exceptions for expected failure cases. This pattern is common in functional-leaning C# codebases and in every API layer that needs to communicate failure without polluting control flow with exceptions.
The T in Result<T> is a type parameter — a placeholder that the compiler replaces with a concrete type when you instantiate the class. You can name it anything, but T is the convention for a single generic type. TKey and TValue are conventional for two parameters, as you see in Dictionary.
Notice how the class is defined once, but can hold a string result, an int result, or a complex User object result. The internal logic — storing the value, checking success, returning errors — is written exactly once. That's the core promise of generics: write the shape of the behaviour, defer the type decision to the caller.
The private constructor pattern combined with static factory methods also means you can never accidentally create a Result<T> in an invalid state — a bonus architectural win that generics enable cleanly.
using System; public class Result<T> { public T? Value { get; } public string? ErrorMessage { get; } public bool IsSuccess { get; } private Result(T? value, string? error, bool isSuccess) { Value = value; ErrorMessage = error; IsSuccess = isSuccess; } public static Result<T> Success(T value) => new Result<T>(value, null, true); public static Result<T> Failure(string errorMessage) => new Result<T>(default, errorMessage, false); public override string ToString() => IsSuccess ? $"Success: {Value}" : $"Failure: {ErrorMessage}"; } public class UserService { public Result<string> FindUsername(int userId) { if (userId == 42) return Result<string>.Success("ada.lovelace"); return Result<string>.Failure($"No user found with ID {userId}"); } } class Program { static void Main() { var service = new UserService(); Result<string> found = service.FindUsername(42); if (found.IsSuccess) Console.WriteLine($"Found user: {found.Value}"); else Console.WriteLine($"Error: {found.ErrorMessage}"); Result<string> notFound = service.FindUsername(99); Console.WriteLine(notFound); Result<int> calculationResult = Result<int>.Success(1337); Console.WriteLine($"Calculation gave us: {calculationResult.Value + 1}"); } }
Generic Constraints — Teaching the Compiler What T Can Do
Here's the most powerful — and most misunderstood — feature of C# generics: constraints. Without them, T is a complete mystery to the compiler. It could be anything, so you can only call the methods that every single type in C# shares: ToString(), GetHashCode(), and Equals(). That's a pretty short list.
Constraints let you tell the compiler 'T is guaranteed to be at least this kind of thing'. Once you add a constraint, the compiler unlocks every method and property defined by that constraint. You get IntelliSense, type checking, and zero casting.
The where keyword is how you add constraints. The most common ones are: where T : class (T must be a reference type), where T : struct (T must be a value type), where T : new() (T must have a parameterless constructor), and where T : ISomeInterface (T must implement that interface). You can combine multiple constraints on the same type parameter.
The interface constraint is the one you'll use most in real codebases. It's how you write algorithms that are generic over behaviour, not type. A sorting method that works on anything sortable, a repository that works on anything with an ID — these are built with interface constraints.
using System; using System.Collections.Generic; public interface IEntity { int Id { get; } string Describe(); } public class Product : IEntity { public int Id { get; init; } public string Name { get; init; } = string.Empty; public decimal Price { get; init; } public string Describe() => $"Product #{Id}: {Name} at ${Price:F2}"; } public class Employee : IEntity { public int Id { get; init; } public string FullName { get; init; } = string.Empty; public string Department { get; init; } = string.Empty; public string Describe() => $"Employee #{Id}: {FullName} in {Department}"; } public class InMemoryRepository<T> where T : IEntity { private readonly Dictionary<int, T> _store = new(); public void Save(T entity) { _store[entity.Id] = entity; Console.WriteLine($"Saved: {entity.Describe()}"); } public T? FindById(int id) { _store.TryGetValue(id, out T? entity); return entity; } public void PrintAll() { foreach (var entry in _store.Values) Console.WriteLine($" -> {entry.Describe()}"); } } public static class EntityMapper { public static List<TResult> MapDescriptions<TSource, TResult>( IEnumerable<TSource> entities, Func<TSource, TResult> mapFunc) where TSource : IEntity where TResult : new() { var results = new List<TResult>(); foreach (var entity in entities) results.Add(mapFunc(entity)); return results; } } class Program { static void Main() { var productRepo = new InMemoryRepository<Product>(); productRepo.Save(new Product { Id = 1, Name = "Mechanical Keyboard", Price = 149.99m }); productRepo.Save(new Product { Id = 2, Name = "USB-C Hub", Price = 49.95m }); Console.WriteLine("\nAll products:"); productRepo.PrintAll(); var employeeRepo = new InMemoryRepository<Employee>(); employeeRepo.Save(new Employee { Id = 101, FullName = "Grace Hopper", Department = "Engineering" }); employeeRepo.Save(new Employee { Id = 102, FullName = "Alan Turing", Department = "Research" }); Console.WriteLine("\nAll employees:"); employeeRepo.PrintAll(); Product? keyboard = productRepo.FindById(1); Console.WriteLine($"\nFound: {keyboard?.Describe() ?? "not found"}"); } }
ToString() on T works without constraint, but .CompareTo() does not.Generic Interfaces and Covariance — The Pattern Behind LINQ and IEnumerable
Once you're comfortable writing generic classes, the next level is understanding generic interfaces and variance — specifically covariance (out) and contravariance (in). These aren't academic features; they're why you can assign a List<string> to an IEnumerable<string> variable, and why LINQ works seamlessly across all collection types.
Covariance means a generic type with a more derived type argument can be treated as a generic type with a base type. So IEnumerable<string> can be assigned to IEnumerable<object> because string derives from object, and IEnumerable<T> is declared with out T — meaning T is only ever produced (returned), never consumed. The out keyword is what tells the compiler it's safe to widen the type.
Contravariance is the reverse — Action<object> can be assigned to Action<string> because Action<T> uses in T, meaning T is only consumed (taken as input). If you can handle any object, you can certainly handle a string.
In practice, you'll consume covariant and contravariant interfaces far more often than you'll write them. But knowing WHY IEnumerable<T> uses out T explains why so much LINQ code just works, and it's the kind of deep knowledge that separates engineers who use the framework from those who understand it.
using System; using System.Collections.Generic; public interface IProducer<out T> { T Produce(); } public interface IConsumer<in T> { void Consume(T item); } public class Animal { public string Name { get; init; } = string.Empty; public virtual string Sound() => "..."; } public class Dog : Animal { public override string Sound() => "Woof"; } public class DogProducer : IProducer<Dog> { public Dog Produce() => new Dog { Name = "Rex" }; } public class AnimalLogger : IConsumer<Animal> { public void Consume(Animal animal) => Console.WriteLine($"Logging animal: {animal.Name} says {animal.Sound()}"); } class Program { static void Main() { // COVARIANCE: IProducer<Dog> -> IProducer<Animal> IProducer<Dog> dogProducer = new DogProducer(); IProducer<Animal> animalProducer = dogProducer; Animal producedAnimal = animalProducer.Produce(); Console.WriteLine($"Produced: {producedAnimal.Name} says {producedAnimal.Sound()}"); // CONTRAVARIANCE: IConsumer<Animal> -> IConsumer<Dog> IConsumer<Animal> animalConsumer = new AnimalLogger(); IConsumer<Dog> dogConsumer = animalConsumer; dogConsumer.Consume(new Dog { Name = "Buddy" }); // REAL-WORLD COVARIANCE: IEnumerable<T> List<string> dogNames = new() { "Max", "Bella", "Charlie" }; IEnumerable<object> objectNames = dogNames; Console.WriteLine("\nDog names as objects:"); foreach (object name in objectNames) Console.WriteLine($" {name}"); } }
Generic Methods and Type Inference — When the Compiler Deducing T Works (and When It Doesn't)
Generic methods are distinct from generic classes. You can have a generic method inside a non-generic class, and the type parameter is inferred from the arguments you pass. This is incredibly convenient — you don't need to specify the type unless the compiler can't figure it out. For example, when you write var result = Helper.Swap(ref a, ref b); the compiler infers T from the type of a.
But type inference has limits. If the method's type parameter appears only in the return type, the compiler cannot infer it — you must specify it explicitly. This is common in factory patterns: T Create<T>() where T : new() requires you to call Create<MyType>().
Another common gotcha is overload resolution. If two overloads differ only by a generic type parameter, the compiler may pick the wrong one or fail with an ambiguity error. In that case, explicitly specifying the type argument resolves the ambiguity.
Understanding when inference works and when it doesn't separates developers who fight the compiler from those who let it work for them.
using System; using System.Collections.Generic; public class Helper { public static void Swap<T>(ref T a, ref T b) { T temp = a; a = b; b = temp; } public static T Create<T>() where T : new() { return new T(); } public static void Process<T>(T item) { Console.WriteLine($"Single item of type {typeof(T).Name}: {item}"); } public static void Process<T>(IEnumerable<T> items) { Console.WriteLine($"Collection of {typeof(T).Name}:"); foreach (var item in items) Console.WriteLine($" - {item}"); } } public class Example { public static void Main() { int x = 1, y = 2; Helper.Swap(ref x, ref y); Console.WriteLine($"Swapped: x={x}, y={y}"); string s1 = "hello", s2 = "world"; Helper.Swap(ref s1, ref s2); Console.WriteLine($"Swapped: s1={s1}, s2={s2}"); var list = Helper.Create<List<int>>(); list.Add(42); Console.WriteLine($"Created list with: {list[0]}"); List<int> numbers = new() { 10, 20, 30 }; Helper.Process(numbers); int[] array = { 1, 2, 3 }; Helper.Process(array); Helper.Process<int[]>(array); } }
Real-World Generic Patterns — Repository, Specification and Type-Safe Builders
Now that you understand constraints and variance, let's look at three real-world patterns that use generics to solve production problems. These aren't academic — they're patterns you'll find in every mature C# codebase.
The Generic Repository pattern keeps data access code consistent across entity types. With a constraint like where T : IEntity, you get a single implementation that handles Product, Order, User — any type with an identity. The pattern reduces duplication, but it also introduces a decision: do you build one grand repository or compose small ones? Generics let you do either.
The Specification pattern pairs with generics to build composable, testable query logic. A Specification<T> is a predicate wrapped in a class. You combine specifications with &&, ||, and ! operators. Pass them to a generic repository method: repository.Find(spec). The T makes the specification reusable across entity types without casting.
Type-safe Builders use generic methods to enforce a construction sequence at compile time. Instead of a builder that throws InvalidOperationException when you call Build() too early, you make each step return a new builder type. The compiler prevents you from creating an invalid object in the first place — no runtime checks needed.
using System; using System.Linq.Expressions; using System.Collections.Generic; // Specification pattern — encapsulate query logic public class Specification<T> { public Expression<Func<T, bool>> Criteria { get; } public Specification(Expression<Func<T, bool>> criteria) => Criteria = criteria; public static Specification<T> operator &(Specification<T> left, Specification<T> right) => new Specification<T>(CombineAnd(left.Criteria, right.Criteria)); private static Expression<Func<T, bool>> CombineAnd( Expression<Func<T, bool>> left, Expression<Func<T, bool>> right) { var param = Expression.Parameter(typeof(T)); var body = Expression.AndAlso( Expression.Invoke(left, param), Expression.Invoke(right, param)); return Expression.Lambda<Func<T, bool>>(body, param); } } public interface IEntity { int Id { get; } } public class Order : IEntity { public int Id { get; init; } public decimal Total { get; init; } } // Generic repository with specification support public class Repository<T> where T : IEntity { private readonly List<T> _store = new(); public void Add(T entity) => _store.Add(entity); public IEnumerable<T> Find(Specification<T> spec) { var compiled = spec.Criteria.Compile(); return _store.FindAll(item => compiled(item)); } public IEnumerable<T> GetAll() => _store.AsReadOnly(); } // Type-safe builder: compile-time construction enforcement public class EmailBuilder { private string _to = string.Empty; private string _subject = string.Empty; public EmailBuilder WithTo(string to) { _to = to; return this; } public EmailBuilder WithSubject(string subject) { _subject = subject; return this; } public Email Build() => new Email(_to, _subject); } public record Email(string To, string Subject); class Program { static void Main() { var repo = new Repository<Order>(); repo.Add(new Order { Id = 1, Total = 99.95m }); repo.Add(new Order { Id = 2, Total = 199.99m }); var largeOrders = new Specification<Order>(o => o.Total > 100); foreach (var order in repo.Find(largeOrders)) Console.WriteLine($"Large order #{order.Id}: ${order.Total}"); var email = new EmailBuilder() .WithTo("user@example.com") .WithSubject("Your order confirmation") .Build(); Console.WriteLine($"Email ready: {email}"); } }
- A non-generic class says: I work with Product. A generic class says: I work with anything that has an Id.
- Constraints define the minimum set of capabilities a type must have to work with your code.
- The more generic your code, the more constraints you need — otherwise the compiler can't guarantee anything.
- Every generic parameter is a trade-off: more flexibility means more complexity in understanding the code.
- The goal isn't maximum genericity — it's the right amount of genericity for your use case.
Generic Delegates — The Silent Assassin in Event-Driven Systems
Delegates without generics are null-checks waiting to happen. In trading engines, a price-feed handler that casts from object will eventually blow up when someone passes an OrderBook instead of a Quote. Generic delegates — Action<T>, Func<T, TResult>, Predicate<T> — force the contract at compile time. Your event publisher says 'I expect a Quote' and the compiler enforces it. No runtime casting, no reflection overhead. The pattern is brutal: define a delegate that matches your payload exactly, then wire it with generics. When the market data feed changes schema, the compiler breaks every mismatched subscriber before CI finishes. That's not paranoia; that's production discipline. The junior who wires a multicast delegate with object is the same junior who gets paged at 3 AM because a JSON parser returned a different type than expected.
// io.thecodeforge public delegate void QuoteHandler<T>(T quote) where T : IQuote; public sealed class MarketDataFeed { private readonly List<QuoteHandler<IQuote>> _subscribers = new(); public void Subscribe(QuoteHandler<IQuote> handler) => _subscribers.Add(handler); public void Publish(IQuote quote) { foreach (var handler in _subscribers) handler(quote); // Compile-time safe – no casting } } public interface IQuote { decimal Price { get; } string Symbol { get; } }
Generic Constraints — The Nine Words That Saved a Weekend
Constraints aren't compiler pedantry; they're contracts that prevent runtime explosions. Without constraints, your generic repository's GetById<T> will happily accept T = StringBuilder, then fail when you try to cast it to an entity. The 'where T : class' constraint says 'only reference types'. The 'where T : new()' constraint promises the default constructor exists. Stack them: 'where T : class, IEntity, new()'. Now your generic cache actually works because every T has an Id property and a parameterless constructor. The junior who says 'I'll add constraints later' is the same junior who ship a generic serializer that throws on structs. In .NET 8, constraints also enable static abstract interface methods — your generic math finally compiles. 'where T : INumber<T>' unlocks Add, Subtract, Multiply without boxing. That is the difference between latency-critical code and toy examples.
// io.thecodeforge public interface IEntity { int Id { get; } } public sealed class EntityCache<T> where T : class, IEntity, new() { private readonly Dictionary<int, T> _store = new(); public T GetOrCreate(int id) { if (_store.TryGetValue(id, out var entity)) return entity; entity = new T { Id = id }; // new() constraint guarantees this works _store[id] = entity; return entity; } } // Usage – compiler rejects anything that doesn't satisfy all constraints // EntityCache<string> cache; // ERROR: string doesn't implement IEntity // EntityCache<SomeRecord> cache; // ERROR: record structs may not have parameterless ctor
Boxing-Induced OutOfMemory in a High-Frequency Trading Engine
- Value types in non-generic collections cause boxing overhead in every read and write.
- When profiling shows high GC, check collections for boxing — it's the silent killer.
- Generics are not just a safety feature; they are a performance requirement in hot paths dealing with value types.
ToList() or use .ElementAt(). Beware of multiple enumeration — cache to list if iterating more than once.ToList() or use .ConvertAll().dotnet-counters monitor --counters System.Runtime --process-id <pid>dotnet-dump collect --process-id <pid> then analyze with dotnet-dump analyzeAdd explicit type arguments: Method<MyType>(arg);If overloads exist, verify overload resolution by removing one overload temporarily.Use IEnumerable<object> for covariance, not List<object>.If you need a mutable collection of a base type, use .Cast<T>().ToList().| Aspect | Non-Generic (object / ArrayList) | Generic (List<T>, custom class<T>) |
|---|---|---|
| Type Safety | Runtime — errors surface as InvalidCastException when executed | Compile-time — type mismatch caught before the program ever runs |
| Casting Required | Yes — every read requires an explicit (Type) cast | No — the compiler already knows the type, no cast needed |
| Boxing of Value Types | Yes — int/double are boxed to heap on every write | No — value types stored directly, zero boxing overhead |
| Code Reuse | One class handles all types via object, but unsafely | One class handles all types via T, fully type-checked |
| IntelliSense Support | Minimal — IDE only knows it's object | Full — IDE knows the real type, shows all members |
| Readability | Unclear — you must hunt for cast comments to know the type | Self-documenting — List<Invoice> tells you exactly what's inside |
| Performance (hot paths) | Degraded — boxing/unboxing generates garbage for GC | Optimal — no heap allocation overhead for value types |
| File | Command / Code | Purpose |
|---|---|---|
| BeforeAndAfterGenerics.cs | using System; | The Problem Generics Solve |
| ResultWrapper.cs | using System; | Writing Your Own Generic Class |
| GenericConstraints.cs | using System; | Generic Constraints |
| VarianceAndGenericInterfaces.cs | using System; | Generic Interfaces and Covariance |
| GenericMethodsInference.cs | using System; | Generic Methods and Type Inference |
| RealWorldPatterns.cs | using System; | Real-World Generic Patterns |
| MarketDataFeed.cs | public delegate void QuoteHandler | Generic Delegates |
| EntityCache.cs | public interface IEntity { int Id { get; } } | Generic Constraints |
Key takeaways
Common mistakes to avoid
5 patternsAssuming T can do anything without constraints
Using a generic class when a generic method is all you need
Clone(). Reserve generic classes for when state must be stored per-T.Confusing covariance with inheritance and getting an InvalidCastException
Cat()) and corrupt it.Over-constraining with new() when the type doesn't need construction
new() to a generic class, but callers that use reference types with parameterized constructors cannot use your class. They get a compile error even though your code never actually calls new T().new() if you explicitly call the parameterless constructor somewhere in your generic code. Otherwise leave it off. Adding unnecessary constraints limits reusability and forces callers into workarounds.Using typeof(T) for runtime type discrimination inside generic methods
Interview Questions on This Topic
What is the difference between a generic constraint 'where T : class' and 'where T : IMyInterface', and when would you choose one over the other?
new()'.Why can you assign List
If you have a method that needs to work on any type T that can be compared for ordering, what constraint would you add, and what interface does that constraint typically reference?
Explain how type inference works for generic methods. What's the one case where the compiler cannot infer T and you must specify it explicitly?
new() requires explicit specification: Create<MyType>(). This is because the compiler can only infer from input parameters, not from how the return value is assigned (type inference in C# is input-based, not output-based). Another edge case: if the method has multiple overloads and inference produces ambiguous results, you must specify the type explicitly.Frequently Asked Questions
T is just a conventional name for a type parameter — a placeholder the compiler replaces with a real type when you use the class or method. You could name it anything (TItem, TEntity), but T is the single-parameter convention. It carries no special meaning by itself; its behaviour is entirely determined by any constraints you add with the where keyword.
Absolutely. Dictionary<TKey, TValue> is the most famous example in the BCL. You declare them as class MyPair<TFirst, TSecond> and can add separate constraints on each: where TFirst : class where TSecond : struct. Each type parameter is independent — callers supply both when they instantiate the class.
Yes, and it's most significant for value types. A List<int> stores integers directly in contiguous memory. An ArrayList stores each integer boxed as an object on the heap. In a tight loop processing millions of integers, the non-generic version generates enormous garbage collection pressure. For reference types the difference is smaller, but the compile-time safety of generics is still worth it regardless of performance.
Use a generic method when only a single method needs to operate on a generic type, and that type doesn't need to be stored as state. For example, a Swap<T> method doesn't need a class — it can be a static method in a utility class. If you need to store state across multiple methods (like a Repository<T> that has Save, FindById, etc.), use a generic class. The rule: prefer generic methods to reduce complexity unless you need per-type state.
No. The compiler doesn't allow operators on an unconstrained T because not all types support + or -. For arithmetic on generics, you have two options: use the interfaces from System.Numerics (like IAdditionOperators<T, T, T>) available in .NET 7+, or provide a calculator delegate: Func<T, T, T> add. The first approach is cleaner but requires .NET 7 or later.
Because List<T> both produces and consumes T. The Add(T) method consumes T (input), and the indexer T this[int] gets T (output). Covariance (out) requires T to appear only in output positions. Contravariance (in) requires T to appear only in input positions. Since List<T> does both, it must be invariant. If it were covariant, you could add an Apple to a List<Banana> through a List<Fruit> reference — that would break type safety.
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
That's OOP in C#. Mark it forged?
7 min read · try the examples if you haven't