Skip to main content

C# Generic Methods

In this tutorial, you'll learn about generic methods in C#, a powerful feature that allows you to write flexible, reusable, and type-safe code. Generic methods are an essential part of C# programming and understanding them will significantly improve your coding skills.

Introduction to Generic Methods

Generic methods allow you to define a method that can work with any data type while maintaining type safety. Unlike non-generic methods, generic methods don't require you to create separate implementations for different data types or use object types that require casting.

A generic method is defined with one or more type parameters that are used within the method's signature and body. These type parameters are specified at the time of method invocation.

Why Use Generic Methods?

Before diving into how generic methods work, let's understand why they're valuable:

  1. Type Safety: Compiler catches type errors at compile time, not runtime
  2. Code Reusability: Write once, use with many different types
  3. Performance: Avoid boxing/unboxing operations that occur with object types
  4. Maintainability: Less duplicate code means easier maintenance
  5. Readability: Code intentions are clearer with proper type parameters

Basic Syntax of Generic Methods

Here's the basic syntax of a generic method in C#:

csharp
access_modifier static_modifier return_type MethodName<T>(T parameter)
{
// Method implementation using T
}

Where:

  • access_modifier is the visibility of the method (public, private, etc.)
  • static_modifier indicates if the method is static (optional)
  • return_type is the data type returned by the method
  • MethodName is the name of the method
  • <T> is the type parameter (can be multiple, separated by commas)
  • T parameter is the parameter of type T

Your First Generic Method

Let's create a simple generic method that displays the value passed to it:

csharp
using System;

class Program
{
// Generic method that displays any type of data
static void Display<T>(T value)
{
Console.WriteLine($"Type: {typeof(T).Name}, Value: {value}");
}

static void Main(string[] args)
{
Display<int>(10);
Display<double>(10.5);
Display<string>("Hello Generics!");
Display<bool>(true);

// Type inference - compiler determines the type automatically
Display(20); // int is inferred
Display("C# is fun!"); // string is inferred
}
}

Output:

Type: Int32, Value: 10
Type: Double, Value: 10.5
Type: String, Value: Hello Generics!
Type: Boolean, Value: True
Type: Int32, Value: 20
Type: String, Value: C# is fun!

Notice how the same method works with different data types. Also, in the later examples, we didn't explicitly specify the type parameter because C# can infer the type from the arguments passed.

Multiple Type Parameters

A generic method can have multiple type parameters when needed:

csharp
using System;

class Program
{
// Generic method with two type parameters
static void DisplayPair<TFirst, TSecond>(TFirst first, TSecond second)
{
Console.WriteLine($"First: {first} ({typeof(TFirst).Name})");
Console.WriteLine($"Second: {second} ({typeof(TSecond).Name})");
}

static void Main(string[] args)
{
DisplayPair<int, string>(10, "Ten");
DisplayPair<bool, double>(true, 99.9);

// Type inference works here too
DisplayPair("Key", 100); // TFirst is string, TSecond is int
}
}

Output:

First: 10 (Int32)
Second: Ten (String)
First: True (Boolean)
Second: 99.9 (Double)
First: Key (String)
Second: 100 (Int32)

Generic Methods with Generic Return Types

Generic methods can also return the specified generic type:

csharp
using System;

class Program
{
// Generic method returning the same type
static T Echo<T>(T value)
{
Console.WriteLine($"Echoing: {value}");
return value;
}

static void Main(string[] args)
{
int intResult = Echo<int>(42);
Console.WriteLine($"Returned value: {intResult}");

string stringResult = Echo("Hello world"); // Type inference
Console.WriteLine($"Returned value: {stringResult}");
}
}

Output:

Echoing: 42
Returned value: 42
Echoing: Hello world
Returned value: Hello world

Generic Constraints

Sometimes you want to restrict which types can be used with your generic method. This is where constraints come in handy:

csharp
using System;

class Program
{
// Generic method constrained to reference types
static void DisplayReference<T>(T value) where T : class
{
Console.WriteLine($"Reference type: {value}");
}

// Generic method constrained to value types
static void DisplayValue<T>(T value) where T : struct
{
Console.WriteLine($"Value type: {value}");
}

// Generic method constrained to types implementing IComparable
static T GetLarger<T>(T first, T second) where T : IComparable<T>
{
if (first.CompareTo(second) > 0)
return first;
return second;
}

static void Main(string[] args)
{
// Works with reference types
DisplayReference<string>("Hello");

// Works with value types
DisplayValue<int>(42);

// Works with types implementing IComparable<T>
int largerInt = GetLarger<int>(10, 20);
Console.WriteLine($"Larger int: {largerInt}");

string largerString = GetLarger("apple", "banana");
Console.WriteLine($"Larger string: {largerString}");
}
}

Output:

Reference type: Hello
Value type: 42
Larger int: 20
Larger string: banana

Common constraints include:

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

Practical Examples

Example 1: Generic Swap Method

csharp
using System;

class Program
{
// Generic swap method
static void Swap<T>(ref T first, ref T second)
{
T temp = first;
first = second;
second = temp;
}

static void Main(string[] args)
{
int a = 10, b = 20;
Console.WriteLine($"Before swap: a = {a}, b = {b}");

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

string firstName = "John", lastName = "Doe";
Console.WriteLine($"Before swap: firstName = {firstName}, lastName = {lastName}");

Swap(ref firstName, ref lastName); // Type inference
Console.WriteLine($"After swap: firstName = {firstName}, lastName = {lastName}");
}
}

Output:

Before swap: a = 10, b = 20
After swap: a = 20, b = 10
Before swap: firstName = John, lastName = Doe
After swap: firstName = Doe, lastName = John

Example 2: Generic Repository Pattern

A more real-world application - implementing a simple generic repository:

csharp
using System;
using System.Collections.Generic;

// Sample entity classes
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }

public override string ToString()
{
return $"Product {Id}: {Name} (${Price})";
}
}

public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }

public override string ToString()
{
return $"Customer {Id}: {Name} ({Email})";
}
}

// Generic repository interface
public interface IRepository<T>
{
void Add(T item);
void Remove(T item);
T GetById(int id);
IEnumerable<T> GetAll();
}

// Generic repository implementation
public class Repository<T> : IRepository<T> where T : class
{
// Simulate a database with a list
private List<T> _items = new List<T>();
private Func<T, int> _getIdFunc;

public Repository(Func<T, int> getIdFunc)
{
_getIdFunc = getIdFunc;
}

public void Add(T item)
{
_items.Add(item);
}

public void Remove(T item)
{
_items.Remove(item);
}

public T GetById(int id)
{
return _items.Find(item => _getIdFunc(item) == id);
}

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

class Program
{
static void Main(string[] args)
{
// Create a product repository
var productRepo = new Repository<Product>(p => p.Id);

// Add products
productRepo.Add(new Product { Id = 1, Name = "Laptop", Price = 999.99m });
productRepo.Add(new Product { Id = 2, Name = "Phone", Price = 499.99m });

// Get and display all products
Console.WriteLine("All Products:");
foreach (var product in productRepo.GetAll())
{
Console.WriteLine(product);
}

// Get product by id
var laptop = productRepo.GetById(1);
Console.WriteLine($"\nFound product: {laptop}");

// Create a customer repository
var customerRepo = new Repository<Customer>(c => c.Id);

// Add customers
customerRepo.Add(new Customer { Id = 1, Name = "Alice", Email = "[email protected]" });
customerRepo.Add(new Customer { Id = 2, Name = "Bob", Email = "[email protected]" });

// Get and display all customers
Console.WriteLine("\nAll Customers:");
foreach (var customer in customerRepo.GetAll())
{
Console.WriteLine(customer);
}
}
}

Output:

All Products:
Product 1: Laptop ($999.99)
Product 2: Phone ($499.99)

Found product: Product 1: Laptop ($999.99)

All Customers:
Customer 1: Alice ([email protected])
Customer 2: Bob ([email protected])

This example demonstrates how generic methods and classes can be used to create reusable components like a repository that works with different entity types.

Generic Methods vs Generic Classes

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

csharp
using System;

// Non-generic class with generic method
public class Utility
{
public T GetDefault<T>()
{
return default(T);
}
}

// Generic class with additional generic method
public class Container<T>
{
private T _item;

public Container(T item)
{
_item = item;
}

public T GetItem()
{
return _item;
}

// Generic method with different type parameter
public void PrintPair<U>(U secondItem)
{
Console.WriteLine($"Pair: {_item} and {secondItem}");
}
}

class Program
{
static void Main(string[] args)
{
// Using generic method from non-generic class
var utility = new Utility();
int defaultInt = utility.GetDefault<int>();
string defaultString = utility.GetDefault<string>();

Console.WriteLine($"Default int: {defaultInt}");
Console.WriteLine($"Default string: {defaultString ?? "null"}");

// Using generic method from generic class
var stringContainer = new Container<string>("Hello");
stringContainer.PrintPair(42);
stringContainer.PrintPair<double>(3.14);
}
}

Output:

Default int: 0
Default string: null
Pair: Hello and 42
Pair: Hello and 3.14

Summary

In this tutorial, you've learned:

  1. What generic methods are and why they're useful in C# programming
  2. How to define and use generic methods with one or multiple type parameters
  3. How to use type inference to simplify method calls
  4. How to apply constraints to restrict which types can be used with your generic methods
  5. Real-world applications of generic methods, including the repository pattern
  6. How to use generic methods in both generic and non-generic classes

Generic methods are a powerful feature of C# that provide type safety, code reusability, and better performance. By mastering generic methods, you'll write more flexible and maintainable code.

Exercises

To reinforce your understanding of generic methods, try these exercises:

  1. Write a generic method FindMax<T> that finds the maximum value in an array of comparable items.
  2. Create a generic Pair<TFirst, TSecond> class with methods to get and set values.
  3. Implement a generic Cache<TKey, TValue> class that stores items with an expiration time.
  4. Write a generic method that can convert any collection to a comma-separated string.
  5. Implement a generic method that can filter a list based on a predicate function.

Additional Resources

Happy coding with generic methods!



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