C# -

Generics

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.


1. Understanding Generics

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
    }
}
        
    

2. Defining Generic Classes

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
    }
}
        
    

3. Defining Generic Methods

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
    }
}
        
    

4. Generic Interfaces

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
    }
}
        
    

5. Constraints on Type Parameters

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 : struct { }
where T : class Specifies that the type must be a reference type. public class MyClass where T : class { }
where T : new() Specifies that the type must have a parameterless constructor. public class MyGenericClass where T : new() { }
where T : SomeBaseClass Specifies that the type must inherit from a specific base class. public class MyGenericClass where T : SomeBaseClass { }
where T : ISomeInterface Specifies that the type must implement a specific interface. public class MyGenericClass where T : ISomeInterface { }

6. Covariance and Contravariance

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
    }
}

        
    

7. Real-Life Examples of Generics

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);
        }
    }
}
        
    
Database Access Example:
        
            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);
    }
}
        
    
Delegates and Events Example:
        
            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
    }
}

        
    

8. Best Practices for Using Generics



Conclusion

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.