Skip to main content

C# Type Parameters

Type parameters are the cornerstone of generic programming in C#. They allow you to write flexible, reusable code that works with different data types while maintaining type safety at compile time. In this tutorial, we'll explore how type parameters work in C# generics and how you can use them effectively in your code.

What are Type Parameters?

Type parameters act as placeholders for specific types that will be provided later when the generic class, method, interface, or delegate is used. They enable you to define code that can work with any type, rather than being limited to a single specific type.

In C#, type parameters are denoted by angle brackets (<>) and typically use single uppercase letters as convention:

csharp
public class Container<T>
{
public T Item { get; set; }
}

In this example, T is a type parameter that will be replaced with an actual type when the Container class is instantiated.

Basic Syntax for Type Parameters

Type parameters can be applied to:

  1. Classes and structs
  2. Interfaces
  3. Methods
  4. Delegates

Generic Classes and Structs

Here's how to define a generic class:

csharp
public class GenericClass<T>
{
public T Data { get; set; }

public GenericClass(T data)
{
Data = data;
}

public T GetData()
{
return Data;
}
}

To use this class, you specify the concrete type:

csharp
// Create a GenericClass that works with strings
GenericClass<string> stringContainer = new GenericClass<string>("Hello Generics!");
string greeting = stringContainer.GetData();
Console.WriteLine(greeting); // Output: Hello Generics!

// Create a GenericClass that works with integers
GenericClass<int> intContainer = new GenericClass<int>(42);
int number = intContainer.GetData();
Console.WriteLine(number); // Output: 42

Generic Methods

You can define generic methods within both generic and non-generic classes:

csharp
public class Utilities
{
// Generic method with type parameter T
public T[] CreateArray<T>(T firstItem, T secondItem)
{
return new T[] { firstItem, secondItem };
}
}

When calling generic methods, you can either explicitly specify the type or let the compiler infer it:

csharp
Utilities utils = new Utilities();

// Explicitly specifying the type
string[] names = utils.CreateArray<string>("Alice", "Bob");

// Type inference - compiler determines T is int
int[] numbers = utils.CreateArray(1, 2);

Console.WriteLine($"Names: {names[0]}, {names[1]}"); // Output: Names: Alice, Bob
Console.WriteLine($"Numbers: {numbers[0]}, {numbers[1]}"); // Output: Numbers: 1, 2

Multiple Type Parameters

You can define multiple type parameters when you need to work with more than one generic type:

csharp
public class KeyValuePair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }

public KeyValuePair(TKey key, TValue value)
{
Key = key;
Value = value;
}
}

Usage example:

csharp
// Use string keys and int values
KeyValuePair<string, int> studentScore = new KeyValuePair<string, int>("John", 95);
Console.WriteLine($"{studentScore.Key}'s score: {studentScore.Value}");
// Output: John's score: 95

// Use int keys and bool values
KeyValuePair<int, bool> taskComplete = new KeyValuePair<int, bool>(1234, true);
Console.WriteLine($"Task {taskComplete.Key} completed: {taskComplete.Value}");
// Output: Task 1234 completed: True

Naming Type Parameters

While single-letter type parameters like T are common, it's often more readable to use descriptive names, especially when working with multiple type parameters. The convention is to prefix type parameter names with "T":

csharp
public class Repository<TEntity, TId> where TEntity : class where TId : struct
{
public TEntity GetById(TId id)
{
// Implementation here
return default;
}
}

Common naming conventions include:

  • T for a general type parameter
  • TKey, TValue for dictionary-like structures
  • TResult for return types
  • TEntity for entity or model types
  • TElement for collection element types

Type Parameter Constraints

Sometimes you need to restrict what types can be used with your generic code. Type parameter constraints allow you to specify requirements for the types that can be used:

csharp
public class DataProcessor<T> where T : IComparable
{
public T FindMaximum(T[] items)
{
if (items == null || items.Length == 0)
throw new ArgumentException("Array cannot be empty");

T max = items[0];
foreach (T item in items)
{
if (item.CompareTo(max) > 0)
max = item;
}
return max;
}
}

In this example, we've constrained T to types that implement IComparable, which ensures that the CompareTo method is available.

Let's see it in action:

csharp
DataProcessor<int> intProcessor = new DataProcessor<int>();
int[] numbers = { 5, 12, 8, 3, 23, 1 };
int max = intProcessor.FindMaximum(numbers);
Console.WriteLine($"Maximum value: {max}"); // Output: Maximum value: 23

DataProcessor<string> stringProcessor = new DataProcessor<string>();
string[] names = { "Alice", "Bob", "Charlie", "David" };
string longest = stringProcessor.FindMaximum(names);
Console.WriteLine($"Alphabetically last name: {longest}"); // Output: Alphabetically last name: David

Real-World Example: Generic Repository

A common use of generics in real-world applications is the repository pattern for data access:

csharp
// Define a base entity interface
public interface IEntity<TId>
{
TId Id { get; set; }
}

// Generic repository interface
public interface IRepository<TEntity, TId> where TEntity : class, IEntity<TId>
{
IEnumerable<TEntity> GetAll();
TEntity GetById(TId id);
void Add(TEntity entity);
void Update(TEntity entity);
void Delete(TId id);
}

// Concrete entity class
public class Product : IEntity<int>
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

// Repository implementation
public class ProductRepository : IRepository<Product, int>
{
private List<Product> _products = new List<Product>();

public IEnumerable<Product> GetAll()
{
return _products;
}

public Product GetById(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}

public void Add(Product entity)
{
_products.Add(entity);
}

public void Update(Product entity)
{
var index = _products.FindIndex(p => p.Id == entity.Id);
if (index != -1)
_products[index] = entity;
}

public void Delete(int id)
{
_products.RemoveAll(p => p.Id == id);
}
}

Using the repository:

csharp
// Creating and using the repository
ProductRepository repo = new ProductRepository();

// Add products
repo.Add(new Product { Id = 1, Name = "Laptop", Price = 1200.00m });
repo.Add(new Product { Id = 2, Name = "Mouse", Price = 25.50m });

// Get and display products
foreach (var product in repo.GetAll())
{
Console.WriteLine($"Product: {product.Name}, Price: ${product.Price}");
}

// Get specific product
Product laptop = repo.GetById(1);
Console.WriteLine($"Found product: {laptop.Name}");

// Update product
laptop.Price = 1299.99m;
repo.Update(laptop);

// Delete product
repo.Delete(2);

Type Parameter Default Values

When working with type parameters, you might need to return a default value:

csharp
public class GenericDefault<T>
{
public T GetDefaultValue()
{
return default(T);
}
}

The default keyword returns the default value for the type parameter:

  • For reference types: null
  • For numeric types: 0
  • For bool: false
  • For char: '\0'
  • For structs: A struct with all fields set to their default values
csharp
GenericDefault<int> defaultInt = new GenericDefault<int>();
Console.WriteLine($"Default int value: {defaultInt.GetDefaultValue()}"); // Output: Default int value: 0

GenericDefault<string> defaultString = new GenericDefault<string>();
Console.WriteLine($"Default string value: {defaultString.GetDefaultValue() ?? "null"}"); // Output: Default string value: null

GenericDefault<bool> defaultBool = new GenericDefault<bool>();
Console.WriteLine($"Default bool value: {defaultBool.GetDefaultValue()}"); // Output: Default bool value: False

Summary

Type parameters are a powerful feature in C# that allow you to write flexible and reusable code while maintaining type safety. They enable you to create generic classes, interfaces, methods, and delegates that can work with multiple types without sacrificing performance or type checking.

Key points to remember:

  • Type parameters act as placeholders for actual types
  • They are defined using angle brackets (<>)
  • They can be applied to classes, structs, interfaces, methods, and delegates
  • You can use constraints to restrict what types can be used
  • Descriptive names make your code more readable
  • Type parameters enable code reuse without sacrificing type safety

Exercises

  1. Create a generic Stack<T> class that provides Push, Pop, and Peek methods.
  2. Implement a generic Pair<T1, T2> class that holds two values of potentially different types.
  3. Write a generic method Swap<T> that exchanges the values of two variables of the same type.
  4. Create a generic class Calculator<T> with constraints that allow it to perform addition, subtraction, multiplication, and division on numeric types.
  5. Implement a generic Repository<TEntity> pattern for a specific domain model (e.g., for a bookstore with books, authors, and customers).

Additional Resources



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