Page Content

Tutorials

Generics In C#: Achieving Reusability And Type Safety

Generics in C#

Designing classes, interfaces, methods, and delegates that postpone the defining of one or more types until they are defined and instantiated by client code is made possible by the strong feature of generics in C#, which adds type parameters to the.NET Framework. As a result, you may write code that supports any data type without sacrificing performance or type safety.

Benefits of Generics

Using generics has a number of important benefits:

Code Reusability: A class or method can be written once and used with any sort of data with generics. Code reusability and flexibility are increased as a result of the necessity to not rewrite the same logic for various types.

Type Safety: At compilation time, generics enforce type checking. By doing this, type-related mistakes that arise with non-generic types (such as ArrayList, which casts everything to Object) are avoided. These issues would otherwise only be discovered after runtime. When working with collections, the C# compiler shields you from these types of errors.

Performance: Generics can greatly increase speed by removing implicit upcasting to Object and the boxing/unboxing procedures that follow for value types, particularly when iterating over huge collections. Conversions are not required because the value type is native to each specialised generic class.

Clarity and Maintainability: Because it clearly identifies the types it works with, even if they are placeholders, generic code is frequently simpler to read and comprehend.

Generic Classes

The class name is followed by a type parameter in angle brackets to define a generic class. This type argument serves as a stand-in for a particular type that will be supplied when an instance of the class is generated; it is commonly referred to as T by convention.

Example: Defining a Generic Class (DataStore<T> or AnimalShelter<T>)

using System;
using System.Collections.Generic; // Make sure this is present for List<T>

public class AnimalShelter<TAnimal>
{
    private List<TAnimal> animalList;
    public AnimalShelter()
    {
        this.animalList = new List<TAnimal>();
    }
    public AnimalShelter(int initialCapacity)
    {
        if (initialCapacity < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(initialCapacity), "Initial capacity cannot be negative.");
        }
        this.animalList = new List<TAnimal>(initialCapacity);
    }
    public void Shelter(TAnimal newAnimal)
    {
        if (newAnimal == null)
        {
            throw new ArgumentNullException(nameof(newAnimal), "Cannot shelter a null animal.");
        }
        this.animalList.Add(newAnimal);
    }
    public TAnimal Release(int index)
    {
        if (index < 0 || index >= this.animalList.Count)
        {
            throw new ArgumentOutOfRangeException(nameof(index), $"Invalid index. Must be between 0 and {this.animalList.Count - 1}.");
        }
        
        TAnimal releasedAnimal = this.animalList[index];
        this.animalList.RemoveAt(index);
        return releasedAnimal;
    }
    public int CurrentAnimalsCount => animalList.Count;
    public IReadOnlyList<TAnimal> GetAllAnimals()
    {
        return animalList.AsReadOnly();
    }
}
public class Program
{
    public static void Main(string[] args)
    {
        
        Console.WriteLine("\n--- AnimalShelter Example ---");
        AnimalShelter<string> dogShelter = new AnimalShelter<string>(3);
        Console.WriteLine($"Current animals in shelter: {dogShelter.CurrentAnimalsCount}");
        try
        {
            dogShelter.Shelter("Buddy");
            dogShelter.Shelter("Max");
            dogShelter.Shelter("Lucy");
            Console.WriteLine($"Current animals in shelter after sheltering: {dogShelter.CurrentAnimalsCount}");
            Console.WriteLine("Animals currently in shelter:");
            foreach (var animal in dogShelter.GetAllAnimals())
            {
                Console.WriteLine($"- {animal}");
            }
            string releasedDog = dogShelter.Release(1);
            Console.WriteLine($"\nReleased animal: {releasedDog}");
            Console.WriteLine($"Current animals in shelter after release: {dogShelter.CurrentAnimalsCount}");
            Console.WriteLine("Animals currently in shelter:");
            foreach (var animal in dogShelter.GetAllAnimals())
            {
                Console.WriteLine($"- {animal}");
            }
        }
        catch (ArgumentNullException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        catch (ArgumentOutOfRangeException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"An unexpected error occurred: {ex.Message}");
        }
        Console.WriteLine("\nProgram finished.");
    } // This is the closing brace for the Main method
} // This is the closing brace for the Program class

Output

--- AnimalShelter Example ---
Current animals in shelter: 0
Current animals in shelter after sheltering: 3
Animals currently in shelter:
- Buddy
- Max
- Lucy
Released animal: Max
Current animals in shelter after release: 2
Animals currently in shelter:
- Buddy
- Lucy
Program finished.

Instantiating Generic Classes

In order to construct an instance of a generic class, you must define it with the actual type enclosed in angle brackets.

Example

using System;
using System.Collections.Generic; // Required for List<T>
// --- Existing DataStore Class ---
public class DataStore<T>
{
    // Property was changed from 'Data' to 'Value' for better naming
    public T Value { get; set; } 
    public DataStore(T value)
    {
        this.Value = value;
    }
    public void DisplayValue()
    {
        Console.WriteLine($"Stored Value: {Value}");
    }
}

// --- Main Program Class ---
public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("--- DataStore Example ---");
        
        // Example using DataStore
        DataStore<string> stringStore = new DataStore<string>("Hello Generics!"); 
        // Corrected from .Data to .Value
        Console.WriteLine(stringStore.Value); 
        DataStore<int> intStore = new DataStore<int>(123); 
        // Corrected from .Data to .Value
        Console.WriteLine(intStore.Value); 
        
    }
}

Output

--- DataStore Example ---
Hello Generics!
123

Constraints on Type Parameters

There are situations when limiting the kinds that can be used for a generic type parameter is necessary. The where contextual keyword is used to provide constraints in this way. These limitations provide the compiler assurance that the type argument will fall into a particular category or support particular operations.

Typical categories of restrictions include of:

where T : struct: Value types (other than nullable) are required for the type parameter.

where T : class: Any class, interface, delegate, or array that is a reference type must be used as the type argument.

where T : new():  A public parameterless constructor is required for type arguments; if there are other requirements, it must be given last.

where T : <base class name>: The type argument needs to belong to or be derived from the designated base class.

where T : <interface name>: The type argument needs to implement the designated interface or be it. You can specify more than one interface constraint.

where T : U:  According to the naked type constraint, the type argument provided for T must be the same as or be derived from the argument provided for U.

Example with Constraints

using System; // Required for Console, IComparable<T>
using System.Collections.Generic; // Required for List<T>
// Only types that implement IComparable<T> and have a parameterless constructor can be used
public class SortedList<T> where T : IComparable<T>, new()
{
    private List<T> _items;
    public SortedList()
    {
        _items = new List<T>();
    }
    // A simple method to add items and maintain sorted order
    public void Add(T item)
    {
        if (item == null)
        {
            // For reference types, adding null might be problematic if CompareTo expects non-null
            // Consider if your specific scenario allows nulls.
            throw new ArgumentNullException(nameof(item), "Cannot add a null item to SortedList.");
        }
        int index = 0;
        // Find the correct insertion point to maintain sorted order
        while (index < _items.Count && item.CompareTo(_items[index]) > 0)
        {
            index++;
        }
        _items.Insert(index, item);
    }
    // Method to display items
    public void DisplayItems()
    {
        Console.WriteLine("Current SortedList Items:");
        if (_items.Count == 0)
        {
            Console.WriteLine("(Empty)");
        }
        else
        {
            foreach (var item in _items)
            {
                Console.WriteLine($"- {item}"); // Assumes T has a meaningful ToString() override
            }
        }
    }
    // Example of using the 'new()' constraint to create a new instance
    public T GetNewDefaultInstance()
    {
        return new T(); // This creates a new instance using the parameterless constructor
    }
}
// Example usage:
public class Product : IComparable<Product>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    // IMPORTANT: This public parameterless constructor is REQUIRED by the 'new()' constraint
    public Product()
    {
        Name = "Unknown Product";
        Price = 0.0m;
    }
    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
    public int CompareTo(Product other)
    {
        // Handle null 'other' to prevent NullReferenceException
        if (other == null) return 1; // Conventionally, a non-null instance is greater than null
        return this.Price.CompareTo(other.Price);
    }
    public override string ToString()
    {
        return $"Product: {Name}, Price: {Price:C}"; // :C formats as currency
    }
}

// Main Program class to execute the code
public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("--- SortedList<Product> Example ---");
        // Valid: Product implements IComparable<Product> and has a public parameterless constructor
        SortedList<Product> products = new SortedList<Product>();
        products.Add(new Product("Laptop", 1200.50m));
        products.Add(new Product("Mouse", 25.00m));
        products.Add(new Product("Keyboard", 75.25m));
        products.Add(new Product("Monitor", 300.00m));
        products.Add(new Product("USB Drive", 15.75m));
        products.DisplayItems();
        Console.WriteLine($"\nCreating a new default product using GetNewDefaultInstance(): {products.GetNewDefaultInstance()}");

        Console.WriteLine("\n--- SortedList<int> (Invalid Example) ---");
        // Invalid: int is a value type. While it implicitly implements IComparable<int>,
        // the 'new()' constraint requires a public parameterless constructor accessible to the generic type.
        // For value types (structs), 'new T()' performs a default initialization (e.g., 0 for int).
        // However, the *compiler* enforces the 'new()' constraint more strictly on types that can *have*
        // a user-defined parameterless constructor. Since 'int' doesn't have a *public parameterless constructor*
        // in the way a class or struct might, it fails this specific constraint.
        Console.WriteLine("// SortedList<int> numbers = new SortedList<int>(); // This line would cause a compile-time error.");
        Console.WriteLine("Compilation Error Expected: 'int' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'SortedList<T>'.");
        Console.WriteLine("This demonstrates compile-time type safety due to the 'new()' constraint.");
    }
}

Output

--- SortedList<Product> Example ---
Current SortedList Items:
- Product: USB Drive, Price: $15.75
- Product: Mouse, Price: $25.00
- Product: Keyboard, Price: $75.25
- Product: Monitor, Price: $300.00
- Product: Laptop, Price: $1,200.50
Creating a new default product using GetNewDefaultInstance(): Product: Unknown Product, Price: $0.00
--- SortedList<int> (Invalid Example) ---
// SortedList<int> numbers = new SortedList<int>(); // This line would cause a compile-time error.
Compilation Error Expected: 'int' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'SortedList<T>'.
This demonstrates compile-time type safety due to the 'new()' constraint.

Generic Methods

Methods are generic because, like classes, they can be specified with type parameters. As a result, type safety is maintained when a single method definition works with arguments of several types.

Example: Defining a Generic Method (Swap<K>)

using System; // Required for Console.WriteLine
// A non-generic class that contains a generic method
public class CommonOperations
{
    // A generic method: 'K' is the type parameter for this method only.
    // It can swap the values of two variables of any type K.
    public void Swap<K>(ref K a, ref K b)
    {
        K temp = a; // 'temp' will be of type K
        a = b;
        b = temp;
    }
    // Another simple generic method example
    public void PrintInfo<T>(T item)
    {
        Console.WriteLine($"\nItem Type: {item.GetType().Name}, Value: {item}");
    }
}
// The Main program to run the example
public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("=== Generic Method (Swap<K>) Example ===");
        CommonOperations ops = new CommonOperations();
        // --- Example with Integers ---
        int num1 = 10, num2 = 20;
        Console.WriteLine($"\nBefore swap: num1={num1}, num2={num2}");
        ops.Swap(ref num1, ref num2); // Compiler infers K as int
        Console.WriteLine($"After swap: num1={num1}, num2={num2}");
        ops.PrintInfo(num1);
        // --- Example with Strings ---
        string str1 = "Hello", str2 = "World";
        Console.WriteLine($"\nBefore swap: str1={str1}, str2={str2}");
        ops.Swap(ref str1, ref str2); // Compiler infers K as string
        Console.WriteLine($"After swap: str1={str1}, str2={str2}");
        ops.PrintInfo(str1);
        // --- Example with a custom object (requires class definition) ---
        // For this to work, ensure 'Product' class (or any class) is defined somewhere reachable.
        // For simplicity, let's just define a simple object here if Product is not accessible.
        var obj1 = new { Id = 1, Value = "Apple" };
        var obj2 = new { Id = 2, Value = "Banana" };
        Console.WriteLine($"\nBefore swap: obj1={obj1}, obj2={obj2}");
        ops.Swap(ref obj1, ref obj2); // Compiler infers K as an anonymous type
        Console.WriteLine($"After swap: obj1={obj1}, obj2={obj2}");
        ops.PrintInfo(obj1);

        Console.WriteLine("\nExample Finished.");
    }
}

Output

=== Generic Method (Swap<K>) Example ===
Before swap: num1=10, num2=20
After swap: num1=20, num2=10
Item Type: Int32, Value: 20
Before swap: str1=Hello, str2=World
After swap: str1=World, str2=Hello
Item Type: String, Value: World
Before swap: obj1={ Id = 1, Value = Apple }, obj2={ Id = 2, Value = Banana }
After swap: obj1={ Id = 2, Value = Banana }, obj2={ Id = 1, Value = Apple }
Item Type: <>__AnonType0`2, Value: { Id = 2, Value = Banana }
Example Finished.

Type Inference: Type arguments for generic methods don’t always need to be explicitly specified in angle brackets because the compiler may frequently infer them implicitly from the method parameters.

default Keyword: You may be unsure whether T will be a reference type or a value type when working with generic types. The default value for any parameterised type T is provided via the default keyword. For reference types, it returns null; for numeric value types, it returns zero; for structs, it initialises each member to its default value. For generic variables to be initialised, this is essential.

Other Generic Constructs

Generic Interfaces: It is possible to construct generic interfaces, like IComparable, which are better than non-generic equivalents since they do not require boxing and unpacking of value types.

Generic Delegates: Also, delegates have the ability to specify their own type arguments.

Generics vs. C++ Templates

Although both C++ templates and C# generics provide parameterised types, there are some significant distinctions:

Implementation Level: Generic type information is maintained for instantiated objects, and generic type substitutions in C# are carried out during runtime. This contrasts with templates In C++, where specialization usually takes place during compilation, potentially resulting in bigger code sizes.

Complexity and Features: C# generics provide a more straightforward method without the intricacy that is frequently connected to C++ templates. Whereas C++ templates permit code that may only be valid for particular type parameters, with faults manifesting at instantiation time, C# mandates that generic code be valid for any type that fulfils itself.

Type Parameter Usage: Type parameters in C# are not allowed to have default types or to be the base class for the generic type itself.

Reflection with Generics

Generic type information is kept at runtime via the Common Language Runtime (CLR). As with non-generic types, this enables you to query information about generic types and methods using reflection.

Example of Reflection:

using System; // Required for Console and Type
using System.Collections.Generic; // Required for List<T>
public class Program
{
    public static void Main(string[] args)
    {
        // 1. Create an instance of a closed generic type (List<int>)
        List<int> myList = new List<int>();
        
        // 2. Get the Type object for the instance
        Type type = myList.GetType();
        Console.WriteLine($"Inspected Type: {type.Name}"); // Output: List`1
        // 3. Check if the type is generic
        if (type.IsGenericType) // Returns true if the type is generic
        {
            Console.WriteLine("Type is generic: TRUE");
            // 4. Get the open generic type definition (e.g., List<>)
            Type genericTypeDefinition = type.GetGenericTypeDefinition();
            Console.WriteLine($"Generic Type Definition: {genericTypeDefinition.Name}"); // Output: List`1
            // 5. Get the actual type arguments (e.g., int)
            Type[] genericArguments = type.GetGenericArguments();
            Console.WriteLine("Generic Arguments:");
            foreach (Type arg in genericArguments)
            {
                Console.WriteLine($"- {arg.Name}"); // Output: Int32
            }
        }
        else
        {
            Console.WriteLine("Type is generic: FALSE");
        }
        Console.WriteLine("\n--- Another Example (Non-Generic) ---");
        Type stringType = typeof(string);
        Console.WriteLine($"Inspected Type: {stringType.Name}");
        if (stringType.IsGenericType)
        {
            Console.WriteLine("Type is generic: TRUE");
        }
        else
        {
            Console.WriteLine("Type is generic: FALSE"); // Output: Type is generic: FALSE
        }
        Console.WriteLine("\n--- Another Example (Open Generic Type) ---");
        Type openListType = typeof(List<>); // This gets the open generic type directly
        Console.WriteLine($"Inspected Type: {openListType.Name}");
        if (openListType.IsGenericType)
        {
            Console.WriteLine("Type is generic: TRUE");
            Console.WriteLine($"Generic Type Definition: {openListType.GetGenericTypeDefinition().Name}");
            // openListType.GetGenericArguments() would return an empty array here
            // as there are no concrete arguments yet.
        }
        else
        {
            Console.WriteLine("Type is generic: FALSE");
        }
    }
}

Output

Inspected Type: List`1
Type is generic: TRUE
Generic Type Definition: List`1
Generic Arguments:
- Int32
--- Another Example (Non-Generic) ---
Inspected Type: String
Type is generic: FALSE
--- Another Example (Open Generic Type) ---
Inspected Type: List`1
Type is generic: TRUE
Generic Type Definition: List`1

Consider generics as an all-purpose mould for creating specialised tools. The generic class/method is to produce a single “adjustable wrench mould” rather than making a different wrench for each nut size (e.g., a “small nut wrench,” “medium nut wrench,” and “large nut wrench”). When you instruct the mould to “make me a wrench for this 10mm nut,” it will create a precisely sized 10mm wrench that fits the specified nut. In this manner, you may safely modify a single, reliable design to accommodate a wide range of “nut sizes” (data kinds) without always creating a new tool from the ground up.

You can also read Events In C#: An Essential Concept for Modern Applications

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index