Paramclasses Academy - Blog

Tutorials Details

←
Post 22 of 30
→

What is Generics in C# ? Explain: Benefits, Syntax and Practical Examples

What is Generics in C# ? Explain: Benefits, Syntax and Practical Examples

October 26, 2026

Introduction

In C# programming, developers often need to write code that works with different data types. For example, an application may need to store integers, strings, decimal values, or custom objects.

Without generics, developers may end up writing separate classes and methods for different data types, or rely on less type-safe approaches that require casting.

Generics in C# allow you to define classes, methods, interfaces, and other types that work with specified data types while maintaining compile-time type safety. They make code more reusable, readable, and easier to maintain.

In this tutorial, you will learn what generics are, why they are useful, how to create generic methods and classes, and how to use generic constraints with practical examples.

What You Will Learn

  • What generics are in C#

  • Why generics are useful

  • The difference between generic and non-generic code

  • How to create generic methods

  • How to create generic classes

  • How to use multiple type parameters

  • How to apply generic constraints

  • How to work with generic interfaces

  • How generics are used in collections

  • Common mistakes and best practices

1. What Are Generics in C#?

Generics allow you to write reusable code that works with different data types without sacrificing compile-time type safety.

Instead of specifying a concrete type when defining a generic class or method, you use a type parameter, commonly represented by a letter such as T.

For example:

public class Box<T>
{
    public T Value { get; set; }
}

Here, T is a type parameter. When you create an object, you specify the actual type.

Box<int> numberBox = new Box<int>();
numberBox.Value = 100;

Box<string> textBox = new Box<string>();
textBox.Value = "Hello, C#";

The first object stores an integer, while the second stores a string.

The compiler checks that the assigned values match the specified type.

Common Generic Type Parameter Names

Parameter Typical Meaning
T A general type
TKey A key type
TValue A value type
TInput An input type
TOutput An output type

These names are conventions, not special C# keywords. You can use descriptive names when they make the code easier to understand.

2. Why Do We Need Generics?

Consider a method that returns the same value it receives.

Without generics, you might write separate methods:

static int GetInt(int value)
{
    return value;
}

static string GetString(string value)
{
    return value;
}

static double GetDouble(double value)
{
    return value;
}

These methods perform the same operation, but they must be repeated for different types.

Generics let you write one reusable method:

static T GetValue<T>(T value)
{
    return value;
}

Now you can use it with different types:

int number = GetValue<int>(100);
string name = GetValue<string>("Aman");
double price = GetValue<double>(99.50);

The same method works with all three types.

Main Benefits of Generics

1. Code reusability: Write a class or method once and use it with multiple compatible data types.

2. Type safety: The compiler can detect many incorrect type assignments before the program runs.

3. Less casting: Generic collections and methods can return values of their specified types directly.

4. Better maintainability: Shared logic can be implemented in one place instead of being duplicated across several methods.

5. Performance advantages: Generics can avoid boxing for value types in many common scenarios, unlike approaches that store values as object. Actual performance depends on the code and runtime behavior.

3. Generic Methods in C#

A generic method defines one or more type parameters that can be used in its parameters, return type, or method body.

Syntax

returnType MethodName<T>(T parameter)
{
    // Method body
}

The <T> declaration introduces the type parameter. When calling the method, you can provide the type explicitly or allow the compiler to infer it from the arguments.

Example: Displaying Different Data Types

using System;

class Program
{
    static void Display<T>(T value)
    {
        Console.WriteLine($"Value: {value}");
        Console.WriteLine($"Type: {typeof(T).Name}");
    }

    static void Main()
    {
        Display<int>(100);
        Display<string>("Hello");
        Display<double>(25.75);
        Display<bool>(true);
    }
}

Output:

Value: 100
Type: Int32
Value: Hello
Type: String
Value: 25.75
Type: Double
Value: True
Type: Boolean

The method accepts a value of type T, prints it, and displays the name of the selected type.

Using Type Inference

You do not always need to write the type argument explicitly.

Display(100);
Display("Hello");
Display(25.75);

The compiler infers the type parameter from each argument.

Example: Returning a Value

static T GetFirst<T>(T first, T second)
{
    return first;
}

Usage:

int result1 = GetFirst(10, 20);
string result2 = GetFirst("C#", "Java");

The compiler infers T as int for the first call and string for the second.

This method is intentionally simple: it demonstrates that a generic method can return the same type as its input parameters.

4. Generic Classes in C#

A generic class declares a type parameter at the class level. Its fields, properties, constructors, and methods can use that parameter.

Syntax

public class ClassName<T>
{
    public T Value { get; set; }
}

Example: Generic Storage Class

using System;

public class Storage<T>
{
    private T _value;

    public Storage(T value)
    {
        _value = value;
    }

    public T GetValue()
    {
        return _value;
    }

    public void SetValue(T value)
    {
        _value = value;
    }
}

class Program
{
    static void Main()
    {
        Storage<int> numberStorage = new Storage<int>(500);
        Storage<string> nameStorage = new Storage<string>("Priya");

        Console.WriteLine(numberStorage.GetValue());
        Console.WriteLine(nameStorage.GetValue());

        numberStorage.SetValue(750);

        Console.WriteLine(numberStorage.GetValue());
    }
}

Output:

500
Priya
750

Each constructed type, such as Storage<int> and Storage<string>, uses a different type argument.

This approach is useful when you need reusable storage or processing logic for different types of data.

5. Generic Classes with Multiple Type Parameters

A generic class can declare more than one type parameter.

For example, a class may need to store a product identifier and its associated name.

public class Pair<TFirst, TSecond>
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }

    public Pair(TFirst first, TSecond second)
    {
        First = first;
        Second = second;
    }
}

Usage:

Pair<int, string> product = new Pair<int, string>(101, "Laptop");

Console.WriteLine(product.First);
Console.WriteLine(product.Second);

Output:

101
Laptop

Here, TFirst represents the first value's type, and TSecond represents the second value's type.

Multiple type parameters are useful for pairs, key-value structures, results containing different types, and reusable data-processing components.

6. Generic Constraints in C#

Generic constraints restrict which types can be used as type arguments. They allow the compiler to know that a type parameter supports certain operations or has specific characteristics.

Without constraints, you cannot call arbitrary members on a value of type T just because you expect the type to have them.

C# provides several kinds of generic constraints.

Constraint Purpose
where T : class Requires a reference type
where T : struct Requires a non-nullable value type
where T : notnull Disallows nullable type arguments under the relevant compiler checks
where T : new() Requires a public parameterless constructor
where T : BaseClass Requires the type to derive from a specified base class
where T : IInterface Requires the type to implement a specified interface
where T : U Requires one type parameter to be convertible to another under the constraint rules

The class constraint means a reference type; it does not mean that the type must be a specific class. Likewise, struct excludes nullable value types such as int?.

Example: Using an Interface Constraint

Suppose different classes represent printable documents. You want a generic method that calls Print() on any supported document.

using System;

public interface IPrintable
{
    void Print();
}

public class Invoice : IPrintable
{
    public void Print()
    {
        Console.WriteLine("Printing invoice...");
    }
}

public class Report : IPrintable
{
    public void Print()
    {
        Console.WriteLine("Printing report...");
    }
}

class Program
{
    static void PrintDocument<T>(T document)
        where T : IPrintable
    {
        document.Print();
    }

    static void Main()
    {
        PrintDocument(new Invoice());
        PrintDocument(new Report());
    }
}

Output:

Printing invoice...
Printing report...

The constraint where T : IPrintable ensures that the type argument implements IPrintable. Therefore, the compiler allows the method to call Print().

A class that does not implement IPrintable cannot be used as the type argument for this method.

Example: The New() Constraint

The new() constraint lets generic code create an instance of T using a parameterless constructor.

public class Factory<T>
    where T : new()
{
    public T Create()
    {
        return new T();
    }
}

Usage:

Factory<List<int>> factory = new Factory<List<int>>();

List<int> numbers = factory.Create();
numbers.Add(10);

The type argument must satisfy the new() constraint. In particular, it must have an accessible parameterless constructor and meet the applicable constraint rules.

7. Generic Interfaces in C#

Interfaces can also declare type parameters. Generic interfaces define contracts that can work with different types.

For example, a repository interface might provide a way to retrieve an item by its identifier.

public interface IRepository<T>
{
    void Add(T item);
    T? GetById(int id);
}

The interface declares operations without specifying a concrete entity type.

A class can implement the interface for a particular type:

using System;
using System.Collections.Generic;

public interface IRepository<T>
{
    void Add(T item);
    T? GetById(int id);
}

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

public class StudentRepository : IRepository<Student>
{
    private readonly Dictionary<int, Student> _students = new();

    public void Add(Student item)
    {
        _students.Add(item.Id, item);
    }

    public Student? GetById(int id)
    {
        _students.TryGetValue(id, out Student? student);
        return student;
    }
}

This example uses a dictionary to store students by ID. In a production repository, you would also define appropriate behavior for duplicate IDs and persistence requirements.

Generic interfaces are especially useful in service layers, repository patterns, reusable components, and dependency injection.

8. Generics in Collections

Many commonly used .NET collections are generic.

Examples include:

  • List<T>

  • Dictionary<TKey, TValue>

  • HashSet<T>

  • Queue<T>

  • Stack<T>

These types use type parameters to specify which kinds of values they can store.

Example: Generic List

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 10, 20, 30 };

        numbers.Add(40);

        foreach (int number in numbers)
        {
            Console.WriteLine(number);
        }
    }
}

Because the list is declared as List<int>, the compiler prevents you from adding a string directly to it.

For example, numbers.Add("Hello") produces a compilation error.

This is a key benefit of generics: the collection's intended element type is explicit and checked by the compiler.

9. Generic Methods vs. Generic Classes

Although both use type parameters, generic methods and generic classes solve different problems.

Feature Generic Method Generic Class
Type parameter scope Method Class
Typical use One reusable operation Reusable object or data structure
Type specified when Calling the method, explicitly or by inference Constructing the generic type
Example Display<T>(T value) Storage<T>

Use a generic method when a particular operation should work with different types. Use a generic class when an object itself needs to work with a specified type throughout its lifetime.

A class can also contain generic methods, so these approaches are not mutually exclusive.

10. Practical Example: Generic Result Class

Applications often need to return a result that contains a value and indicates whether an operation succeeded.

A generic result class can represent the value using a type parameter.

using System;

public class Result<T>
{
    public bool Success { get; }
    public T? Value { get; }
    public string Message { get; }

    public Result(bool success, T? value, string message)
    {
        Success = success;
        Value = value;
        Message = message;
    }
}

class Program
{
    static Result<int> FindStudentScore(int studentId)
    {
        if (studentId == 101)
        {
            return new Result<int>(true, 90, "Score found.");
        }

        return new Result<int>(false, default, "Student not found.");
    }

    static void Main()
    {
        Result<int> result = FindStudentScore(101);

        if (result.Success)
        {
            Console.WriteLine($"Score: {result.Value}");
        }
        else
        {
            Console.WriteLine(result.Message);
        }
    }
}

Output:

Score: 90

The same result type can be used with other types, such as Result<string> or Result<Student>. This example uses a simple status and message; a production implementation should define how absent values and error details are represented.

11. Common Mistakes When Using Generics

Mistake 1: Using Members That Are Not Guaranteed by T

This will not compile:

static void Process<T>(T value)
{
    // value.SomeMethod();
}

The compiler does not know whether every possible type argument has a SomeMethod() member.

Solution: Add a suitable interface or base-class constraint if the operation is part of a defined contract.

Mistake 2: Forgetting to Specify Type Arguments

A declaration such as Storage<int> specifies the type argument for the generic class. The type argument must be supplied or inferred where the language supports inference.

Mistake 3: Using Object Instead of Generics Without a Reason

Storing everything as object often requires casting and weakens compile-time type checking.

Generics are usually preferable when the type relationship is known and can be represented by a type parameter.

Mistake 4: Adding Constraints That Are Too Restrictive

Do not add constraints simply because they are available. Add only the constraints required by the operations or behavior of your generic implementation.

Mistake 5: Assuming T Is Always a Reference Type

A generic type parameter may represent a value type or a reference type unless constraints restrict it. Avoid assumptions that apply only to strings or classes.

12. Best Practices for Generics in C#

  • Use generics when multiple data types share the same behavior or structure.

  • Prefer descriptive type parameter names when T is not clear enough.

  • Use constraints to express real requirements, such as implementing an interface.

  • Prefer generic collections over non-generic collections for type-safe data handling.

  • Avoid unnecessary casting when a generic type can express the intended relationship.

  • Keep generic classes and methods focused on a clear responsibility.

  • Avoid overcomplicating simple code with generics when a concrete type is sufficient.

  • Test generic implementations with both value types and reference types when both are supported.

13. Practice Exercises

Exercise 1: Generic Display Method

Create a generic method named DisplayValue<T>() that prints an integer, a string, a decimal value, and a Boolean value.

Exercise 2: Generic Box Class

Create a class Box<T> with a property named Value. Create one object that stores an integer and another that stores a string.

Exercise 3: Generic Pair Class

Create a Pair<TFirst, TSecond> class that stores two values of different types. Test it with an integer and a string.

Exercise 4: Interface Constraint

Create an interface named IPrintable with a Print() method. Implement it in two classes and write a generic method that accepts only types implementing the interface.

Exercise 5: Generic Repository

Create a simple generic repository that stores objects in a list and provides methods to add an item and retrieve all items.

14. Knowledge Check

1. What is the primary purpose of generics in C#?

A. To remove all data types
B. To create reusable, type-safe code
C. To prevent methods from returning values
D. To replace every class with an interface

2. Which symbol commonly represents a generic type parameter?

A. #
B. @
C. T
D. &

3. Which constraint requires a type to implement an interface?

A. where T : IPrintable
B. where T : string
C. where T : void
D. where T : static

4. Which of these is a generic collection?

A. Console
B. List<T>
C. Math
D. DateTime

5. What is one major benefit of generics over storing values as object?

A. They eliminate all runtime errors
B. They make every algorithm constant-time
C. They improve type safety and often reduce casting
D. They prevent the use of classes

Answers

  1. B — To create reusable, type-safe code.

  2. C — T.

  3. A — where T : IPrintable.

  4. B — List<T>.

  5. C — They improve type safety and often reduce casting.

15. Frequently Asked Questions

What are generics in C#?

Generics let developers define classes, methods, interfaces, and other types that work with type parameters, providing reusable code with compile-time type checking.

Why are generics important?

Generics reduce duplicated code, improve type safety, minimize unnecessary casting, and support reusable components.

What does T mean in C# generics?

T is a conventional name for a generic type parameter. It is not a reserved keyword, and developers can choose other meaningful names.

What is a generic class?

A generic class declares type parameters and uses them in its members. For example, Box<T> can store values of different specified types.

What is a generic method?

A generic method declares its own type parameter and performs an operation that can work with different types.

What are generic constraints?

Generic constraints restrict the types that can be used as type arguments. They allow the compiler to verify that the type provides required characteristics or implements required interfaces.

Are List and Dictionary generic collections?

Yes. List<T> uses one type parameter for its elements, while Dictionary<TKey, TValue> uses separate type parameters for keys and values.

Do generics improve performance?

They can improve performance in some scenarios, especially by avoiding boxing of value types and reducing casting. Actual performance depends on the implementation and workload.

Conclusion

Generics are an important C# feature that makes it possible to create reusable components without losing type safety. Generic methods help you reuse operations, generic classes let you build flexible objects, and generic constraints allow you to specify which types your code supports.

By learning to use generics effectively, you can write cleaner, safer, and more maintainable applications across the .NET ecosystem.

In the next tutorial, you will learn about Delegates and Events in C#: A Beginner-Friendly Tutorial, including how methods can be passed as values and how objects can communicate through event notifications.

←
Post 22 of 30
→