Skip to main content

C# Generic Best Practices

When working with generics in C#, following established best practices helps you write code that's more maintainable, readable, and efficient. This guide covers essential recommendations for using generics effectively in your C# applications.

Introduction to Generic Best Practices

Generics in C# provide a powerful way to create reusable, type-safe code without sacrificing performance. However, to fully leverage their benefits, you need to follow certain patterns and avoid common pitfalls. These best practices will help you write generic code that's both elegant and practical.

When to Use Generics

✅ Do Use Generics When:

  • You need the same functionality for different types
  • You want to avoid boxing/unboxing performance penalties
  • You need strict type safety
  • You're working with collections of various types

❌ Don't Use Generics When:

  • The implementation would be radically different for different types
  • You need only one specific type implementation
  • The complexity outweighs the benefits

Naming Conventions

Follow these established naming conventions for generic type parameters:

csharp
// Single letter type parameters for simple cases
public class Cache<T>
{
// Implementation
}

// Descriptive names with 'T' prefix for more complex scenarios
public class KeyValuePair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }
}

Common conventions include:

  • T for a single generic type parameter
  • TKey, TValue for key-value pairs
  • TResult for return values
  • TInput for input parameters
  • TEntity for data models

Use Constraints Effectively

Type constraints help you write more specific and reliable generic code by restricting the types that can be used as generic arguments.

csharp
// Constraining to reference types
public class Logger<T> where T : class
{
public void Log(T item) => Console.WriteLine($"Logging {item}");
}

// Constraining to value types
public class Calculator<T> where T : struct
{
// Implementation
}

// Constraining to types with a parameterless constructor
public class Factory<T> where T : new()
{
public T Create() => new T();
}

// Multiple constraints
public class Repository<T> where T : class, IEntity, new()
{
public void Save(T entity)
{
// Implementation
}
}

Best Practices for Constraints:

  1. Add constraints only when necessary
  2. Use the most specific constraint that meets your needs
  3. Consider interface constraints for better abstraction
  4. Remember that multiple constraints can be combined

Consider Performance Implications

Avoid Boxing and Unboxing

One of the main benefits of generics is avoiding boxing/unboxing penalties when working with value types.

csharp
// Without generics - causes boxing/unboxing with value types
public object GetDefaultNonGeneric()
{
return default(int); // Boxes the int to object
}

// With generics - no boxing
public T GetDefaultGeneric<T>()
{
return default(T); // No boxing!
}

Static Fields in Generic Classes

Be careful with static fields in generic classes as they're unique for each closed generic type:

csharp
public class GenericCounter<T>
{
// This static field will be separate for each type T
public static int Count;
}

// Usage
GenericCounter<int>.Count = 5;
GenericCounter<string>.Count = 10;

Console.WriteLine(GenericCounter<int>.Count); // Output: 5
Console.WriteLine(GenericCounter<string>.Count); // Output: 10

Write Generic Extension Methods

Generic extension methods allow you to add functionality to any type that satisfies your constraints:

csharp
public static class Extensions
{
public static bool IsDefault<T>(this T value)
{
return EqualityComparer<T>.Default.Equals(value, default(T));
}

public static IEnumerable<TResult> CustomSelect<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
foreach (var item in source)
{
yield return selector(item);
}
}
}

// Usage
int number = 0;
bool isDefault = number.IsDefault(); // true

var numbers = new[] { 1, 2, 3 };
var doubled = numbers.CustomSelect(n => n * 2); // [2, 4, 6]

Real-World Application: Generic Repository Pattern

The Repository Pattern is a common use case for generics in C#. Here's a simplified implementation:

csharp
// Define a common interface for entities
public interface IEntity
{
int Id { get; set; }
}

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

// Implementation for a generic repository
public class Repository<T> : IRepository<T> where T : class, IEntity, new()
{
private readonly List<T> _items = new List<T>();
private int _nextId = 1;

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

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

public void Add(T entity)
{
if (entity.Id == 0)
{
entity.Id = _nextId++;
}
_items.Add(entity);
}

public void Update(T entity)
{
var existingItem = GetById(entity.Id);
if (existingItem != null)
{
_items.Remove(existingItem);
_items.Add(entity);
}
}

public void Delete(int id)
{
var entity = GetById(id);
if (entity != null)
{
_items.Remove(entity);
}
}
}

// Example usage
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

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

// Using the generic repositories
public void DemoRepositories()
{
// Create repositories for different entity types
var productRepo = new Repository<Product>();
var customerRepo = new Repository<Customer>();

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

// Add customers
customerRepo.Add(new Customer { Name = "John Doe", Email = "[email protected]" });

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

Generic Method vs. Generic Class

Choose between generic methods and generic classes based on your needs:

csharp
// Generic method - use when only specific methods need type parameters
public class Utilities
{
public void ProcessNonGeneric(string input) { /* ... */ }

public T ProcessGeneric<T>(T input)
{
// Generic processing logic
return input;
}
}

// Generic class - use when the entire class needs to operate on the type
public class Processor<T>
{
private T _value;

public Processor(T initialValue)
{
_value = initialValue;
}

public void Process()
{
// Process _value
}
}

// Usage
var utils = new Utilities();
int result = utils.ProcessGeneric(42); // T is inferred as int

var stringProcessor = new Processor<string>("hello");
stringProcessor.Process();

Avoid Generic Type Explosion

Be cautious about creating many different closed generic types, as each creates separate code in memory:

csharp
// Bad practice - creates many closed types
public class DataService<T1, T2, T3, T4, T5, T6>
{
// Implementation
}

// Better approach - use fewer type parameters
public class DataContext<TEntity>
{
public Repository<TEntity> GetRepository()
{
return new Repository<TEntity>();
}
}

Variance in Generics (Advanced)

For interfaces and delegates, use variance modifiers when appropriate:

  • out (covariance) - allows more derived types in the type parameter
  • in (contravariance) - allows less derived types in the type parameter
csharp
// Covariance example - IEnumerable<T> is covariant
// (can use a more derived type)
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // This works because of covariance

// Contravariance example - Action<T> is contravariant
// (can use a less derived type)
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // This works because of contravariance

Summary

Following these best practices for C# generics will help you write cleaner, more efficient, and maintainable code:

  • Use descriptive type parameter names with proper casing
  • Apply appropriate constraints to enhance type safety and code clarity
  • Be mindful of performance implications
  • Leverage generic extension methods for reusable functionality
  • Understand when to use generic classes versus generic methods
  • Consider variance (covariance and contravariance) when working with interfaces and delegates

By incorporating these practices into your development workflow, you'll be able to harness the full power of C# generics while avoiding common pitfalls.

Exercises

  1. Create a generic Stack<T> class with Push, Pop, and Peek methods
  2. Implement a generic Result<T> class that can hold either a successful result or an error message
  3. Write a generic extension method that can convert any collection to a comma-separated string
  4. Develop a generic Cache<TKey, TValue> that stores items with an expiration time
  5. Implement a generic EventBus<T> for a simple pub/sub pattern

Additional Resources



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