Generics in C# are a powerful feature that lets you design classes, interfaces, and methods with a placeholder for the type of data they store or operate on. Instead of specifying a specific data type, generics allow you to use a type parameter as a placeholder, which you can replace with any type at runtime. This flexibility promotes code reuse, type safety, and performance.
Generics in C# allow you to define classes, interfaces, and methods with a type placeholder. By doing so, you don’t have to commit to a specific data type and can instead substitute any type at runtime. This is useful when creating classes and methods that are meant to work across different data types.
For instance, rather than creating multiple versions of a class for different data types, a single generic class can handle all data types.
Using generics in C# offers several benefits:
A generic class allows you to define a class with one or more type parameters. These type parameters act as placeholders that can be replaced with any data type when an instance of the class is created.
The basic syntax for defining a generic class looks like this:
public class GenericClass<T>
{
private T data;
public GenericClass(T value)
{
data = value;
}
public void Display()
{
Console.WriteLine($"Stored value: {data}");
}
}
Here is an example of a simple generic container class.
public class Container<T>
{
private T item;
public Container(T item)
{
this.item = item;
}
public T GetItem()
{
return item;
}
}
You can now create Container
instances for any data type:
Container<int> intContainer = new Container<int>(42);
Container<string> stringContainer = new Container<string>("Hello, Generics!");
Console.WriteLine(intContainer.GetItem()); // Output: 42
Console.WriteLine(stringContainer.GetItem()); // Output: Hello, Generics!
In this example, Container<int>
is used to store an integer, while Container<string>
is used for a string. This flexibility is what makes generics so valuable.
Generic methods work similarly to generic classes, allowing you to define methods with type parameters. Generic methods can be defined within both generic and non-generic classes.
Here’s the basic syntax for a generic method:
public void GenericMethod<T>(T parameter)
{
Console.WriteLine($"Parameter type: {typeof(T)}, Value: {parameter}");
}
Consider this example of a method that swaps two values, regardless of their type:
public class Utilities
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
Usage
You can now use this method to swap integers, strings, or any other types:
int x = 5, y = 10;
Utilities.Swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}"); // Output: x: 10, y: 5
string str1 = "Hello", str2 = "World";
Utilities.Swap(ref str1, ref str2);
Console.WriteLine($"str1: {str1}, str2: {str2}"); // Output: str1: World, str2: Hello
Generic constraints allow you to limit the types that can be used with a generic class or method. Constraints make generics more flexible by restricting the types to only those that meet specific requirements.
where T : struct
- T must be a value type.where T : class
- T must be a reference type.where T : new()
- T must have a parameterless constructor.where T : <base class>
- T must inherit from a specific base class.where T : <interface>
- T must implement a specific interface.Here’s a generic class that works only with objects that implement IDisposable
.
public class ResourceHandler<T> where T : IDisposable
{
private T resource;
public ResourceHandler(T resource)
{
this.resource = resource;
}
public void DisposeResource()
{
resource.Dispose();
}
}
A common use case for generics in real-world applications is creating a repository pattern in a data-driven application. A generic repository can handle operations for any data type, such as adding, deleting, and retrieving records from a database.
Consider a repository class for managing different entities in an application.
public interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
IEnumerable<T> GetAll();
}
public class Repository<T> : IRepository<T> where T : class
{
private List<T> entities = new List<T>();
public void Add(T entity)
{
entities.Add(entity);
}
public void Delete(T entity)
{
entities.Remove(entity);
}
public IEnumerable<T> GetAll()
{
return entities;
}
}
Suppose we have Customer
and Order
classes. Using the generic repository, we can create repositories for each type.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public string ProductName { get; set; }
}
public class Program
{
public static void Main()
{
Repository<Customer> customerRepo = new Repository<Customer>();
customerRepo.Add(new Customer { Id = 1, Name = "John Doe" });
Repository<Order> orderRepo = new Repository<Order>();
orderRepo.Add(new Order { OrderId = 101, ProductName = "Laptop" });
foreach (var customer in customerRepo.GetAll())
{
Console.WriteLine($"Customer ID: {customer.Id}, Name: {customer.Name}");
}
foreach (var order in orderRepo.GetAll())
{
Console.WriteLine($"Order ID: {order.OrderId}, Product: {order.ProductName}");
}
}
}
This generic repository can now manage any type of entity (e.g., Customer
, Order
) without rewriting separate repository classes for each type.
Generics in C# allow for the creation of reusable, type-safe, and performance-optimized classes and methods that can work with any data type. With type safety and flexibility, generics help streamline code and support a wide variety of use cases, especially in applications involving collections, repositories, and other data structures. Adopting generics is a best practice for writing cleaner, more efficient, and adaptable code, enabling you to build robust, modular applications that are easy to extend and maintain.