C# Generics
Generics provide a way to create programming elements that operate on a variety of types rather than a single, fixed type. Think of them as placeholders for types that are specified when the generic type is used.
What are Generics?
Before generics, if you wanted to create a collection that could hold any type of object, you would typically use the base object type. This worked, but it had drawbacks:
- Performance Overhead: Storing objects as
objectrequired boxing (converting value types to reference types) and unboxing (converting reference types back to value types), which could impact performance. - Type Safety Issues: You couldn't guarantee the type of object retrieved from the collection at compile time. You had to cast it explicitly, and if the cast was incorrect, it would result in a runtime error (
InvalidCastException).
Generics solve these problems by allowing you to specify the type parameter at the time you declare or instantiate a generic type.
Generic Classes
A generic class is a class that is parameterized by one or more type parameters. The most common example is the generic List<T> class in the .NET Framework.
Defining a Generic Class
Here's a simple generic class for a stack:
public class GenericStack<T>
{
private T[] _stack;
private int _top;
public GenericStack(int capacity = 10)
{
_stack = new T[capacity];
_top = -1;
}
public void Push(T item)
{
if (_top == _stack.Length - 1)
{
// Handle stack overflow, e.g., resize array
throw new StackOverflowException("Stack is full.");
}
_stack[++_top] = item;
}
public T Pop()
{
if (_top < 0)
{
throw new InvalidOperationException("Stack is empty.");
}
T item = _stack[_top--];
_stack[_top + 1] = default(T); // Optional: Clear reference
return item;
}
public T Peek()
{
if (_top < 0)
{
throw new InvalidOperationException("Stack is empty.");
}
return _stack[_top];
}
public bool IsEmpty => _top == -1;
public int Count => _top + 1;
}
Using a Generic Class
You specify the type parameter when you create an instance of the generic class:
// Stack for integers
GenericStack<int> intStack = new GenericStack<int>(5);
intStack.Push(10);
intStack.Push(20);
int poppedInt = intStack.Pop(); // poppedInt will be 20
// Stack for strings
GenericStack<string> stringStack = new GenericStack<string>();
stringStack.Push("Hello");
stringStack.Push("World");
string poppedString = stringStack.Pop(); // poppedString will be "World"
// This would cause a compile-time error:
// intStack.Push("This is not an int");
Generic Interfaces
Generic interfaces are interfaces that are parameterized by one or more type parameters.
Defining a Generic Interface
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
IEnumerable<T> GetAll();
void Update(T item);
void Delete(int id);
}
Implementing a Generic Interface
public class CustomerRepository : IRepository<Customer>
{
// ... implementation details for Customer
public void Add(Customer item) { /* ... */ }
public Customer GetById(int id) { /* ... */ return null; }
public IEnumerable<Customer> GetAll() { /* ... */ return new List<Customer>(); }
public void Update(Customer item) { /* ... */ }
public void Delete(int id) { /* ... */ }
}
Generic Methods
You can also define generic methods, which are methods that are parameterized by one or more type parameters.
Defining a Generic Method
public static class UtilityHelper
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static T Copy<T>(T source) where T : ICloneable
{
return (T)source.Clone();
}
}
Using a Generic Method
int x = 5;
int y = 10;
UtilityHelper.Swap(ref x, ref y); // x is now 10, y is now 5
string s1 = "apple";
string s2 = "banana";
UtilityHelper.Swap(ref s1, ref s2); // s1 is now "banana", s2 is now "apple"
// Using the Copy method (assuming Customer implements ICloneable)
// Customer originalCustomer = GetCustomer();
// Customer copiedCustomer = UtilityHelper.Copy(originalCustomer);
Constraints
Constraints are used to restrict the types that can be used as type arguments for a generic type or method. This allows you to use certain operations on the type parameters that might not be available for arbitrary types.
Common constraints include:
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 public parameterless constructor.where T : BaseClassName: T must be or derive fromBaseClassName.where T : InterfaceName: T must implementInterfaceName.- Multiple constraints can be specified, including base class and interfaces.
new() constraint must be specified last.
Benefits of Generics
- Type Safety: Compile-time checking prevents runtime type errors.
- Performance: Avoids boxing and unboxing overhead for value types.
- Code Reusability: Write algorithms and data structures once and use them with various types.
- Readability: Code becomes more expressive and easier to understand.
Common Generic Collections
The .NET Framework provides a rich set of generic collections in the System.Collections.Generic namespace, such as:
List<T>Dictionary<TKey, TValue>HashSet<T>Queue<T>Stack<T>LinkedList<T>
Familiarizing yourself with these collections is crucial for efficient data management in C#.