C# Generics
$count++; if($count == 1) { include "../mobilemenu.php"; } if ($count == 2) { include "../sharemediasubfolder.php"; } ?>
Generics are a powerful feature in C# that allow developers to define classes, methods, interfaces, and delegates with placeholders for the type of data they store and manipulate. By leveraging generics, you can create highly reusable, type-safe, and efficient code. This comprehensive guide explores every aspect of generics in C#, including declaration, usage, constraints, variance, performance considerations, best practices, common mistakes, and real-world examples.
Table of Contents
- Introduction to Generics - What are Generics?
- Generic Types - Declaring Generic Classes
- Generic Methods - Declaring Generic Methods
- Generic Delegates - Declaring Generic Delegates
- Generic Constraints - where T : class
- Variance in Generics - Covariance
- Generic Collections - List<T>, Dictionary<TKey, TValue>, etc.
- Advanced Topics - Recursive Generics
- Performance Considerations - Avoiding Boxing and Unboxing
- Best Practices for Generics
- Common Mistakes with Generics
- Real-World Example
- Summary
- Benefits of Using Generics
- Generic Interfaces
- Generic Structs
- Type Inference
- Using Generic Delegates
- where T : struct
- where T : new()
- where T : BaseClass
- where T : Interface
- Multiple Constraints
- Contravariance
- Invariance
- Comparison with Non-Generic Collections
- Generic Constraints with Multiple Interfaces
- Reflection with Generics
- Nullable Reference Types with Generics
- Generic Covariance and Contravariance in Delegates and Interfaces
- Code Reuse and Type Safety
1. Introduction to Generics
What are Generics?
Generics allow you to define classes, methods, interfaces, and delegates with placeholders for the type of data they store and manipulate. This enables type-safe data structures without committing to actual data types in advance.Basic Syntax:
// Generic Class
public class GenericClass<T>
{
public T Data { get; set; }
public void Display()
{
Console.WriteLine($"Data: {Data}");
}
}
// Generic Method
public void GenericMethod<T>(T param)
{
Console.WriteLine($"Parameter: {param}");
}
Benefits of Using Generics
- Type Safety: Compile-time type checking prevents runtime errors.- Performance: Eliminates the need for boxing/unboxing and type casting.
- Code Reuse: Create versatile and reusable components.
- Readability: Clearer and more maintainable code by specifying intended types.
2. Generic Types
Generics can be applied to various types in C#, including classes, interfaces, and structs.2.1 Declaring Generic Classes
Example: Generic Repository Classusing System;
using System.Collections.Generic;
public class Repository<T>
{
private List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
Console.WriteLine($"{item} added to repository.");
}
public T Get(int index)
{
if(index >= 0 && index < _items.Count)
{
return _items[index];
}
throw new IndexOutOfRangeException("Invalid index.");
}
public void DisplayAll()
{
Console.WriteLine("Repository Contents:");
foreach(var item in _items)
{
Console.WriteLine(item);
}
}
}
class Program
{
static void Main()
{
Repository<string> stringRepo = new Repository<string>();
stringRepo.Add("Apple");
stringRepo.Add("Banana");
stringRepo.DisplayAll();
Console.WriteLine($"Item at index 1: {stringRepo.Get(1)}"); // Output: Banana
Repository<int> intRepo = new Repository<int>();
intRepo.Add(100);
intRepo.Add(200);
intRepo.DisplayAll();
Console.WriteLine($"Item at index 0: {intRepo.Get(0)}"); // Output: 100
}
}
Sample Output:
Apple added to repository.
Banana added to repository.
Repository Contents:
Apple
Banana
Item at index 1: Banana
100 added to repository.
200 added to repository.
Repository Contents:
100
200
Item at index 0: 100
- Generic Class Declaration: `Repository<T>` can store items of any type `T`.
- Type Safety: Ensures only specified types are stored, preventing type mismatches.
- Reusability: The same repository can be used for different data types without code duplication.
2.2 Generic Interfaces
Example: Generic Comparable Interfaceusing System;
public interface IComparable<T>
{
int CompareTo(T other);
}
public class Product : IComparable<Product>
{
public string Name { get; set; }
public decimal Price { get; set; }
public int CompareTo(Product other)
{
if(other == null) return 1;
return this.Price.CompareTo(other.Price);
}
public override string ToString()
{
return $"{Name}: {Price:C}";
}
}
class Program
{
static void Main()
{
Product p1 = new Product { Name = "Laptop", Price = 1500m };
Product p2 = new Product { Name = "Smartphone", Price = 800m };
Console.WriteLine($"Comparing {p1.Name} to {p2.Name}: {p1.CompareTo(p2)}"); // Output: 1
Console.WriteLine($"Comparing {p2.Name} to {p1.Name}: {p2.CompareTo(p1)}"); // Output: -1
}
}
Sample Output:
Comparing Laptop to Smartphone: 1
Comparing Smartphone to Laptop: -1
- Generic Interface Implementation: `Product` implements `IComparable<Product>`, enabling comparison between `Product` instances.
- Type Safety: The comparison method `CompareTo` ensures that only `Product` types are compared, preventing invalid operations.
2.3 Generic Structs
Example: Generic Pair Structusing System;
public struct Pair<T1, T2>
{
public T1 First { get; }
public T2 Second { get; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
public override string ToString()
{
return $"Pair: ({First}, {Second})";
}
}
class Program
{
static void Main()
{
Pair<int, string> idNamePair = new Pair<int, string>(1, "Alice");
Console.WriteLine(idNamePair); // Output: Pair: (1, Alice)
Pair<string, double> productPricePair = new Pair<string, double>("Book", 29.99);
Console.WriteLine(productPricePair); // Output: Pair: (Book, 29.99)
}
}
Sample Output:
Pair: (1, Alice)
Pair: (Book, 29.99)
- Generic Struct Declaration: `Pair<T1, T2>` can hold two related values of different types.
- Type Safety and Flexibility: Ensures that only specified types are used, while allowing diverse type combinations.
3. Generic Methods
Generics aren't limited to types; methods can also be generic, allowing them to operate on different data types while maintaining type safety.3.1 Declaring Generic Methods
Example: Generic Swap Methodusing System;
public class Utility
{
public void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
class Program
{
static void Main()
{
Utility util = new Utility();
int x = 10, y = 20;
Console.WriteLine($"Before Swap: x = {x}, y = {y}"); // Output: x = 10, y = 20
util.Swap<int>(ref x, ref y);
Console.WriteLine($"After Swap: x = {x}, y = {y}"); // Output: x = 20, y = 10
string str1 = "Hello", str2 = "World";
Console.WriteLine($"Before Swap: str1 = {str1}, str2 = {str2}"); // Output: str1 = Hello, str2 = World
util.Swap<string>(ref str1, ref str2);
Console.WriteLine($"After Swap: str1 = {str1}, str2 = {str2}"); // Output: str1 = World, str2 = Hello
}
}
Sample Output:
Before Swap: x = 10, y = 20
After Swap: x = 20, y = 10
Before Swap: str1 = Hello, str2 = World
After Swap: str1 = World, str2 = Hello
- Generic Method Declaration: `Swap<T>` can swap values of any type `T`.
- Type Inference: The compiler infers the type based on the arguments, so explicit type parameters can be omitted.
3.2 Type Inference
C# allows the compiler to infer the type arguments of a generic method based on the method's parameters, reducing the need for explicit type specification.Example: Generic Method with Type Inference
using System;
public class Utility
{
public void DisplayType<T>(T item)
{
Console.WriteLine($"Type of item: {typeof(T)}");
}
}
class Program
{
static void Main()
{
Utility util = new Utility();
util.DisplayType(123); // Output: Type of item: System.Int32
util.DisplayType("Generic"); // Output: Type of item: System.String
util.DisplayType(45.67); // Output: Type of item: System.Double
}
}
Sample Output:
Type of item: System.Int32
Type of item: System.String
Type of item: System.Double
- Type Inference: The compiler automatically determines the type `T` based on the argument passed to `DisplayType`.
- Simplified Syntax: No need to specify `<int>`, `<string>`, etc., when calling the method.
4. Generic Delegates
Delegates can also be generic, allowing them to work with various data types while maintaining type safety.4.1 Declaring Generic Delegates
Example: Generic Func DelegateC# provides predefined generic delegates like `Func<T, TResult>` and `Action<T>`. Here's how to declare and use a generic delegate.
using System;
public delegate TResult MyFunc<T, TResult>(T arg);
class Program
{
static void Main()
{
MyFunc<int, string> intToString = num => $"Number is {num}";
Console.WriteLine(intToString(5)); // Output: Number is 5
MyFunc<string, int> stringLength = str => str.Length;
Console.WriteLine($"Length of 'Generics': {stringLength("Generics")}"); // Output: Length of 'Generics': 8
}
}
Sample Output:
Number is 5
Length of 'Generics': 8
- Generic Delegate Declaration: `MyFunc<T, TResult>` takes an argument of type `T` and returns a result of type `TResult`.
- Usage: Assigns lambda expressions that match the delegate signature to perform specific operations.
4.2 Using Generic Delegates
Example: Using Func and ActionC#'s built-in generic delegates `Func` and `Action` are widely used for functional programming patterns.
using System;
class Program
{
static void Main()
{
// Func delegate: takes two integers and returns their sum
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine($"Sum: {add(10, 20)}"); // Output: Sum: 30
// Action delegate: takes a string and prints it
Action<string> print = message => Console.WriteLine($"Message: {message}");
print("Hello, Generics!"); // Output: Message: Hello, Generics!
}
}
Sample Output:
Sum: 30
Message: Hello, Generics!
- Func Delegate: Represents a method that takes two `int` parameters and returns an `int`.
- Action Delegate: Represents a method that takes a `string` parameter and returns void.
5. Generic Constraints
Constraints limit the types that can be used as arguments for type parameters in generic classes or methods, ensuring that the type arguments meet certain requirements.5.1 where T : class
Constrains the type parameter `T` to be a reference type.Example: Generic Repository with Class Constraint
using System;
using System.Collections.Generic;
public class Repository<T> where T : class
{
private List<T> _items = new List<T>();
public void Add(T item)
{
if(item == null)
throw new ArgumentNullException(nameof(item));
_items.Add(item);
Console.WriteLine($"{item} added to repository.");
}
public T Get(int index)
{
if(index >= 0 && index < _items.Count)
{
return _items[index];
}
throw new IndexOutOfRangeException("Invalid index.");
}
public void DisplayAll()
{
Console.WriteLine("Repository Contents:");
foreach(var item in _items)
{
Console.WriteLine(item);
}
}
}
class Program
{
static void Main()
{
Repository<string> stringRepo = new Repository<string>();
stringRepo.Add("Apple");
stringRepo.Add("Banana");
stringRepo.DisplayAll();
}
}
Sample Output:
Apple added to repository.
Banana added to repository.
Repository Contents:
Apple
Banana
- Class Constraint: Ensures that `T` is a reference type, allowing null checks and certain operations.
- Null Check: Prevents adding `null` items to the repository.
5.2 where T : struct
Constrains the type parameter `T` to be a value type.Example: Generic Pair Struct with Struct Constraint
using System;
public struct Pair<T1, T2> where T1 : struct where T2 : struct
{
public T1 First { get; }
public T2 Second { get; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
public override string ToString()
{
return $"Pair: ({First}, {Second})";
}
}
class Program
{
static void Main()
{
Pair<int, double> numericPair = new Pair<int, double>(10, 20.5);
Console.WriteLine(numericPair); // Output: Pair: (10, 20.5)
}
}
Sample Output:
Pair: (10, 20.5)
- Struct Constraint: Ensures that both `T1` and `T2` are value types, enabling efficient memory usage and avoiding null references.
5.3 where T : new()
Constrains the type parameter `T` to have a public parameterless constructor.Example: Generic Factory Method
using System;
public class Factory<T> where T : new()
{
public T CreateInstance()
{
return new T();
}
}
public class Product
{
public string Name { get; set; }
public Product()
{
Name = "Default Product";
}
public override string ToString()
{
return $"Product Name: {Name}";
}
}
class Program
{
static void Main()
{
Factory<Product> productFactory = new Factory<Product>();
Product p = productFactory.CreateInstance();
Console.WriteLine(p); // Output: Product Name: Default Product
}
}
Sample Output:
Product Name: Default Product
- new() Constraint: Allows the factory to instantiate `T` using the `new` keyword.
- Usage: Ensures that `T` has a parameterless constructor required for object creation.
5.4 where T : BaseClass
Constrains the type parameter `T` to inherit from a specific base class.Example: Generic Manager with Base Class Constraint
using System;
using System.Collections.Generic;
public class Employee
{
public string Name { get; set; }
}
public class Manager : Employee
{
public int TeamSize { get; set; }
}
public class ManagerRepository<T> where T : Manager
{
private List<T> _managers = new List<T>();
public void AddManager(T manager)
{
_managers.Add(manager);
Console.WriteLine($"Manager {manager.Name} with team size {manager.TeamSize} added.");
}
public void DisplayManagers()
{
Console.WriteLine("Managers:");
foreach(var mgr in _managers)
{
Console.WriteLine($"{mgr.Name} - Team Size: {mgr.TeamSize}");
}
}
}
class Program
{
static void Main()
{
ManagerRepository<Manager> mgrRepo = new ManagerRepository<Manager>();
mgrRepo.AddManager(new Manager { Name = "Alice", TeamSize = 5 });
mgrRepo.AddManager(new Manager { Name = "Bob", TeamSize = 3 });
mgrRepo.DisplayManagers();
}
}
Sample Output:
Manager Alice with team size 5 added.
Manager Bob with team size 3 added.
Managers:
Alice - Team Size: 5
Bob - Team Size: 3
- Base Class Constraint: Ensures that `T` is a type that inherits from `Manager`.
- Type Safety: Only `Manager` or its derived types can be used with `ManagerRepository<T>`.
5.5 where T : Interface
Constrains the type parameter `T` to implement a specific interface.Example: Generic Logger with Interface Constraint
using System;
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"ConsoleLogger: {message}");
}
}
public class FileLogger : ILogger
{
public void Log(string message)
{
// Simulate file logging
Console.WriteLine($"FileLogger: {message}");
}
}
public class LoggerService<T> where T : ILogger
{
private T _logger;
public LoggerService(T logger)
{
_logger = logger;
}
public void LogMessage(string message)
{
_logger.Log(message);
}
}
class Program
{
static void Main()
{
LoggerService<ConsoleLogger> consoleLoggerService = new LoggerService<ConsoleLogger>(new ConsoleLogger());
consoleLoggerService.LogMessage("Logging to console.");
LoggerService<FileLogger> fileLoggerService = new LoggerService<FileLogger>(new FileLogger());
fileLoggerService.LogMessage("Logging to file.");
}
}
Sample Output:
ConsoleLogger: Logging to console.
FileLogger: Logging to file.
- Interface Constraint: Ensures that `T` implements the `ILogger` interface.
- Flexibility: `LoggerService<T>` can work with any logger that implements `ILogger`, promoting extensibility.
5.6 Multiple Constraints
You can apply multiple constraints to a type parameter by separating them with commas.Example: Generic Service with Multiple Constraints
using System;
public interface IEntity
{
int Id { get; set; }
}
public interface IAudit
{
void Audit();
}
public class User : IEntity, IAudit
{
public int Id { get; set; }
public string Username { get; set; }
public void Audit()
{
Console.WriteLine($"Auditing user {Username} with ID {Id}.");
}
}
public class Service<T> where T : class, IEntity, IAudit, new()
{
public T Create()
{
T entity = new T();
entity.Audit();
return entity;
}
}
class Program
{
static void Main()
{
Service<User> userService = new Service<User>();
User newUser = userService.Create();
newUser.Username = "JohnDoe";
Console.WriteLine($"Created User: {newUser.Username}, ID: {newUser.Id}");
}
}
Sample Output:
Auditing user with ID 0.
Created User: JohnDoe, ID: 0
- Multiple Constraints: `T` must be a reference type (`class`), implement `IEntity` and `IAudit` interfaces, and have a parameterless constructor (`new()`).
- Type Safety and Flexibility: Ensures that `Service<T>` operates on types that meet all specified requirements.
6. Variance in Generics
Variance allows for implicit reference conversions for generic type parameters, enabling more flexible code reuse. C# supports covariance and contravariance in generics, particularly with interfaces and delegates.6.1 Covariance
Covariance allows a generic type parameter to be substituted with a more derived type. It is applied to output (return) types.- Syntax: Use the `out` keyword.
- Usage: Suitable for read-only scenarios.
Example: Covariant Interface
using System;
using System.Collections.Generic;
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
public class Animal { }
public class Dog : Animal { }
class Program
{
static void Main()
{
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // Covariance allows this assignment
Console.WriteLine("Covariance allows IEnumerable<Dog> to be assigned to IEnumerable<Animal>.");
}
}
Sample Output:
Covariance allows IEnumerable<Dog> to be assigned to IEnumerable<Animal>.
- Covariant Interface: `IEnumerable<out T>` allows `IEnumerable<Dog>` to be treated as `IEnumerable<Animal>`.
- Read-Only Scenario: Since `IEnumerable<T>` only outputs `T` (no input), covariance is safe.
6.2 Contravariance
Contravariance allows a generic type parameter to be substituted with a less derived type. It is applied to input (parameter) types.- Syntax: Use the `in` keyword.
- Usage: Suitable for write-only scenarios.
Example: Contravariant Interface
using System;
public interface IComparer<in T>
{
int Compare(T x, T y);
}
public class Animal { }
public class Dog : Animal { }
public class AnimalComparer : IComparer<Animal>
{
public int Compare(Animal x, Animal y)
{
// Simple comparison logic
return 0;
}
}
class Program
{
static void Main()
{
IComparer<Animal> animalComparer = new AnimalComparer();
IComparer<Dog> dogComparer = animalComparer; // Contravariance allows this assignment
Console.WriteLine("Contravariance allows IComparer<Animal> to be assigned to IComparer<Dog>.");
}
}
Sample Output:
Contravariance allows IComparer<Animal> to be assigned to IComparer<Dog>.
Explanation:
- Contravariant Interface: `IComparer<in T>` allows `IComparer<Animal>` to be treated as `IComparer<Dog>`.
- Write-Only Scenario: Since `IComparer<T>` only consumes `T` (no output), contravariance is safe.
6.3 Invariance
Invariance means that no implicit conversion exists between different generic types, even if their type parameters are related.Example: Invariant Interface
using System;
public interface IContainer<T>
{
T Value { get; set; }
}
public class Animal { }
public class Dog : Animal { }
class Program
{
static void Main()
{
IContainer<Dog> dogContainer = null;
// IContainer<Animal> animalContainer = dogContainer; // Compilation Error: Incompatible types
Console.WriteLine("Invariance prevents IContainer<Dog> from being assigned to IContainer<Animal>.");
}
}
Sample Output:
Invariance prevents IContainer<Dog> from being assigned to IContainer<Animal>.
Explanation:
- Invariant Interface: `IContainer<T>` does not allow any variance, so `IContainer<Dog>` cannot be assigned to `IContainer<Animal>`.
6.4 Variance with Delegates
Delegates in C# also support covariance and contravariance, enhancing flexibility.Example: Covariant Delegate
using System;
public delegate Animal AnimalFactory();
public class Animal { }
public class Dog : Animal { }
class Program
{
static void Main()
{
AnimalFactory dogFactory = CreateDog;
AnimalFactory animalFactory = dogFactory; // Covariance allows this assignment
Animal animal = animalFactory();
Console.WriteLine($"Created an animal of type: {animal.GetType().Name}"); // Output: Dog
}
static Dog CreateDog()
{
return new Dog();
}
}
Sample Output:
Created an animal of type: Dog
Explanation:
- Covariant Delegate: `AnimalFactory` returns `Animal`. `CreateDog` returns `Dog`, which is a subclass of `Animal`, so covariance allows the assignment.
Example: Contravariant Delegate
using System;
public delegate void AnimalAction<in T>(T animal);
public class Animal { }
public class Dog : Animal { }
class Program
{
static void Main()
{
AnimalAction<Animal> action = PerformActionOnAnimal;
AnimalAction<Dog> dogAction = action; // Contravariance allows this assignment
dogAction(new Dog());
}
static void PerformActionOnAnimal(Animal animal)
{
Console.WriteLine($"Performing action on {animal.GetType().Name}");
}
}
Sample Output:
Performing action on Dog
Explanation:
- Contravariant Delegate: `AnimalAction<in T>` consumes `T`. `PerformActionOnAnimal` can accept any `Animal`, so `AnimalAction<Animal>` can be treated as `AnimalAction<Dog>`.
7. Generic Collections
C# provides a rich set of generic collection classes in the `System.Collections.Generic` namespace, offering type safety and performance benefits over non-generic collections.7.1 List<T>
A dynamic array that can grow and shrink in size.Example: Using List<T>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> fruits = new List<string>();
fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Cherry");
Console.WriteLine("Fruits List:");
foreach(var fruit in fruits)
{
Console.WriteLine(fruit);
}
Console.WriteLine($"Total Fruits: {fruits.Count}"); // Output: 3
}
}
Sample Output:
Fruits List:
Apple
Banana
Cherry
Total Fruits: 3
Explanation:
- Type Safety: Only `string` types can be added to `List<string>`.
- Dynamic Sizing: The list automatically resizes as items are added or removed.
7.2 Dictionary<TKey, TValue>
A collection of key-value pairs, optimized for fast lookups.Example: Using Dictionary<TKey, TValue>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary<int, string> studentGrades = new Dictionary<int, string>();
studentGrades.Add(101, "A");
studentGrades.Add(102, "B+");
studentGrades.Add(103, "A-");
Console.WriteLine("Student Grades:");
foreach(var kvp in studentGrades)
{
Console.WriteLine($"Student ID: {kvp.Key}, Grade: {kvp.Value}");
}
// Accessing by key
if(studentGrades.TryGetValue(102, out string grade))
{
Console.WriteLine($"Student 102's Grade: {grade}"); // Output: B+
}
}
}
Sample Output:
Student Grades:
Student ID: 101, Grade: A
Student ID: 102, Grade: B+
Student ID: 103, Grade: A-
Student 102's Grade: B+
- Type Safety: Keys and values are strongly typed (`int` and `string` respectively).
- Efficient Lookups: Provides fast retrieval of values based on keys.
7.3 Queue<T> and Stack<T>
Queue<T>: Represents a first-in, first-out (FIFO) collection.Example: Using Queue<T>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Queue<string> tasks = new Queue<string>();
tasks.Enqueue("Task1");
tasks.Enqueue("Task2");
tasks.Enqueue("Task3");
Console.WriteLine("Tasks Queue:");
while(tasks.Count > 0)
{
string task = tasks.Dequeue();
Console.WriteLine($"Processing {task}");
}
}
}
Sample Output:
Tasks Queue:
Processing Task1
Processing Task2
Processing Task3
- Dequeue: Removes items from the front, following FIFO order.
Stack<T>: Represents a last-in, first-out (LIFO) collection.
Example: Using Stack<T>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Stack<string> history = new Stack<string>();
history.Push("Page1");
history.Push("Page2");
history.Push("Page3");
Console.WriteLine("Browsing History:");
while(history.Count > 0)
{
string page = history.Pop();
Console.WriteLine($"Visited {page}");
}
}
}
Sample Output:
Browsing History:
Visited Page3
Visited Page2
Visited Page1
- Push: Adds items to the top of the stack.
- Pop: Removes items from the top, following LIFO order.
7.4 Comparison with Non-Generic Collections
Generic collections provide type safety and performance improvements over non-generic collections like `ArrayList` and `Hashtable`.Example: Generic vs. Non-Generic Collections
using System;
using System.Collections;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Non-Generic ArrayList
ArrayList arrayList = new ArrayList();
arrayList.Add(1);
arrayList.Add("Two");
arrayList.Add(3.0);
Console.WriteLine("ArrayList Contents:");
foreach(var item in arrayList)
{
Console.WriteLine(item);
}
// Generic List<int>
List<int> intList = new List<int>();
intList.Add(1);
intList.Add(2);
intList.Add(3);
Console.WriteLine("\nList<int> Contents:");
foreach(var item in intList)
{
Console.WriteLine(item);
}
}
}
Sample Output:
ArrayList Contents:
1
Two
3
List<int> Contents:
1
2
3
- Type Safety: `List<int>` ensures only integers are added, preventing runtime errors.
- Performance: Generic collections avoid boxing/unboxing and provide better performance.
8. Advanced Topics
8.1 Recursive Generics
Generics can reference themselves in their own type parameters, enabling powerful design patterns.Example: Generic Node for Linked List
using System;
public class Node<T>
{
public T Data { get; set; }
public Node<T> Next { get; set; }
public Node(T data)
{
Data = data;
Next = null;
}
}
class Program
{
static void Main()
{
Node<int> head = new Node<int>(1);
head.Next = new Node<int>(2);
head.Next.Next = new Node<int>(3);
Console.WriteLine("Linked List:");
Node<int> current = head;
while(current != null)
{
Console.WriteLine(current.Data);
current = current.Next;
}
}
}
Sample Output:
Linked List:
1
2
3
- Recursive Generic Type: `Node<T>` contains a reference to another `Node<T>`, enabling the creation of linked lists.
- Type Safety: Ensures that all nodes in the list hold the same data type.
8.2 Generic Constraints with Multiple Interfaces
A generic type parameter can be constrained to implement multiple interfaces.Example: Generic Processor with Multiple Interface Constraints
using System;
public interface IPrintable
{
void Print();
}
public interface IStorable
{
void Store();
}
public class Document : IPrintable, IStorable
{
public string Title { get; set; }
public void Print()
{
Console.WriteLine($"Printing Document: {Title}");
}
public void Store()
{
Console.WriteLine($"Storing Document: {Title}");
}
}
public class Processor<T> where T : IPrintable, IStorable
{
public void Process(T item)
{
item.Print();
item.Store();
}
}
class Program
{
static void Main()
{
Processor<Document> docProcessor = new Processor<Document>();
Document doc = new Document { Title = "Generics Guide" };
docProcessor.Process(doc);
}
}
Sample Output:
Printing Document: Generics Guide
Storing Document: Generics Guide
- Multiple Interface Constraints: `T` must implement both `IPrintable` and `IStorable`.
- Enhanced Functionality: Ensures that the `Process` method can safely call methods from both interfaces.
8.3 Reflection with Generics
Generics can be inspected and manipulated using reflection, enabling dynamic type operations.Example: Inspecting Generic Type Arguments with Reflection
using System;
using System.Collections.Generic;
using System.Reflection;
class Program
{
static void Main()
{
List<string> stringList = new List<string>();
Type type = stringList.GetType();
if(type.IsGenericType)
{
Type genericDefinition = type.GetGenericTypeDefinition();
Type[] genericArguments = type.GetGenericArguments();
Console.WriteLine($"Generic Type Definition: {genericDefinition}");
Console.WriteLine("Generic Type Arguments:");
foreach(var arg in genericArguments)
{
Console.WriteLine(arg);
}
}
}
}
Sample Output:
Generic Type Definition: System.Collections.Generic.List`1[T]
Generic Type Arguments:
System.String
Explanation:
- Reflection Usage: Determines if a type is generic, retrieves its generic definition, and lists its type arguments.
- Dynamic Operations: Useful for creating instances or invoking methods dynamically based on generic types.
8.4 Nullable Reference Types with Generics
C# 8.0 introduced nullable reference types, enhancing null safety in generic types.Example: Generic Class with Nullable Reference Types
using System;
public class Container<T> where T : class?
{
public T? Value { get; set; }
public void Display()
{
if(Value != null)
{
Console.WriteLine($"Value: {Value}");
}
else
{
Console.WriteLine("Value is null.");
}
}
}
class Program
{
static void Main()
{
Container<string> nonNullableContainer = new Container<string> { Value = "Hello" };
nonNullableContainer.Display(); // Output: Value: Hello
Container<string?> nullableContainer = new Container<string?> { Value = null };
nullableContainer.Display(); // Output: Value is null.
}
}
Sample Output:
Value: Hello
Value is null.
Explanation:
- Nullable Reference Types: `Container<T>` can handle both nullable and non-nullable reference types.
- Type Constraints: `where T : class?` allows `T` to be a nullable reference type.
8.5 Generic Covariance and Contravariance in Delegates and Interfaces
Generics in delegates and interfaces can leverage variance to enable more flexible type assignments.Example: Covariant Interface with IEnumerable<out T>
using System;
using System.Collections.Generic;
public class Animal { }
public class Dog : Animal { }
class Program
{
static void Main()
{
IEnumerable<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
IEnumerable<Animal> animals = dogs; // Covariance allows this assignment
Console.WriteLine("Animals Collection:");
foreach(var animal in animals)
{
Console.WriteLine(animal.GetType().Name);
}
}
}
Sample Output:
Animals Collection:
Dog
Dog
- Covariance: `IEnumerable<out T>` allows `IEnumerable<Dog>` to be treated as `IEnumerable<Animal>`.
- Type Safety: Ensures that operations are safe despite the type substitution.
Example: Contravariant Delegate with Action<in T>
using System;
public class Animal { }
public class Dog : Animal { }
public delegate void AnimalHandler<in T>(T animal);
class Program
{
static void Main()
{
AnimalHandler<Animal> handleAnimal = HandleAnimal;
AnimalHandler<Dog> handleDog = handleAnimal; // Contravariance allows this assignment
Dog dog = new Dog();
handleDog(dog); // Output: Handling an Animal.
}
static void HandleAnimal(Animal animal)
{
Console.WriteLine("Handling an Animal.");
}
}
Sample Output:
Handling an Animal.
Explanation:
- Contravariance: `AnimalHandler<in T>` allows `AnimalHandler<Animal>` to be assigned to `AnimalHandler<Dog>`.
- Type Safety: Ensures that the delegate can handle derived types safely.
9. Performance Considerations
Generics offer significant performance benefits by enabling type-safe code without the overhead of type casting or boxing/unboxing.9.1 Avoiding Boxing and Unboxing
Boxing converts a value type to a reference type, and unboxing reverses the process. Generics help avoid these operations, enhancing performance.Example: Boxing vs. Generics
using System;
using System.Collections;
class Program
{
static void Main()
{
// Without Generics: Boxing occurs
ArrayList list = new ArrayList();
list.Add(10); // Boxing
list.Add(20); // Boxing
int sum = 0;
foreach(var item in list)
{
sum += (int)item; // Unboxing
}
Console.WriteLine($"Sum without Generics: {sum}"); // Output: 30
// With Generics: No Boxing
List<int> genericList = new List<int>();
genericList.Add(10);
genericList.Add(20);
int genericSum = 0;
foreach(var item in genericList)
{
genericSum += item;
}
Console.WriteLine($"Sum with Generics: {genericSum}"); // Output: 30
}
}
Sample Output:
Sum without Generics: 30
Sum with Generics: 30
Explanation:
- Non-Generic Collection: `ArrayList` stores items as `object`, requiring boxing and unboxing for value types.
- Generic Collection: `List<int>` stores items as `int`, avoiding boxing/unboxing and improving performance.
9.2 Code Reuse and Type Safety
Generics promote code reuse without sacrificing type safety, leading to cleaner and more maintainable code.Example: Generic Method for Finding Maximum
using System;
public class Utility
{
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
}
class Program
{
static void Main()
{
Utility util = new Utility();
int maxInt = util.Max(10, 20);
Console.WriteLine($"Max Int: {maxInt}"); // Output: 20
string maxString = util.Max("Apple", "Banana");
Console.WriteLine($"Max String: {maxString}"); // Output: Banana
}
}
Sample Output:
Max Int: 20
Max String: Banana
Explanation:
- Generic Method: `Max<T>` finds the maximum of two comparable values.
- Type Safety: Ensures that only types implementing `IComparable<T>` can be used, preventing invalid comparisons.
10. Best Practices for Generics
Adhering to best practices ensures that generics are used effectively, enhancing code quality and maintainability.10.1 Favor Type Parameters Over Object
Using type parameters maintains type safety and avoids the need for casting.Bad Practice: Using Object
public class Box
{
private object _item;
public void Put(object item)
{
_item = item;
}
public object Get()
{
return _item;
}
}
Good Practice: Using Generics
public class Box<T>
{
private T _item;
public void Put(T item)
{
_item = item;
}
public T Get()
{
return _item;
}
}
Explanation:- Type Safety: Generics ensure that only the specified type `T` is stored, eliminating the risk of invalid casts.
- Performance: Avoids boxing/unboxing for value types.
10.2 Use Constraints Appropriately
Apply constraints to enforce type requirements, ensuring that generic methods and classes operate on compatible types.Example: Enforcing Interface Implementation
using System;
public interface IShape
{
double Area();
}
public class Circle : IShape
{
public double Radius { get; set; }
public double Area()
{
return Math.PI * Radius * Radius;
}
}
public class ShapePrinter<T> where T : IShape
{
public void PrintArea(T shape)
{
Console.WriteLine($"Area: {shape.Area()}");
}
}
class Program
{
static void Main()
{
Circle circle = new Circle { Radius = 5 };
ShapePrinter<Circle> printer = new ShapePrinter<Circle>();
printer.PrintArea(circle); // Output: Area: 78.53981633974483
}
}
Explanation:- Interface Constraint: Ensures that `T` implements `IShape`, allowing access to the `Area` method.
- Type Safety: Prevents passing incompatible types to `ShapePrinter<T>`.
10.3 Prefer Generic Interfaces and Delegates
Utilize built-in generic interfaces and delegates like `IEnumerable<T>`, `IComparer<T>`, `Func<T, TResult>`, and `Action<T>` to leverage language features and enhance interoperability.Example: Using IEnumerable<T>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach(var num in numbers)
{
Console.WriteLine(num);
}
}
}
Sample Output:
1
2
3
4
5
- Generic Interface: `IEnumerable<T>` provides a standardized way to iterate over collections.
- Interoperability: Enhances compatibility with LINQ and other language features.
10.4 Keep Generic Types Simple and Focused
Design generic classes and methods to have a single responsibility, promoting modularity and ease of understanding.Example: Single Responsibility Principle with Generics
using System;
using System.Collections.Generic;
// Responsibility: Manage items
public class Repository<T>
{
private List<T> _items = new List<T>();
public void Add(T item) => _items.Add(item);
public T Get(int index) => _items[index];
public IEnumerable<T> GetAll() => _items;
}
// Responsibility: Log items
public class Logger<T>
{
public void Log(T item)
{
Console.WriteLine($"Logging item: {item}");
}
}
class Program
{
static void Main()
{
Repository<string> repo = new Repository<string>();
Logger<string> logger = new Logger<string>();
repo.Add("Item1");
repo.Add("Item2");
foreach(var item in repo.GetAll())
{
logger.Log(item);
}
}
}
Sample Output:
Logging item: Item1
Logging item: Item2
- Separation of Concerns: `Repository<T>` handles data storage, while `Logger<T>` handles logging, each with a focused responsibility.
10.5 Avoid Overusing Generics
While generics offer flexibility, overusing them can lead to complex and hard-to-maintain code. Use generics when they provide clear benefits.Example: Overusing Generics
public class Singleton<T> where T : new()
{
private static T _instance;
public static T Instance
{
get
{
if(_instance == null)
{
_instance = new T();
}
return _instance;
}
}
}
Issue: Enforcing a singleton pattern with generics can be confusing and may not always be necessary.Alternative Approach: Use a non-generic singleton if the type is known in advance.
11. Common Mistakes with Generics
Avoiding common pitfalls ensures that generics are used effectively and do not introduce bugs or performance issues.11.1 Ignoring Generic Constraints
Mistake: Not applying necessary constraints, leading to runtime errors.Example: Missing Constraints
using System;
public class Factory<T>
{
public T Create()
{
return new T(); // Compilation Error: 'T' must have a parameterless constructor
}
}
class Program
{
static void Main()
{
Factory<string> stringFactory = new Factory<string>();
}
}
Solution: Apply the `new()` constraint.Corrected Example:
using System;
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
class Program
{
static void Main()
{
Factory<string> stringFactory = new Factory<string>();
string s = stringFactory.Create();
Console.WriteLine($"Created string: '{s}'"); // Output: Created string: ''
}
}
Explanation:- Constraint Application: Ensures that `T` has a parameterless constructor, allowing `new T()` to compile.
11.2 Using Non-Generic Collections When Generics Are Suitable
Mistake: Using non-generic collections like `ArrayList` when generic alternatives like `List<T>` are available.Example:
using System;
using System.Collections;
class Program
{
static void Main()
{
ArrayList list = new ArrayList();
list.Add(1);
list.Add("Two"); // Allows adding different types, leading to potential runtime errors
foreach(var item in list)
{
Console.WriteLine(item);
}
}
}
Solution: Use generic collections for type safety.Corrected Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> list = new List<int>();
list.Add(1);
// list.Add("Two"); // Compilation Error: Cannot add string to List<int>
foreach(var item in list)
{
Console.WriteLine(item);
}
}
}
Explanation:
- Type Safety: Prevents adding incompatible types, catching errors at compile time.
- Performance: Avoids boxing/unboxing associated with non-generic collections.
11.3 Overusing Constraints
Mistake: Applying unnecessary or overly restrictive constraints, reducing flexibility.Example:
public class Manager<T> where T : class, new(), IDisposable
{
public T Resource { get; set; }
public Manager()
{
Resource = new T();
}
public void DisposeResource()
{
Resource.Dispose();
}
}
Issue: Not all classes need to implement `IDisposable` or have a parameterless constructor, limiting the utility of `Manager<T>`.Solution: Apply constraints only when necessary, and consider alternative designs.
11.4 Forgetting to Specify Type Parameters
Mistake: Not specifying type arguments when creating generic instances, leading to runtime issues or the use of `object`.Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List list = new List(); // Compilation Error: Type arguments required
}
}
Solution: Always specify type parameters.Corrected Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
Console.WriteLine($"List Count: {list.Count}"); // Output: 2
}
}
Explanation:- Type Specification: Ensures that the collection is strongly typed, providing compile-time type checking.
11.5 Misunderstanding Variance
Mistake: Incorrectly assuming that all generic interfaces and delegates support variance, leading to compilation errors.Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<Dog> dogList = new List<Dog>();
IEnumerable<Animal> animalEnumerable = dogList; // Covariance works with IEnumerable<out T>
}
}
public class Animal { }
public class Dog : Animal { }
Explanation:- Covariance Limitation: Only certain generic interfaces like `IEnumerable<out T>` support covariance. `List<T>` does not support it directly.
Solution: Use covariant interfaces when type substitution is needed.
12. Real-World Example
Example: Generic Repository Pattern with Constraints and Interfaces
The Repository Pattern abstracts data access, providing a centralized way to manage data operations. Combining generics with constraints and interfaces enhances flexibility and type safety.Code Example:
using System;
using System.Collections.Generic;
// Base Entity Interface
public interface IEntity
{
int Id { get; set; }
}
// Repository Interface
public interface IRepository<T> where T : IEntity
{
void Add(T entity);
T Get(int id);
IEnumerable<T> GetAll();
void Remove(int id);
}
// Generic Repository Implementation
public class Repository<T> : IRepository<T> where T : IEntity, new()
{
private readonly Dictionary<int, T> _store = new Dictionary<int, T>();
public void Add(T entity)
{
if(_store.ContainsKey(entity.Id))
throw new ArgumentException("Entity with the same ID already exists.");
_store[entity.Id] = entity;
Console.WriteLine($"Entity with ID {entity.Id} added.");
}
public T Get(int id)
{
if(_store.TryGetValue(id, out T entity))
return entity;
throw new KeyNotFoundException("Entity not found.");
}
public IEnumerable<T> GetAll()
{
return _store.Values;
}
public void Remove(int id)
{
if(_store.Remove(id))
Console.WriteLine($"Entity with ID {id} removed.");
else
Console.WriteLine($"Entity with ID {id} not found.");
}
}
// Example Entity
public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"User ID: {Id}, Name: {Name}";
}
}
class Program
{
static void Main()
{
IRepository<User> userRepo = new Repository<User>();
User user1 = new User { Id = 1, Name = "Alice" };
User user2 = new User { Id = 2, Name = "Bob" };
userRepo.Add(user1); // Output: Entity with ID 1 added.
userRepo.Add(user2); // Output: Entity with ID 2 added.
Console.WriteLine("\nAll Users:");
foreach(var user in userRepo.GetAll())
{
Console.WriteLine(user);
}
// Output:
// User ID: 1, Name: Alice
// User ID: 2, Name: Bob
User fetchedUser = userRepo.Get(1);
Console.WriteLine($"\nFetched User: {fetchedUser}"); // Output: User ID: 1, Name: Alice
userRepo.Remove(2); // Output: Entity with ID 2 removed.
userRepo.Remove(3); // Output: Entity with ID 3 not found.
}
}
Sample Output:
Entity with ID 1 added.
Entity with ID 2 added.
All Users:
User ID: 1, Name: Alice
User ID: 2, Name: Bob
Fetched User: User ID: 1, Name: Alice
Entity with ID 2 removed.
Entity with ID 3 not found.
- IEntity Interface: Ensures that all entities have an `Id` property.
- IRepository<T> Interface: Defines generic CRUD operations constrained to `IEntity`.
- Repository<T> Class: Implements `IRepository<T>`, managing entities in an internal `Dictionary<int, T>`.
- Type Safety and Reusability: The repository can manage any entity type that implements `IEntity` and has a parameterless constructor.
- Usage: Demonstrates adding, retrieving, displaying, and removing `User` entities with type safety and clear operations.
13. Summary
Generics in C# are a robust feature that enable developers to write flexible, reusable, and type-safe code. By understanding and leveraging generics, you can create components that work with any data type, enhance performance by avoiding unnecessary type casting and boxing, and enforce compile-time type checks to prevent runtime errors.Key Takeaways:
- Type Safety and Reusability: Generics allow you to create components that work with any specified type while ensuring type safety.
- Performance Benefits: Avoids boxing/unboxing and type casting, leading to more efficient code.
- Generic Constraints: Apply constraints (`where` clauses) to enforce type requirements, enhancing reliability.
- Variance: Covariance and contravariance in generics provide flexibility in type assignments, particularly with interfaces and delegates.
- Generic Collections: Utilize built-in generic collections (`List<T>`, `Dictionary<TKey, TValue>`, etc.) for type-safe and performant data management.
- Advanced Features: Explore recursive generics, reflection with generics, and generic covariance/contravariance for sophisticated scenarios.
- Best Practices: Favor type parameters over `object`, apply constraints judiciously, use built-in generic interfaces and delegates, and keep generic types focused and simple.
- Common Mistakes: Avoid ignoring constraints, overusing generics, misunderstanding variance, and mismanaging type parameters.
- Real-World Applications: Implement design patterns like Repository, Factory, and Service using generics to enhance code modularity and maintainability.
By mastering generics, you empower yourself to write more efficient, maintainable, and robust C# applications, fully leveraging the language's capabilities to handle a wide range of programming challenges.