Generics in C#: A Beginner's Guide

Generics in C#: A Beginner's Guide

28 Oct 2024
Beginner
492 Views
42 min read
Learn with an interactive course and practical hands-on labs

Best Free C# Course: Learn C# In 21 Days

Generics in C#

Generics in C# are incredibly useful for developers who need to write flexible, reusable code that works with any data type while maintaining type safety. Imagine being able to create a single method, class, or interface that can work with multiple types, such as integers, strings, or custom objects, without rewriting the logic. Generics allow you to handle different data types in a more organized, efficient way, reducing code duplication and enhancing maintainability.

In this C# tutorial, we will learn how Generics work and explore their key features. You'll discover how to create generic classes, methods, and collections to enhance code flexibility and type safety.

What Are Generics in C#?

  • In C#, generics allow you to write code that can work with any data type, such as custom objects, strings, or ints, without having to write the same code for each kind. Because of this, your code is more adaptable and reusable.
  • Generics help keep your code safe by ensuring you use the right types and prevent errors that could happen when using the wrong type.
  • You can define generics using angle brackets < > and specify the type later when you use the generic class or method.
For example, you can create a generic list that works with any type of data, making it easy to store and manage values without repeating code.

Why Generics Are Important?

So, why are generics necessary? Let’s say you need a collection that holds integers, strings, and custom objects. You could use older non-generic collections like ArrayList, but they come with a major drawback: they aren’t type-safe.
This means you might accidentally add an item of the wrong type, causing errors at runtime. Generics fix this by letting you define what type your collection or class should hold, ensuring type safety and reducing bugs.

Core Concepts of Generic in C#

Generics are a valuable feature in C# that allows you to define classes, methods, delegates, and interfaces with placeholders for the types on which they operate. The following are the fundamental concepts of generics in C#:

1. Generic Class

  • A generic class in C# lets you define a class with a placeholder (called a type parameter) that works with multiple data types. Think of it like a cookie cutter that can make different shapes while using the same cutter.

Here’s a simple example


using System;
public class Box<T>
{
    public T Item { get; set; }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        var amitBox = new Box<int> { Item = 10 };
        Console.WriteLine("${amitBox.Item} is stored in Amit's box.");

        var priyaBox = new Box<string> { Item = "Hello, World!" };
        Console.WriteLine("${priyaBox.Item} is stored in Priya's box.");
    }
}

Output


10 is stored in Amit's box.
Hello, World! is stored in Priya's box.

Explanation

  • The Box<T> class allows us to store any type in it. The <T> is a placeholder for the type we define later.
  • We create instances of Box<int> and Box<string>, which means Box will only accept integers and strings, respectively.

2. Generic Method

  • You can also create generic methods that can handle different data types without repeating code.

using System;

public class Program
{
    public void Display<T>(T message)
    {
        Console.WriteLine(message);
    }

    static void Main(string[] args)
    {
        var program = new Program();
        program.Display(123);
        program.Display("Hello!");
        program.Display(45.67);
    }
}

Output


123
Hello!
45.67

Explanation

  • The Display<T> method can handle any data type by specifying the type parameter <T>.
  • In the Main method, the same Display method is used to print an integer, a string, and a double.

3. Generic Interfaces

  • Generic interfaces allow you to create flexible and reusable contracts that can be implemented by classes using different data types.
  • Let’s explore an example where we define a generic repository interface.

using System;
using System.Collections.Generic;

public interface IRepository<T>
{
    void Add(T item);
    List<T> GetAll();
}

public class Repository<T> : IRepository<T>
{
    private List<T> items = new List<T>();

    public void Add(T item) => items.Add(item);

    public List<T> GetAll() => items;
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        IRepository<int> intRepo = new Repository<int>();
        intRepo.Add(5);
        Console.WriteLine("Repository contains:");
        foreach (int item in intRepo.GetAll())
        {
            Console.WriteLine(item);
        }
    }
}

Output

Repository contains:
5

Explanation

  • The IRepository<T> interface defines a contract for adding items and getting all items from a collection.
  • The Repository<T> class implements this interface and provides concrete functionality for adding and retrieving items.
  • In the Main method, we create a repository for integers and add a value to it, then print the value stored in the repository.

4. Constraints in Generics

Constraints help you limit the types that can be used as type parameters. For instance, you can enforce that a type must be a class or implement a specific interface.
using System;

public class Manager<T> where T : class
{
    public T Entity { get; set; }
}

Explanation

  • In this example, the Manager<T> class uses the where T: class constraint, ensuring that the type parameter T must be a reference type (class).
  • This makes sure that only reference types can be used, adding a layer of type safety to the generic class.

Real-Life Examples of Generics

Generics are commonly used in different scenarios:

1. Collections and Data Structures

  • Generics make collections like List<T>, Dictionary<TKey, TValue>, Queue<T>, and Stack<T> more efficient and type-safe. Let’s take a look:

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4 };
        Dictionary<int, string> students = new Dictionary<int, string>
        {
            { 1, "Amit" },
            { 2, "Priya" }
        };

        Console.WriteLine(numbers[0]);
        Console.WriteLine(students[1]);
    }
}

Output


1
Amit

Explanation

  • The List<int> stores a collection of integers, and the Dictionary<int, string> stores key-value pairs where the keys are integers, and the values are strings.
  • We print the first item in the list and the value associated with the key 1 in the dictionary.

2. Database Access

  • You can use generics to create reusable methods for accessing databases:

using System;
using System.Collections.Generic;

public class Repository<T>
{
    private List<T> items = new List<T>();

    public void Add(T item) => items.Add(item);
    public List<T> GetAll() => items;
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        Repository<int> intRepository = new Repository<int>();
        intRepository.Add(1);
        intRepository.Add(2);
        List<int> allInts = intRepository.GetAll();
        Console.WriteLine("All integers in the repository:");
        foreach (int i in allInts)
        {
            Console.WriteLine(i);
        }
    }
}

Output


All integers in the repository:
1
2

Explanation

  • The Repository<T> class can store and retrieve data of any type <T>.
  • In the Main method, we create a repository for integers, add two integers, and retrieve all the integers stored in the repository.

3. Constraints in Generics

  • Sometimes, you want to restrict the types that can be used in a generic class or method.
  • This is where constraints come into play.
  • You can specify that the type parameter must implement a specific interface, inherit a particular class, or have a default constructor.

Let’s look at an example:


using System;

public class Person
{
    public string Name { get; set; }
}

public class Employee : Person
{
    public int EmployeeId { get; set; }
}

public class EmployeeRepository<T> where T : Person
{
    public void Add(T person)
    {
        Console.WriteLine("${person.Name} added!");
    }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        var employeeRepo = new EmployeeRepository<Employee>();
        employeeRepo.Add(new Employee { Name = "John", EmployeeId = 101 });
    }
}

Output

John added!

Explanation

  • The EmployeeRepository<T> class is constrained by the where T: Person constraint, meaning it only accepts types that inherit from the Person class.
  • In the Main method, we add an Employee object to the EmployeeRepository class.

Features of Generics in C#

Generics provide several powerful features that enhance performance, maintainability, and type safety.

Features of Generics in C#

  1. Type Safety: Generics ensure that only a specific type can be used, catching errors at compile time.
  2. Reusability: You can write generic code once and reuse it for different types without duplicating logic.
  3. Performance: Generics improve performance by avoiding unnecessary boxing and unboxing, which enhances execution speed and memory usage.

1. Type Safety

  • Generics provide type safety, ensuring that only a specific type can be used.
  • This reduces runtime errors because incorrect types can’t be accidentally added to a generic collection or passed to a method.

Example: Type Safety in Generic List


using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        List<int> numberList = new List<int> { 1, 2, 3 };
        // numberList.Add("Hello"); // This line will cause a compile-time error

        Console.WriteLine(numberList[0]); // Output: 1
    }
}

Output

1

Explanation

  • You can’t accidentally add a string to numberList because it only accepts integers.
  • C# enforces this rule at compile time, catching the error early and preventing potential runtime issues.

2. Performance

  • Generics improve performance by avoiding unnecessary boxing and unboxing of value types.
  • Boxing occurs when a value type is converted to an object, and unboxing occurs when an object is converted back to a value type.
  • Generics eliminate this need, making your code faster.

Example: Avoiding Boxing with Generics


using System;
using System.Collections;

class Program
{
    static void Main(string[] args)
    {
        // Without Generics
        ArrayList list = new ArrayList();
        list.Add(10); // Boxing happens
        int number = (int)list[0]; // Unboxing happens

        // With Generics
        List<int> genericList = new List<int> { 10 };
        int genericNumber = genericList[0]; // No boxing or unboxing
    }
}

Explanation

  • Using a List<int> avoids boxing and unboxing, which improves memory usage and execution speed.
  • Boxing occurs when a value type is converted to an object, and unboxing occurs when an object is converted back to a value type.

3. Binary Code Reuse

  • Generics allow you to write code that works with different types without repeating yourself.
  • This means less code duplication and easier maintenance.

Example: Reusable Generic Class


using System;

public class Pair<T1, T2>
{
    public T1 First { get; set; }
    public T2 Second { get; set; }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        var intStringPair = new Pair<int, string> { First = 1, Second = "One" };
        Console.WriteLine(${intStringPair.First}, {intStringPair.Second}); // Output: 1, One
    }
}

Output

1, One

Explanation

  • You create a generic class Pair<T1, T2> that can hold two related values of different types.
  • This allows you to avoid writing separate classes for each possible combination of types.

Role of Collections in Generics: Dictionary, Queue, and Stack

  • In C#, collections like Dictionary, Queue, and Stack utilize generics to enhance code flexibility and type safety.
  • These data structures allow developers to create type-safe collections that efficiently manage data while minimizing runtime errors.

1. Dictionary in C#

  • A Dictionary in C# is a collection that stores key-value pairs.
  • It allows fast lookups by key and enforces uniqueness for the keys.

Example: Dictionary Usage


using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary students = new Dictionary
        {
            { 1, "Amit" },
            { 2, "Priya" }
        };
        
        Console.WriteLine(students[1]); // Output: Amit
        Console.WriteLine(students[2]); // Output: Priya
    }
}

Output

Amit
Priya

Explanation

  • A Dictionary allows you to store key-value pairs. The keys must be unique, and in this example, the key is the student's ID, and the value is their name.

Queues in C#

  • A Queue in C# is a first-in, first-out (FIFO) collection that processes items in the order they were added.

Example: Queue Usage

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Queue queue = new Queue();
        queue.Enqueue("First");
        queue.Enqueue("Second");
        queue.Enqueue("Third");
        
        Console.WriteLine(queue.Dequeue()); // Output: First
        Console.WriteLine(queue.Dequeue()); // Output: Second
    }
}

Output

First
Second

Explanation

  • A Queue processes elements in the order they were added.
  • Here, items are added using Enqueue() and removed using Dequeue().

3. Stacks in C#

  • A Stack in C# is a last-in, first-out (LIFO) collection.
  • The last element added is the first one to be removed.

Example: Stack Usage

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Stack stack = new Stack();
        stack.Push("First");
        stack.Push("Second");
        stack.Push("Third");
        
        Console.WriteLine(stack.Pop()); // Output: Third
        Console.WriteLine(stack.Pop()); // Output: Second
    }
}

Output

Third
Second

Explanation

  • A Stack follows a last-in, first-out order.
  • Items are added using Push() and removed using Pop().

    Advantages of Generics in C#

    • Reusability: You write a generic class or method once, and it works with multiple data types.
    • Type Safety: You prevent errors by enforcing type checks during compile time.
    • Performance: Generics reduce the need for type conversions, improving speed and efficiency.

    Common Pitfalls and Best Practices of Generic in C#

    • Overusing Generics: Not every problem requires a generic solution. Use them only where necessary.
    • Ignoring Constraints: Applying constraints makes your code safer and easier to use.
    Read More: Top 50 C# Interview Questions & Answers To Get Hired
    Summary
    C# generics offer a strong tool for creating adaptable and reusable code. Generics lets you reduce redundancy and guarantee type safety while working with collections, building custom data structures, or implementing methods and classes that function with different data types. Generics are a very useful tool for producing clear, effective, and maintainable code as you continue to code.

    Take advantage of ScholarHat's extensive C# Programming Course to improve your C# proficiency! Develop your coding skills, learn more about complex ideas like reflection, and master the creation of dynamic apps. Take your programming career to the next level by enrolling in our course now!

    FAQs

    The purpose of generics in C# is to let you create flexible, reusable code that can work with any data type while ensuring type safety. It helps you avoid runtime errors by enforcing type constraints at compile time, making your code more reliable and efficient.

    Generics are type-safe in C# because they allow you to define classes, methods, or interfaces without specifying an exact data type. This means you can enforce type constraints at compile time, preventing invalid data types from being used. It ensures that you work with the correct types, reducing runtime errors and making your code more reliable.

    The benefits of generics include type safety, code reusability, and better performance. Generics help you catch type-related errors at compile time, reducing runtime issues. They allow you to write flexible code that works with different data types without duplication. Additionally, generics improve performance by avoiding unnecessary boxing and unboxing of value types.

    Take our Csharp skill challenge to evaluate yourself!

    In less than 5 minutes, with our skill challenge, you can identify your knowledge gaps and strengths in a given skill.

    GET FREE CHALLENGE

    Share Article
    About Author
    Shailendra Chauhan (Microsoft MVP, Founder & CEO at Scholarhat by DotNetTricks)

    Shailendra Chauhan, Founder and CEO of ScholarHat by DotNetTricks, is a renowned expert in System Design, Software Architecture, Azure Cloud, .NET, Angular, React, Node.js, Microservices, DevOps, and Cross-Platform Mobile App Development. His skill set extends into emerging fields like Data Science, Python, Azure AI/ML, and Generative AI, making him a well-rounded expert who bridges traditional development frameworks with cutting-edge advancements. Recognized as a Microsoft Most Valuable Professional (MVP) for an impressive 9 consecutive years (2016–2024), he has consistently demonstrated excellence in delivering impactful solutions and inspiring learners.

    Shailendra’s unique, hands-on training programs and bestselling books have empowered thousands of professionals to excel in their careers and crack tough interviews. A visionary leader, he continues to revolutionize technology education with his innovative approach.
    Accept cookies & close this