Generics in C# provide a way to define classes, interfaces, and methods with a placeholder for the type of data they store or use. This allows for code reuse, type safety, and improved performance by eliminating the need for boxing and unboxing.
Generics enable you to define type-safe data structures without committing to actual data types. Instead, a placeholder is used that will be replaced with the actual type when the generic is instantiated or called.
Example:
namespace GenericsExamples;
// Define a generic class
public class Box<T>
{
private T value;
public void Add(T item)
{
value = item;
}
public T Get()
{
return value;
}
}
class Program
{
static void Main(string[] args)
{
Box<int> intBox = new Box<int>();
intBox.Add(123);
Console.WriteLine(intBox.Get()); // Output: 123
Box<string> strBox = new Box<string>();
strBox.Add("Hello, Generics");
Console.WriteLine(strBox.Get()); // Output: Hello, Generics
}
}
You can define a generic class by using a type parameter in angle brackets. This parameter can then be used within the class to specify the type of its members.
Example:
namespace GenericsExamples;
// Define a generic class with two type parameters
public class Pair<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
public void Display()
{
Console.WriteLine($"First: {First}, Second: {Second}");
}
}
class Program
{
static void Main(string[] args)
{
Pair<int, string> pair = new Pair<int, string>(1, "One");
pair.Display(); // Output: First: 1, Second: One
}
}
Generic methods are methods that include a type parameter. This type parameter allows the method to operate on different data types while maintaining type safety.
Example:
namespace GenericsExamples;
public class Utility
{
// Define a generic method
public void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
class Program
{
static void Main(string[] args)
{
int x = 1, y = 2;
Utility utility = new Utility();
utility.Swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}"); // Output: x: 2, y: 1
string str1 = "Hello", str2 = "World";
utility.Swap(ref str1, ref str2);
Console.WriteLine($"str1: {str1}, str2: {str2}"); // Output: str1: World, str2: Hello
}
}
Generic interfaces are interfaces that include a type parameter. Classes that implement the interface can specify the actual type to use when implementing its members.
Example:
namespace GenericsExamples;
// Define a generic interface
public interface IRepository<T>
{
void Add(T item);
T Get(int id);
}
// Implement the generic interface in a class
public class Repository<T> : IRepository<T>
{
private readonly Dictionary<int, T> _items = new Dictionary<int, T>();
public void Add(T item)
{
int id = _items.Count + 1;
_items[id] = item;
}
public T Get(int id)
{
_items.TryGetValue(id, out T item);
return item;
}
}
class Program
{
static void Main(string[] args)
{
IRepository<string> stringRepo = new Repository<string>();
stringRepo.Add("Item1");
Console.WriteLine(stringRepo.Get(1)); // Output: Item1
IRepository<int> intRepo = new Repository<int>();
intRepo.Add(100);
Console.WriteLine(intRepo.Get(1)); // Output: 100
}
}
Constraints allow you to restrict the types that can be used as arguments for a type parameter in a generic class, method, or interface. Common constraints include where T : struct
, where T : class
, where T : new()
, and where T : SomeBaseClass
.
Constraint | Description | Example |
---|---|---|
where T : struct |
Specifies that the type must be a value type. | public class MyStruct |
where T : class |
Specifies that the type must be a reference type. | public class MyClass |
where T : new() |
Specifies that the type must have a parameterless constructor. | public class MyGenericClass |
where T : SomeBaseClass |
Specifies that the type must inherit from a specific base class. | public class MyGenericClass |
where T : ISomeInterface |
Specifies that the type must implement a specific interface. | public class MyGenericClass |
Covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type parameters. Covariance preserves assignment compatibility, whereas contravariance reverses it.
Example:
using System;
namespace GenericsExamples;
// Define a base class
public class Animal
{
public string Name { get; set; }
}
// Define a derived class
public class Dog : Animal { }
// Define a generic interface with covariance
public interface ICovariant<out T>
{
T Get();
}
// Define a generic interface with contravariance
public interface IContravariant<in T>
{
void Add(T item);
}
public class AnimalShelter<T> : ICovariant<T>, IContravariant<T>
{
private T animal;
public T Get() => animal;
public void Add(T item)
{
animal = item;
}
}
class Program
{
static void Main(string[] args)
{
// Covariance
ICovariant<Dog> dogCovariant = new AnimalShelter<Dog>();
ICovariant<Animal> animalCovariant = dogCovariant;
// Contravariance
IContravariant<Animal> animalContravariant = new AnimalShelter<Animal>();
IContravariant<Dog> dogContravariant = animalContravariant;
// Usage
AnimalShelter<Animal> shelter = new AnimalShelter<Animal>();
shelter.Add(new Dog { Name = "Buddy" });
Animal animal = shelter.Get();
Console.WriteLine($"Animal Name: {animal.Name}"); // Output: Animal Name: Buddy
}
}
Generics are extensively used in C# to implement various collection classes and data structures such as lists, dictionaries, and queues. These data structures can store and manipulate elements of any data type efficiently.
Collections and Data Structures Example:
namespace GenericsExamples;
// Using List<T> with different types
class Program
{
static void Main(string[] args)
{
List<int> integerList = new List<int>();
integerList.Add(1);
integerList.Add(2);
List<string> stringList = new List<string>();
stringList.Add("Apple");
stringList.Add("Banana");
Console.WriteLine("Integer List:");
foreach (int item in integerList)
{
Console.WriteLine(item);
}
Console.WriteLine("String List:");
foreach (string item in stringList)
{
Console.WriteLine(item);
}
}
}
namespace GenericsExamples;
public class Repository<T>
{
public T GetById(int id)
{
// Simulated database retrieval
return default(T);
}
public void Save(T entity)
{
// Simulated database save operation
Console.WriteLine($"Saved {entity} to database.");
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
class Program
{
static void Main(string[] args)
{
Repository<User> userRepository = new Repository<User>();
User user = new User { Id = 1, Name = "Alice" };
userRepository.Save(user);
Repository<Product> productRepository = new Repository<Product>();
Product product = new Product { Id = 1, Name = "Laptop", Price = 999.99M };
productRepository.Save(product);
}
}
using System;
namespace GenericsExamples;
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
public class Button
{
public event EventHandler<ClickEventArgs> Click;
public void SimulateClick()
{
// Raise the Click event
Click?.Invoke(this, new ClickEventArgs());
}
}
public class ClickEventArgs
{
public DateTime ClickTime { get; } = DateTime.Now;
}
class Program
{
static void Main(string[] args)
{
Button button = new Button();
button.Click += (sender, e) =>
{
Console.WriteLine($"Button clicked at {e.ClickTime}");
};
button.SimulateClick(); // Simulate a button click event
}
}
Generics in C# offer a powerful mechanism for defining type-safe and reusable data structures and methods. By understanding how to define and use generics, as well as the benefits and best practices associated with their use, you can build more robust and maintainable applications.