Generics let you write type-safe, reusable code without duplicating logic for every data type, and they power much of the .NET collections framework under the hood.
This collection of 20 C# generics exercises covers writing your own generic methods and classes, applying type constraints, and understanding how generics improve both safety and performance compared to using object.
Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you build real confidence writing reusable, type-safe C# code.
Also, See: C# Exercises with 22 topic-wise sets and 620+ practice questions.
What You’ll Practice
- Generic Methods: Type parameters and type inference.
- Generic Classes: Building reusable containers and utility classes.
- Constraints:
whereclauses likestruct,class, andnew(). - Real-World Use: Comparing generics to
object-based and non-generic code.
+ Table Of Contents (20 Exercises)
Table of contents
- Exercise 1: Generic Box
- Exercise 2: Swap Array Elements
- Exercise 3: Generic Pair
- Exercise 4: Print Collection
- Exercise 5: Reverse Array
- Exercise 6: Max Finder (Comparable Constraint)
- Exercise 7: Entity Repository (Class Constraint)
- Exercise 8: Factory Pattern (New() Constraint)
- Exercise 9: Generic Stack
- Exercise 10: ID-Based Search
- Exercise 11: Nullable Struct Wrapper
- Exercise 12: Double Constraint Repository
- Exercise 13: Generic Validator
- Exercise 14: Generic Cache
- Exercise 15: Type Converter
- Exercise 16: Covariant Logger (Out Keyword)
- Exercise 17: Contravariant Consumer (In Keyword)
- Exercise 18: Generic Event Broker
- Exercise 19: Custom LINQ Filter
- Exercise 20: Generic Spec Evaluation
Exercise 1: Generic Box
Practice Problem: Create a generic Box<T> class that holds a single value of type T. Include methods to Update(T newValue) and Retrieve().
Purpose: This exercise helps you practice defining a generic class, storing a value of a type parameter in a private field, and exposing methods to read and update that value safely without casting or boxing.
Given Input: Box<int> intBox = new Box<int>(42); intBox.Update(100);
Expected Output: Retrieved Value = 100
▼ Hint
- Declare a private field of type
Tto store the value. - Add a constructor that accepts an initial value of type
T. - Implement
Update(T newValue)to overwrite the stored field. - Implement
Retrieve()to return the current stored value.
▼ Solution & Explanation
Explanation:
private T _value: Stores a value of whatever typeTis bound to when the class is instantiated.Box(T initialValue): The constructor assigns the starting value to the field without any casting.Update(T newValue): Replaces the internal value with a new one of the same typeT.Retrieve(): Returns the current value, type safe becauseTis fixed for this instance.
Exercise 2: Swap Array Elements
Practice Problem: Write a generic method Swap<T>(T[] array, int index1, int index2) that swaps the positions of two elements in any array.
Purpose: This exercise helps you practice writing a standalone generic method (not tied to a generic class), letting the compiler infer the type parameter from the array argument, and mutating an array in place.
Given Input: int[] numbers = { 1, 2, 3, 4, 5 }; Swap(numbers, 1, 3);
Expected Output: {1, 4, 3, 2, 5}
▼ Hint
- Declare a static generic method
Swap<T>(T[] array, int index1, int index2). - Store
array[index1]in a temporary variable before overwriting it. - Assign
array[index2]toarray[index1]. - Assign the saved temporary value to
array[index2]to complete the swap.
▼ Solution & Explanation
Explanation:
public static class ArrayHelper: In C#, a method cannot exist on its own; it must live inside a class, so the generic method is placed in a small static helper class.static void Swap<T>(T[] array, ...): Declares a generic method whose type parameterTis inferred automatically from the array passed in.T temp = array[index1]: Saves the first element so it is not lost once it gets overwritten.array[index1] = array[index2]: Copies the second element into the first position.array[index2] = temp: Places the saved value into the second position, finishing the swap.
Exercise 3: Generic Pair
Practice Problem: Create a Pair<TKey, TValue> class that holds two related values of different types, mimicking a basic KeyValuePair.
Purpose: This exercise helps you practice working with multiple type parameters on a single class, and shows how two independent generic types can be combined into one reusable structure.
Given Input: Pair<string, int> pair = new Pair<string, int>("Age", 25);
Expected Output: Key = Age, Value = 25
▼ Hint
- Declare two type parameters,
TKeyandTValue, on the class. - Add two properties, one of type
TKeyand one of typeTValue. - Add a constructor that accepts both values and assigns them to the properties.
- Override
ToString(), or print the properties directly, to display the pair.
▼ Solution & Explanation
Explanation:
Pair<TKey, TValue>: Declares two independent type parameters so the key and value can be different, unrelated types.public TKey Key { get; set; }: Exposes the first value as a strongly typed property.public TValue Value { get; set; }: Exposes the second value as a strongly typed property.Pair(TKey key, TValue value): The constructor initializes both properties in a single call.
Exercise 4: Print Collection
Practice Problem: Write a generic method PrintList<T>(List<T> list) that iterates through and prints every element of a list to the console.
Purpose: This exercise helps you practice iterating over a generic List<T>, and writing a reusable method that works for a list of any element type.
Given Input: List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
Expected Output:
Apple Banana Cherry
▼ Hint
- Declare a generic method
PrintList<T>(List<T> list). - Use a
foreachloop to go through each element inlist. - Print each element on its own line with
Console.WriteLine.
▼ Solution & Explanation
Explanation:
public static class ListPrinter: Methods cannot exist outside a class in C#, so the generic method is placed inside a small static helper class.PrintList<T>(List<T> list): Declares a generic method that accepts a list of any element typeT.foreach (T item in list): Iterates through every element without needing to know the concrete type in advance.Console.WriteLine(item): Prints each element, relying on its defaultToString()representation.
Exercise 5: Reverse Array
Practice Problem: Write a generic method Reverse<T>(T[] array) that reverses the elements of an array in place.
Purpose: This exercise helps you practice the two-pointer technique for in-place array manipulation, combined with generic type parameters so the logic works for any element type.
Given Input: int[] numbers = { 1, 2, 3, 4, 5 };
Expected Output: {5, 4, 3, 2, 1}
▼ Hint
- Keep two index pointers, one starting at the beginning and one at the end.
- Swap the elements at the two pointers, then move them toward each other.
- Continue until the left pointer meets or passes the right pointer.
▼ Solution & Explanation
Explanation:
public static class ArrayHelper: Since a method cannot exist outside a class in C#, the generic method is placed inside a small static helper class.int left = 0; int right = array.Length - 1: Sets up two pointers at opposite ends of the array.while (left < right): Keeps swapping as long as the pointers have not crossed.T temp = array[left]: Temporarily holds the left value so it can be restored on the right side.left++; right--: Moves both pointers one step closer to the middle after each swap.
Exercise 6: Max Finder (Comparable Constraint)
Practice Problem: Write a generic method FindMax<T>(T a, T b) that returns the larger of two values. Restrict T to types that implement IComparable<T>.
Purpose: This exercise helps you practice applying a generic type constraint so the compiler guarantees the type supports comparison, letting one method work correctly across numbers, strings, and any other comparable type.
Given Input: FindMax(10, 20) and FindMax("apple", "banana")
Expected Output:
Max = 20 Max = banana
▼ Hint
- Constrain the method with
where T : IComparable<T>. - Call
a.CompareTo(b)to compare the two values. - Return
aif the result is greater than zero, otherwise returnb.
▼ Solution & Explanation
Explanation:
public static class Comparer: A method cannot exist outside a class in C#, so the generic method is placed inside a small static helper class.where T : IComparable<T>: RestrictsTto types that know how to compare themselves to another instance of the same type.a.CompareTo(b): Returns a positive number whenais greater, zero when equal, and negative when smaller.> 0 ? a : b: Uses the comparison result to pick and return the larger of the two values.
Exercise 7: Entity Repository (Class Constraint)
Practice Problem: Create a generic repository class Repository<T> where T must be a reference type (where T : class). Include basic Add(T entity) and GetAll() methods.
Purpose: This exercise helps you practice using a reference-type constraint to model a common data-access pattern, and shows how a single generic class can store and return any entity type.
Given Input: Repository<string> repo = new Repository<string>(); repo.Add("Item1"); repo.Add("Item2");
Expected Output:
Item1 Item2
▼ Hint
- Add
where T : classto the class declaration to restrictTto reference types. - Store entities internally in a
List<T>. Add(T entity)should append to the internal list.GetAll()should return the internal list, or a read-only view of it.
▼ Solution & Explanation
Explanation:
where T : class: RestrictsTto reference types, which fits the typical use case of storing entity objects rather than value types.private List<T> _entities: Backs the repository with an in-memory generic list.Add(T entity): Appends a new entity to the internal collection.GetAll(): Returns every stored entity so the caller can iterate over them.
Exercise 8: Factory Pattern (New() Constraint)
Practice Problem: Create a generic factory class EntityFactory<T> with a method T CreateInstance() that returns a new instance of T. Use the where T : new() constraint.
Purpose: This exercise helps you practice the new() constraint, which lets a generic class create instances of its type parameter directly, a common building block for factory and dependency-injection style patterns.
Given Input: EntityFactory<Product> factory = new EntityFactory<Product>(); Product product = factory.CreateInstance();
Expected Output: New instance created: Product
▼ Hint
- Add
where T : new()to the class declaration so the compiler knowsThas a parameterless constructor. - Inside
CreateInstance(), returnnew T(). - Use
typeof(T).Nameif you want to print the created type’s name.
▼ Solution & Explanation
Explanation:
where T : new(): Guarantees thatThas an accessible parameterless constructor, so it can be created inside the generic class.return new T(): Creates a brand new instance of whatever typeTwas bound to.product.GetType().Name: Reads the runtime type name so the created type can be printed, here it resolves to Product.
Exercise 9: Generic Stack
Practice Problem: Implement a custom CustomStack<T> class, without using System.Collections.Generic.Stack, using an internal array or linked nodes, supporting Push, Pop, and Peek.
Purpose: This exercise helps you practice building a generic data structure from scratch, managing an internal collection manually, and enforcing last-in-first-out behavior for any element type.
Given Input: CustomStack<int> stack = new CustomStack<int>(); stack.Push(10); stack.Push(20); stack.Push(30);
Expected Output:
Peek = 30 Pop = 30 Peek after pop = 20
▼ Hint
- Use an internal
List<T>to hold the elements, treating the end of the list as the top of the stack. Push(T item)should add to the end of the internal list.Pop()should remove and return the last element, throwing an exception if the stack is empty.Peek()should return the last element without removing it.
▼ Solution & Explanation
Explanation:
private List<T> _items: Backs the stack with a resizable internal list instead of the built-in Stack type.Push(T item): Adds the new item to the end of the internal list, which represents the top of the stack.Pop(): Removes and returns the last element, throwing an exception first if the stack has nothing left.Peek(): Returns the last element without modifying the internal list.
Exercise 10: ID-Based Search
Practice Problem: Create an interface IIdentifiable { int Id { get; } }. Write a generic method FindById<T>(List<T> items, int id) where T : IIdentifiable that returns the matching item.
Purpose: This exercise helps you practice constraining a generic type to an interface, so the method can rely on a shared Id property regardless of the concrete type being searched.
Given Input: A list of Employee objects with Ids 1, 2, and 3, calling FindById(employees, 2).
Expected Output: Found: Id = 2, Name = Bob
▼ Hint
- Define
IIdentifiablewith a single read-onlyint Id { get; }property. - Constrain the method with
where T : IIdentifiablesoIdis accessible on anyT. - Loop through the list and return the first item whose
Idmatches. - Return
default(T)(ornull) if no match is found.
▼ Solution & Explanation
Explanation:
interface IIdentifiable: Defines a contract guaranteeing that any implementing type exposes a readableIdproperty.where T : IIdentifiable: Restricts the generic method so it can safely readitem.Idon any typeT.if (item.Id == id): Compares each item’s Id against the target Id to find a match.return default(T): Provides a safe fallback value, null for reference types, if no item matches the given id.
Exercise 11: Nullable Struct Wrapper
Practice Problem: Create a custom generic struct Optional<T> where T : struct that mimics Nullable<T>, tracking whether a value is present or not.
Purpose: This exercise helps you practice constraining a generic type to value types with where T : struct, and shows how to model presence or absence of a value without relying on the built-in Nullable<T>.
Given Input: Optional<int> some = new Optional<int>(5); Optional<int> none = default(Optional<int>);
Expected Output:
HasValue = True, Value = 5 HasValue = False
▼ Hint
- Add
where T : structto the struct declaration. - Store a private
Tfield and a privateboolflag for whether a value was set. - Have the constructor set the value and flip the flag to true.
- Expose
HasValueas a read-only property, and throw from theValuegetter when no value is present.
▼ Solution & Explanation
Explanation:
where T : struct: RestrictsTto value types, matching howNullable<T>itself is constrained._hasValue = true: Marks the wrapper as populated as soon as a value is supplied through the constructor.default(Optional<int>): Produces a struct with_hasValuedefaulted tofalse, representing an empty optional.Valuegetter: Throws an exception if accessed while_hasValueisfalse, preventing use of an unset value.
Exercise 12: Double Constraint Repository
Practice Problem: Modify your Repository<T> so that T must be a class, must have a parameterless constructor, and must implement an IStoredProcedure interface.
Purpose: This exercise helps you practice combining multiple generic constraints, class, new(), and an interface, on a single type parameter, and shows how the compiler enforces all of them together at the call site.
Given Input: Repository<UserProcedure> repo = new Repository<UserProcedure>(); repo.CreateAndAdd();
Expected Output:
Executing stored procedure logic. Total procedures stored: 1
▼ Hint
- Combine constraints in one clause:
where T : class, IStoredProcedure, new(). - Define
IStoredProcedurewith anExecute()method. - Add a
CreateAndAdd()method that constructs a newT, callsExecute()on it, and stores it. - Remember that constraints must be listed in the order class/struct, interfaces, then
new()last.
▼ Solution & Explanation
Explanation:
where T : class, IStoredProcedure, new(): RequiresTto be a reference type, implement the interface, and expose a parameterless constructor, all at once.T entity = new T(): Valid only because of thenew()constraint, this creates a fresh instance of the concrete type.entity.Execute(): Valid only because of theIStoredProcedureconstraint, letting the repository invoke behavior it otherwise couldn’t know about.repo.GetAll().Count: Confirms the newly created entity was added to the internal list.
Exercise 13: Generic Validator
Practice Problem: Create a generic class Validator<T> that accepts a generic delegate Predicate<T> in its constructor. Add a Validate(T item) method that returns a boolean.
Purpose: This exercise helps you practice combining generics with delegates, letting the validation rule itself be supplied by the caller instead of being hardcoded inside the class.
Given Input: Validator<int> positiveValidator = new Validator<int>(n => n > 0);
Expected Output:
Is 10 valid? True Is -5 valid? False
▼ Hint
- Store the constructor’s
Predicate<T>argument in a private field. - In
Validate(T item), invoke the stored predicate withitemand return its result. - At the call site, pass a lambda expression matching the rule you want to check.
▼ Solution & Explanation
Explanation:
Predicate<T> _rule: Holds a reference to whatever validation logic the caller passed in, decoupled from the class itself.Validator(Predicate<T> rule): Accepts the rule as a constructor argument and saves it for later use.Validate(T item): Runs the stored predicate against the given item and returns the boolean result.n => n > 0: A lambda expression supplied at the call site, defining “positive” as the actual validation rule.
Exercise 14: Generic Cache
Practice Problem: Implement a Cache<TKey, TValue> class using a Dictionary under the hood. Include a basic time-to-live expiration check mechanism.
Purpose: This exercise helps you practice wrapping a generic Dictionary<TKey, TValue> inside your own generic class, and shows how to attach extra metadata, like an expiration time, to each cached entry.
Given Input: Cache<string, string> cache = new Cache<string, string>(); cache.Set("greeting", "Hello", TimeSpan.FromSeconds(5));
Expected Output:
Found: Hello Not found or expired.
▼ Hint
- Create a small private class or struct that stores the cached value along with an expiration timestamp.
- Back the cache with a
Dictionary<TKey, CacheItem>. - In
Set, compute the expiration time asDateTime.UtcNow.Add(ttl). - In
TryGet, returnfalseif the key is missing or ifDateTime.UtcNowhas passed the stored expiration.
▼ Solution & Explanation
Explanation:
private class CacheItem: A small internal wrapper that pairs the cached value with the time it expires.DateTime.UtcNow.Add(ttl): Calculates the exact expiration timestamp when an entry is first stored.DateTime.UtcNow < item.ExpiresAt: Checks whether the entry is still valid at the moment it is requested.TryGet: Follows the standard try-pattern, returningfalseand a default value instead of throwing when the key is missing or expired.
Exercise 15: Type Converter
Practice Problem: Write a generic method ConvertType<TInput, TOutput>(TInput input) that attempts to safely cast or convert TInput into TOutput, handling errors gracefully.
Purpose: This exercise helps you practice using two independent type parameters on a method, and shows how to lean on Convert.ChangeType alongside a try/catch block to convert between types safely at runtime.
Given Input: ConvertType<string, int>("123") and ConvertType<string, int>("abc")
Expected Output:
Converted = 123 Converted = 0
▼ Hint
- Declare the method with two type parameters,
TInputandTOutput. - Wrap the conversion attempt in a
tryblock usingConvert.ChangeType(input, typeof(TOutput)). - Cast the result of
Convert.ChangeTypeback toTOutput. - In the
catchblock, returndefault(TOutput)instead of letting the exception propagate.
▼ Solution & Explanation
Explanation:
ConvertType<TInput, TOutput>(TInput input): Declares a method with two separate type parameters, one for the source type and one for the target type.Convert.ChangeType(input, typeof(TOutput)): Performs the actual runtime conversion based on the target type’s reflection information.catch (Exception): Catches invalid conversions, such as a non-numeric string, so the method never throws.return default(TOutput): Provides a safe fallback value, here0forint, when the conversion fails.
Exercise 16: Covariant Logger (Out Keyword)
Practice Problem: Create a covariant interface ILogger<out T> and implement it. Demonstrate that an ILogger<Dog> can be assigned to an ILogger<Animal>.
Purpose: This exercise helps you practice the out variance modifier, which allows a generic interface to be treated as more general when its type parameter only ever appears in output positions, such as return types.
Given Input: ILogger<Dog> dogLogger = new DogLogger(); ILogger<Animal> animalLogger = dogLogger;
Expected Output: Logged animal says: Woof!
▼ Hint
- Mark the type parameter as
out, meaningTmay only appear in return positions, never as a method argument. - Give the interface a method that returns
T, such asT GetLastLogged(). - Implement the interface for a specific, more derived type like
Dog. - Assign the
ILogger<Dog>instance directly to anILogger<Animal>variable to show covariance in action.
▼ Solution & Explanation
Explanation:
ILogger<out T>: Theoutkeyword tells the compilerTis only ever produced, never consumed, which is what makes covariance safe.ILogger<Animal> animalLogger = dogLogger: Legal because a logger that only ever hands outDogobjects can safely be treated as one that hands outAnimalobjects.animalLogger.GetLastLogged(): Returns anAnimalreference even though the concrete object underneath is still aDog.animal.Speak(): Calls the overridden method, so the actualDogbehavior runs through virtual dispatch.
Exercise 17: Contravariant Consumer (In Keyword)
Practice Problem: Create a contravariant interface IConsumer<in T> with a method void Consume(T item). Demonstrate passing an IConsumer<Animal> to something expecting an IConsumer<Dog>.
Purpose: This exercise helps you practice the in variance modifier, which allows a generic interface to be treated as more specific when its type parameter only ever appears in input positions, such as method arguments.
Given Input: IConsumer<Animal> animalConsumer = new AnimalConsumer(); IConsumer<Dog> dogConsumer = animalConsumer;
Expected Output: Consumed animal: Rex
▼ Hint
- Mark the type parameter as
in, meaningTmay only appear in argument positions, never as a return type. - Give the interface a method that accepts
T, such asvoid Consume(T item). - Implement the interface for a more general type like
Animal. - Assign the
IConsumer<Animal>instance to anIConsumer<Dog>variable to show contravariance in action.
▼ Solution & Explanation
Explanation:
IConsumer<in T>: Theinkeyword tells the compilerTis only ever accepted as input, which is what makes contravariance safe.IConsumer<Dog> dogConsumer = animalConsumer: Legal because a consumer that knows how to handle anyAnimalcan also safely handle a more specificDog.dogConsumer.Consume(dog): Passes aDoginto a method that was really written to acceptAnimal, which works becauseDogis anAnimal.item.Name: Reads a member defined on the baseAnimalclass, so it works regardless of which derived type was actually passed in.
Exercise 18: Generic Event Broker
Practice Problem: Build a simple generic event publisher and subscriber pattern where components can subscribe to a specific event type, MessageEvent<T>.
Purpose: This exercise helps you practice combining generics with delegates and events, showing how a single generic type can broadcast strongly typed messages to any number of subscribers.
Given Input: MessageEvent<string> chatEvent = new MessageEvent<string>(); chatEvent.Publish("Server is going down for maintenance.");
Expected Output:
Subscriber A received: Server is going down for maintenance. Subscriber B received: Server is going down for maintenance.
▼ Hint
- Declare a public
event Action<T>field onMessageEvent<T>. - Add a
Publish(T message)method that invokes the event, checking for null subscribers first. - Subscribe multiple lambda expressions to the event using
+=before publishing.
▼ Solution & Explanation
Explanation:
event Action<T> OnMessage: Declares a strongly typed event, so subscribers only ever receive messages of typeT.OnMessage?.Invoke(message): Safely raises the event, doing nothing if no subscribers have signed up yet.chatEvent.OnMessage += ...: Attaches a lambda expression as a subscriber, and multiple subscribers can be attached the same way.chatEvent.Publish(...): Broadcasts a single message to every currently attached subscriber in the order they were added.
Exercise 19: Custom LINQ Filter
Practice Problem: Write an extension method Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate) that mimics the behavior of LINQ’s .Where() without actually using LINQ.
Purpose: This exercise helps you practice writing generic extension methods, using yield return for lazy evaluation, and understanding how a method like .Where() works under the hood.
Given Input: List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; numbers.Filter(n => n % 2 == 0);
Expected Output:
2 4 6
▼ Hint
- Make the method static, inside a static class, with
this IEnumerable<T> sourceas the first parameter. - Loop through
sourcewith aforeachloop. - Use
yield return itemwhenever the predicate returns true for the current item.
▼ Solution & Explanation
Explanation:
this IEnumerable<T> source: Thethiskeyword on the first parameter turns the method into an extension method callable directly on anyIEnumerable<T>.Func<T, bool> predicate: Accepts the filtering rule as a delegate, exactly like the real.Where()does.yield return item: Lazily produces matching items one at a time instead of building a full list up front.numbers.Filter(n => n % 2 == 0): Calls the extension method as if it were a built-in member of the list.
Exercise 20: Generic Spec Evaluation
Practice Problem: Implement the Specification Pattern using generics. Create an ISpecification<T> interface with an IsSatisfiedBy(T entity) method, then create an AndSpecification<T> that combines two specs together.
Purpose: This exercise helps you practice composing generic behavior out of smaller generic pieces, where one specification wraps two others and reuses their logic instead of duplicating it.
Given Input: ISpecification<int> adultSpec = new AndSpecification<int>(new MinAgeSpecification(18), new MaxAgeSpecification(65));
Expected Output:
Age 30 satisfies: True Age 15 satisfies: False
▼ Hint
- Define
ISpecification<T>with a singlebool IsSatisfiedBy(T entity)method. - Have
AndSpecification<T>implement the same interface and store two otherISpecification<T>instances internally. - Its
IsSatisfiedByshould return true only when both stored specifications return true. - Write a couple of small concrete specifications, such as a minimum age and maximum age check, to combine together.
▼ Solution & Explanation
Explanation:
ISpecification<T>: Defines a single reusable contract for “does this entity satisfy some rule”.AndSpecification<T>: Wraps two other specifications and combines their results with a logical AND, without knowing what those rules actually check._first.IsSatisfiedBy(entity) && _second.IsSatisfiedBy(entity): Only returns true when both wrapped specifications independently agree.MinAgeSpecificationandMaxAgeSpecification: Small, focused rules that can be composed together instead of writing one large combined check.

Leave a Reply