Skip to main content

C# Generics Basics

Introduction

Generics are one of the most powerful features in C#, introduced in C# 2.0. They allow you to define type-safe data structures without committing to specific data types. This means you can create classes, interfaces, methods, and delegates that can work with any data type while maintaining type safety.

In this guide, we'll explore the basics of C# generics, why they're useful, and how to implement them in your code.

Why Generics?

Before diving into the syntax and usage, let's understand why generics exist in the first place:

  1. Type Safety: Generics provide compile-time type checking, reducing the risk of runtime errors.
  2. Code Reusability: Write code once that works with multiple data types.
  3. Performance: Generics avoid boxing/unboxing operations for value types, improving performance.
  4. Cleaner Code: Reduces the need for type casting and the potential for casting exceptions.

Generic Classes

Let's start by creating a simple generic class that can store an item of any type.

csharp
public class Box<T>
{
private T item;

public void SetItem(T newItem)
{
item = newItem;
}

public T GetItem()
{
return item;
}
}

In this example, T is a type parameter that represents a placeholder for a specific type that will be provided later. Here's how you can use this generic class:

csharp
// Create a Box for integers
Box<int> intBox = new Box<int>();
intBox.SetItem(123);
int number = intBox.GetItem(); // Returns 123
Console.WriteLine($"Integer value: {number}");

// Create a Box for strings
Box<string> stringBox = new Box<string>();
stringBox.SetItem("Hello, Generics!");
string message = stringBox.GetItem(); // Returns "Hello, Generics!"
Console.WriteLine($"String value: {message}");

Output:

Integer value: 123
String value: Hello, Generics!

Generic Methods

You can also create generic methods within non-generic classes:

csharp
public class Utilities
{
public static void Swap<T>(ref T first, ref T second)
{
T temp = first;
first = second;
second = temp;
}

public static bool AreEqual<T>(T first, T second)
{
return first.Equals(second);
}
}

Here's how to use these generic methods:

csharp
int a = 5, b = 10;
Console.WriteLine($"Before swap: a = {a}, b = {b}");

Utilities.Swap<int>(ref a, ref b);
Console.WriteLine($"After swap: a = {a}, b = {b}");

string str1 = "Hello";
string str2 = "Hello";
bool areEqual = Utilities.AreEqual<string>(str1, str2);
Console.WriteLine($"Strings equal: {areEqual}");

// Type inference - the compiler can deduce the type
Utilities.Swap(ref a, ref b); // No need to specify <int>

Output:

Before swap: a = 5, b = 10
After swap: a = 10, b = 5
Strings equal: True

Generic Constraints

Sometimes you want to restrict the types that can be used with your generic class or method. C# allows you to apply constraints on type parameters using the where keyword:

csharp
// T must be a class (reference type)
public class Repository<T> where T : class
{
// Implementation
}

// T must have a parameterless constructor
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}

// Multiple constraints: T must inherit from Animal and implement ICloneable
public class AnimalProcessor<T> where T : Animal, ICloneable
{
// Implementation
}

Common constraints include:

  • where T : struct - T must be a value type
  • where T : class - T must be a reference type
  • where T : new() - T must have a parameterless constructor
  • where T : BaseClass - T must inherit from BaseClass
  • where T : IInterface - T must implement IInterface

Practical Example: Generic Collection

Let's create a simple generic list implementation to understand generics better:

csharp
public class SimpleList<T>
{
private T[] items;
private int count;

public SimpleList(int capacity = 4)
{
items = new T[capacity];
count = 0;
}

public void Add(T item)
{
if (count == items.Length)
{
// Resize array if needed
T[] newItems = new T[items.Length * 2];
Array.Copy(items, newItems, items.Length);
items = newItems;
}

items[count] = item;
count++;
}

public T GetAt(int index)
{
if (index < 0 || index >= count)
{
throw new IndexOutOfRangeException();
}

return items[index];
}

public int Count => count;
}

Using our generic list:

csharp
SimpleList<double> grades = new SimpleList<double>();
grades.Add(92.5);
grades.Add(89.0);
grades.Add(95.8);

Console.WriteLine($"Number of grades: {grades.Count}");
Console.WriteLine($"First grade: {grades.GetAt(0)}");

// Using with a different type
SimpleList<string> names = new SimpleList<string>();
names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");

for (int i = 0; i < names.Count; i++)
{
Console.WriteLine($"Name {i+1}: {names.GetAt(i)}");
}

Output:

Number of grades: 3
First grade: 92.5
Name 1: Alice
Name 2: Bob
Name 3: Charlie

Real-World Application: Generic Repository Pattern

A common use of generics in real-world applications is the Repository Pattern in database operations. Here's a simplified example:

csharp
public interface IRepository<T> where T : class
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}

public class Repository<T> : IRepository<T> where T : class
{
private readonly List<T> _entities = new List<T>();
// In a real application, this would connect to a database

public T GetById(int id)
{
// Simplified implementation
return _entities.FirstOrDefault();
}

public IEnumerable<T> GetAll()
{
return _entities;
}

public void Add(T entity)
{
_entities.Add(entity);
}

public void Update(T entity)
{
// Implementation
}

public void Delete(int id)
{
// Implementation
}
}

Usage with different entity types:

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

// Using the generic repository
IRepository<User> userRepository = new Repository<User>();
userRepository.Add(new User { Id = 1, Name = "John Doe" });

IRepository<Product> productRepository = new Repository<Product>();
productRepository.Add(new Product { Id = 1, Name = "Laptop", Price = 999.99m });

Generic Type Inference

C# can often infer the generic type arguments from the method parameters, which makes your code cleaner:

csharp
public class GenericHelper
{
public static void Display<T>(T item)
{
Console.WriteLine($"Type: {typeof(T).Name}, Value: {item}");
}
}

// These calls are equivalent:
GenericHelper.Display<int>(42);
GenericHelper.Display(42); // Type inference in action

Multiple Type Parameters

Generic classes and methods can have multiple type parameters:

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

public override string ToString()
{
return $"({First}, {Second})";
}
}

// Usage
Pair<int, string> entry = new Pair<int, string>(1, "One");
Console.WriteLine(entry); // Output: (1, One)

Pair<string, bool> setting = new Pair<string, bool>("IsEnabled", true);
Console.WriteLine(setting); // Output: (IsEnabled, True)

Default Values in Generics

When you need a default value for a generic type, use the default keyword:

csharp
public static T GetDefaultValue<T>()
{
return default(T); // For reference types: null, for value types: their default
}

int defaultInt = GetDefaultValue<int>(); // Returns 0
string defaultString = GetDefaultValue<string>(); // Returns null

Summary

In this guide, we've learned:

  • What generics are and why they're useful
  • How to create generic classes with type parameters
  • How to write generic methods
  • How to apply constraints to type parameters
  • Real-world applications of generics like collections and repositories
  • How type inference works with generics
  • Creating generics with multiple type parameters

Generics are a cornerstone feature in C# that enables you to write flexible, reusable, and type-safe code. By mastering generics, you'll be able to create more elegant and efficient solutions to common programming problems.

Exercise Ideas

  1. Create a generic Stack<T> class with Push, Pop, and Peek methods.
  2. Implement a generic Calculator<T> class that can perform basic arithmetic on different numeric types.
  3. Write a generic method that can find the maximum value in an array of any comparable type.
  4. Create a generic KeyValuePair<TKey, TValue> class similar to the one in .NET.
  5. Implement a generic Filter<T> method that returns elements from a list that satisfy a given predicate.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)