On This Page
Introduction to Generics
Generics introduce the concept of type parameters to .NET, allowing you to design classes, interfaces, and methods that operate on elements of a specified type. This enables you to reuse code more effectively, create more robust and type-safe collections, and reduce the need for casting.
Before generics, collections like ArrayList
stored elements as object
. This required explicit casting when retrieving elements, which could lead to runtime errors if the cast was incorrect. Generics solve this by allowing you to define collections that hold a specific type, like List<int>
or List<string>
.
Without Generics (Type Safety Issues):
System.Collections.ArrayList myList = new System.Collections.ArrayList();
myList.Add(10);
myList.Add("hello"); // No compile-time error, but a runtime error awaits!
int firstItem = (int)myList[0]; // Works
// string secondItem = (string)myList[1]; // This would throw an InvalidCastException at runtime if myList[1] was an int.
Benefits of Generics
- Type Safety: Generics provide compile-time type checking. If you try to add an incompatible type to a generic collection, you'll get a compile-time error, preventing runtime issues.
- Code Reusability: You can write a single generic class or method that can be used with any data type, eliminating the need to write multiple versions of the same logic for different types.
- Performance: Generic collections avoid boxing and unboxing of value types, leading to improved performance compared to non-generic collections that store elements as
object
. - Readability: Code becomes cleaner and more expressive when the intent of data types is explicitly stated through generics.
Generic Type Syntax
Generic types are declared using angle brackets (<>
) to specify one or more type parameters. The type parameter is a placeholder for an actual type that will be supplied when the generic type is instantiated.
For example, the generic `List<T>` class in C# uses `T` as its type parameter. When you create a list of integers, you instantiate it as List<int>
, where int
replaces the T
placeholder.
Key elements:
- Type Parameter: Usually represented by a single uppercase letter, commonly `T`.
- Generic Type Declaration: `ClassName<T1, T2, ...>`
- Generic Type Instantiation: `new ClassName<ActualType1, ActualType2, ...>()`
Generic Classes
A generic class is a class that is parameterized by one or more types. This allows you to define a class structure that can work with any data type without sacrificing type safety.
Consider a generic `Box<T>` class that can hold any type of item:
public class Box<T>
{
private T _content;
public Box(T content)
{
_content = content;
}
public T GetContent()
{
return _content;
}
public void SetContent(T newContent)
{
_content = newContent;
}
}
Using the generic `Box` class:
Box<int> intBox = new Box<int>(123);
int value = intBox.GetContent(); // value is 123
Box<string> stringBox = new Box<string>("Hello Generics");
string message = stringBox.GetContent(); // message is "Hello Generics"
// intBox.SetContent("This is wrong!"); // Compile-time error: Cannot convert from string to int.
Generic Interfaces
Generic interfaces define a contract that classes can implement. Similar to generic classes, they use type parameters to specify the types they operate on.
The .NET Framework provides many generic interfaces, such as IEnumerable<T>
, ICollection<T>
, and IDictionary<TKey, TValue>
.
Example of a custom generic interface:
public interface IRepository<T> where T : class
{
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
An implementation of this interface:
public class UserRepository : IRepository<User>
{
// ... implementation details ...
public User GetById(int id) { /* ... */ return null; }
public void Add(User user) { /* ... */ }
public void Update(User user) { /* ... */ }
public void Delete(int id) { /* ... */ }
}
Generic Methods
Generic methods are methods that are parameterized by one or more types. They can be defined within non-generic classes or generic classes.
This allows you to create standalone methods that can operate on different types without requiring the entire class to be generic.
Example of a generic static method to swap two values:
public static class Swapper
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
Using the generic `Swap` method:
int x = 5, y = 10;
Swapper.Swap(ref x, ref y); // x is now 10, y is now 5
string s1 = "hello", s2 = "world";
Swapper.Swap(ref s1, ref s2); // s1 is now "world", s2 is now "hello"
Constraints on Generic Types
Constraints are used to enforce rules on the types that can be used as type arguments for a generic type parameter. They limit the kinds of types that a client can supply.
Common constraints include:
where T : struct
: `T` must be a value type (excluding nullable value types).where T : class
: `T` must be a reference type.where T : new()
: `T` must have a public, parameterless constructor.where T : BaseClassName
: `T` must be or derive from `BaseClassName`.where T : InterfaceName
: `T` must implement `InterfaceName`.
A generic class with constraints:
public class DataProcessor<T> where T : class, new()
{
public void Process(T data)
{
// T must be a reference type and have a parameterless constructor
T defaultInstance = new T();
// ... process data ...
}
}
In this example, `DataProcessor<string>` is valid, but `DataProcessor<int>` would result in a compile-time error because `int` is a value type and does not satisfy the `class` constraint.
Covariance and Contravariance
Covariance and contravariance are advanced concepts in generics that deal with how generic types can be substituted for one another.
- Covariance: Allows you to use a more derived type than originally specified. It applies to output positions (e.g., return types of methods, read-only properties). Marked with the
out
keyword. - Contravariance: Allows you to use a less derived type than originally specified. It applies to input positions (e.g., method parameters). Marked with the
in
keyword.
Covariance Example:
If you have a generic interface `IEnumerable<out T>`, you can assign an `IEnumerable<string>` to an `IEnumerable<object>` because `string` is derived from `object`.
IEnumerable<string> strings = new List<string>() { "a", "b" };
IEnumerable<object> objects = strings; // Covariance allows this.
Contravariance Example:
If you have a generic delegate `Action<in T>`, you can assign an `Action<object>` to an `Action<string>` because a method that accepts `object` can also accept a `string`.
Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // Contravariance allows this.
Practical Uses
Generics are fundamental to modern C# development. Some common practical uses include:
- Collections:
List<T>
,Dictionary<TKey, TValue>
,HashSet<T>
,Queue<T>
,Stack<T>
are the go-to for efficient and type-safe data storage. - Asynchronous Programming:
Task<TResult>
represents an asynchronous operation that returns a result of type `TResult`. - LINQ: Language Integrated Query heavily relies on generics, particularly
IEnumerable<T>
andIQueryable<T>
. - Dependency Injection: Generic interfaces are often used to register and resolve services.
- Custom Data Structures: Building your own linked lists, trees, or other data structures that can be parameterized by type.
Embracing generics leads to more robust, maintainable, and performant C# applications.