C# - Generics
C# - Generics
Section titled “C# - Generics”What are Generics in C#?
Section titled “What are Generics in C#?”Generics allow you to design and write classes, methods, and interfaces that are type-safe yet flexible. They let you define a placeholder for a data type (a ‘type parameter’), which is then specified when the code is used. This allows you to write a single piece of code that can work with any data type, maximizing code reuse, performance, and type safety.
The most common example is generic collections. Instead of creating a separate IntList, StringList, and ProductList, you can use the generic List<T>, where T is the type parameter you provide: List<int>, List<string>, List<Product>.
Generic Classes
Section titled “Generic Classes”A generic class is defined with a type parameter in angle brackets (<>). This allows the class to be instantiated for different data types while ensuring that all operations within the class are type-safe.
Example: A Simple Generic ‘Wrapper’ Class
Section titled “Example: A Simple Generic ‘Wrapper’ Class”Let’s create a simple class that can ‘wrap’ a value of any type.
using System;
// A generic class with one type parameter, Tpublic class DataWrapper<T>{ // The internal data is of type T public T Data { get; private set; }
public DataWrapper(T data) { this.Data = data; }
public void DisplayData() { Console.WriteLine($"Data is: {Data} (Type: {typeof(T).Name})"); }}
// --- Usage ---// Create a wrapper for an integervar intWrapper = new DataWrapper<int>(123);intWrapper.DisplayData(); // Output: Data is: 123 (Type: Int32)
// Create a wrapper for a stringvar stringWrapper = new DataWrapper<string>("Hello, Generics!");stringWrapper.DisplayData(); // Output: Data is: Hello, Generics! (Type: String)Best Practice: Use Built-in Generic Collections
Section titled “Best Practice: Use Built-in Generic Collections”While creating your own generic classes is powerful, you should almost always prefer the highly optimized, built-in generic collections in the System.Collections.Generic namespace. These include:
List<T>: The most common, a dynamically-sized list.Dictionary<TKey, TValue>: A collection of key-value pairs.Queue<T>: A first-in, first-out (FIFO) collection.Stack<T>: A last-in, first-out (LIFO) collection.HashSet<T>: A collection of unique elements.
Generic Methods
Section titled “Generic Methods”You can also create methods that are generic, even if they are inside a non-generic class. This is useful for utility functions that perform an operation on any type.
Example: A Generic Swap Method
Section titled “Example: A Generic Swap Method”This classic example shows a method that can swap two variables of any type.
public class Utility{ public static void Swap<T>(ref T a, ref T b) { T temp = a; a = b; b = temp; }}
// --- Usage ---int x = 10, y = 20;Console.WriteLine($"Before: x={x}, y={y}");Utility.Swap(ref x, ref y); // Type T is inferred as intConsole.WriteLine($"After: x={x}, y={y}");
string s1 = "Hello", s2 = "World";Console.WriteLine($"Before: s1={s1}, s2={s2}");Utility.Swap(ref s1, ref s2); // Type T is inferred as stringConsole.WriteLine($"After: s1={s1}, s2={s2}");Generic Delegates (Func and Action)
Section titled “Generic Delegates (Func and Action)”Before .NET introduced built-in generic delegates, you had to define a new delegate for every different method signature. Now, you can use Func<...> and Action<...> for nearly all cases.
Action<T>: Represents a method that takes one or more parameters (up to 16) and returnsvoid.Func<T, TResult>: Represents a method that takes one or more parameters and returns a value of typeTResult(the last type parameter is always the return type).
Example: Using Func
Section titled “Example: Using Func”// This Func delegate represents any method that takes two ints and returns an int.Func<int, int, int> operation;
// Point it to an 'Add' methodoperation = (a, b) => a + b; // Using a lambda expressionConsole.WriteLine($"Sum: {operation(10, 5)}"); // Output: Sum: 15
// Point it to a 'Multiply' methodoperation = (a, b) => a * b;Console.WriteLine($"Product: {operation(10, 5)}"); // Output: Product: 50Generic Interfaces
Section titled “Generic Interfaces”Interfaces can also be generic. This is extremely common in modern software design, for patterns like the Repository Pattern.
Example: A Generic Repository Interface
Section titled “Example: A Generic Repository Interface”This interface defines a contract for a data repository that can work with any entity type.
// The generic interfacepublic interface IRepository<T>{ T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity);}
// A concrete implementation for a 'Product' classpublic class ProductRepository : IRepository<Product>{ private readonly List<Product> _products = new();
public void Add(Product entity) => _products.Add(entity); public IEnumerable<Product> GetAll() => _products; public Product GetById(int id) => _products.FirstOrDefault(p => p.Id == id);}
// (Assuming a Product class exists)public record Product(int Id, string Name);Generic Constraints
Section titled “Generic Constraints”Sometimes you need to guarantee that the type parameter T has certain capabilities (e.g., it has a parameterless constructor or implements a specific interface). You can enforce this using constraints with the where keyword.
// This method will only accept types T that are classes and have a parameterless constructor.public T CreateInstance<T>() where T : class, new(){ return new T();}
// This method requires T to implement the IDisposable interface.public void ProcessAndDispose<T>(T item) where T : IDisposable{ // Now we can safely call Dispose() because the constraint guarantees it exists. item.Dispose();}