Introduction to Generics in C#

Generics are a game-changer in C# that let you write code that works with pretty much any data type, but still keeps all the safety checks in place. They showed up back in C# 2.0 and completely changed how we write reusable code.

Think of generics like a recipe that doesn’t specify exactly what you’re cooking with. You could throw in beef, chicken, or tofu, the cooking instructions still work, but you don’t have to write separate recipes for each ingredient. The best part? The compiler still checks that you’re not accidentally using chicken when you said you’d use beef!

How Generics Work in C#

Generics work by using what we call “type parameters”, most of us just use T, but you can name them whatever makes sense. These are basically placeholders that say “I’ll figure out the actual type later.” We put them inside angle brackets (<>) and use them throughout our code.

Here’s a simple example I put together:

public class GenericContainer<T>
{
    private T _item;

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

    public T GetItem()
    {
        return _item;
    }

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

In this code, T is just a placeholder. When someone actually uses this class, they’ll specify a real type like int or string, and the compiler fills in all the blanks. The cool thing is that our container works with absolutely any type, but it’s still completely type-safe.

Using Generic Classes

When you actually want to use a generic class, you just tell it what type you want to work with:

// For storing numbers
GenericContainer<int> intContainer = new GenericContainer<int>(42);
int myInt = intContainer.GetItem();  // Gets 42 back, no casting needed

// For storing text
GenericContainer<string> stringContainer = new GenericContainer<string>("Hello, Generics!");
string myString = stringContainer.GetItem();  // Gets our string back

// For storing our own types
GenericContainer<Person> personContainer = new GenericContainer<Person>(new Person("John", 30));
Person person = personContainer.GetItem();  // Gets our Person back

Behind the scenes, the compiler is actually creating different versions of our class for each type we use. That’s why we don’t need to cast anything, the compiler has already created a container specifically for integers, another for strings, and so on.

In C#, generics is a feature that allows you to define a type or a method that can work with multiple data types, while still maintaining type safety. Generics are used to create reusable, flexible, and efficient code that can be used in a variety of situations.

Here is an example of a generic class in C#:

public class MyGenericClass<T>
{
  private T _value;

  public MyGenericClass(T value)
  {
    _value = value;
  }

  public T GetValue()
  {
    return _value;
  }
}

In this example, the MyGenericClass class is defined with a generic type parameter T, which represents a placeholder for a data type that will be specified when the class is instantiated. The class has a private field _value of type T, and a constructor that takes a value of type T as an argument. The class also has a method GetValue that returns a value of type T.

To use the MyGenericClass class, you can instantiate it with a specific data type, like this:

MyGenericClass<int> myIntClass = new MyGenericClass<int>(123);
int value = myIntClass.GetValue();

MyGenericClass<string> myStringClass = new MyGenericClass<string>("Hello, world!");
string value = myStringClass.GetValue();

In these examples, the MyGenericClass class is instantiated with the int and string data types, respectively. The GetValue method returns the value that was passed to the constructor as an argument, and the type of the value returned is determined by the data type that was specified when the class was instantiated.

Overall, generics are a useful feature in C# that allow you to create flexible and reusable code that can work with multiple data types, while still maintaining type safety. They are commonly used to create collections, data structures, and other types of reusable code that can be used in a variety of situations.

There are several advantages to using generics in C#:

  • Code reusability: Generics allow you to create reusable code that can work with multiple data types. This means that you can write a single piece of code that can be used in a variety of situations, without having to write separate versions of the code for each data type.

  • Type safety: Generics ensure type safety by allowing you to specify the data type that a class or method can work with. This means that you can be confident that the code will only be used with the correct data type, and that any type errors will be caught at compile-time rather than runtime.

  • Performance: Generics can improve performance by allowing the compiler to generate optimized code for each data type. This is because the compiler can generate specialized versions of the code for each data type, which can be more efficient than using a single version of the code that works with all data types.

  • Readability: Generics can make your code more readable by allowing you to clearly specify the data types that a class or method can work with. This can make it easier for other developers to understand your code, and can help prevent mistakes or misunderstandings.

Generic Methods

You can also create generic methods, which is super handy. You don’t even need a generic class to use them:

public class Utility
{
    // A method that swaps any two things
    public void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }

    // A method that gives you the first item in any array
    public T GetFirst<T>(T[] array)
    {
        if (array == null || array.Length == 0)
            throw new ArgumentException("Array cannot be null or empty");

        return array[0];
    }
}

Using these methods is pretty straightforward:

Utility utility = new Utility();

// You can spell out the type if you want
int x = 10, y = 20;
utility.Swap<int>(ref x, ref y);  // Now x is 20 and y is 10

// Or just let C# figure it out (which is nicer)
string[] names = { "Alice", "Bob", "Charlie" };
string firstName = utility.GetFirst(names);  // Gets "Alice"

I really like how C# can figure out the type on its own. It makes the code much cleaner, you don’t have to keep typing those angle brackets everywhere.

Generic Constraints

Sometimes you need to be a bit pickier about what types can be used. That’s where constraints come in, they let you say “I’ll work with any type, but it needs to have certain characteristics.” You use the where keyword for this:

// Only reference types allowed
public class ReferenceContainer<T> where T : class
{
    // Implementation
}

// Only value types allowed (like int, bool, etc.)
public class ValueContainer<T> where T : struct
{
    // Implementation
}

// Only types that can be compared
public class SortableCollection<T> where T : IComparable<T>
{
    public void Sort(T[] items)
    {
        // We can safely use CompareTo because we know our type supports it
        Array.Sort(items);
    }
}

// Only types that have a parameterless constructor
public class Factory<T> where T : new()
{
    public T Create()
    {
        // We can create new instances because we know the type has a constructor
        return new T();
    }
}

// You can stack multiple constraints too
public class DisposableFactory<T> where T : class, IDisposable, new()
{
    public void CreateAndDispose()
    {
        using (T item = new T())
        {
            // Use item here
        } // item.Dispose() gets called automatically
    }
}

Constraints are really helpful because they let you do more with your generic types. Without them, you’d be limited to operations that work on literally any type, which isn’t much.

Benefits of Using Generics

So why should you bother with generics? They come with some serious advantages:

Type Safety

Generics catch type errors at compile time, not when your program is already running. The compiler makes sure you’re only doing things that make sense for your chosen type:

List<int> numbers = new List<int>();
numbers.Add(10);     // Works fine
numbers.Add("ten");  // Compiler stops you right here!

Without generics, you’d be stuck using collections of object, which is a recipe for bugs:

// The old way with ArrayList
ArrayList numbers = new ArrayList();
numbers.Add(10);       // Gets boxed into an object
numbers.Add("ten");    // This works! But it shouldn't!

// This blows up when your program is running
int firstNumber = (int)numbers[1];  // BOOM! "ten" isn't an int

Performance Improvements

Generics are actually faster, especially with value types like int and decimal. Here’s why:

  • With generics: Values stay as values, no conversion overhead
  • Without generics: Values get “boxed” into objects and then “unboxed” back

If you’re working with lots of data or need speed, this makes a big difference. I’ve seen collection operations get 2-3 times faster just by switching to generics.

Code Reusability

Generics let you write code once that works with tons of different types:

// One method works with anything comparable
public bool IsSorted<T>(List<T> items) where T : IComparable<T>
{
    for (int i = 0; i < items.Count - 1; i++)
    {
        if (items[i].CompareTo(items[i + 1]) > 0)
            return false;
    }
    return true;
}

// Look how versatile this is
IsSorted(new List<int> { 1, 2, 3, 4 });       // Works with numbers
IsSorted(new List<string> { "a", "b", "c" });  // Works with text
IsSorted(new List<DateTime> { DateTime.Now, DateTime.Now.AddDays(1) });  // Works with dates

Without generics, I’d have to write three separate methods for this simple check, or use a bunch of ugly casting code.

Clearer Code

Generic code makes it obvious what you’re working with:

// This tells me exactly what's in this collection
Dictionary<string, Person> employeeDirectory;

// This tells me nothing -> keys and values could be anything!
Hashtable employeeDirectory;

When someone (including future you) reads your code six months from now, they’ll thank you for using generics.

Practical Examples of Generics

Generic Collections

You’ll use generic collections more than anything else. .NET gives us a bunch of really useful ones:

// List -> just a flexible array
List<int> scores = new List<int> { 95, 89, 76, 92, 85 };

// Dictionary -> for looking things up by key
Dictionary<string, decimal> prices = new Dictionary<string, decimal>
{
    { "Apple", 0.99m },
    { "Banana", 0.59m },
    { "Orange", 0.79m }
};

// Queue -> first in, first out (like a line at the store)
Queue<string> printJobs = new Queue<string>();
printJobs.Enqueue("Report.pdf");  // Add to the back
printJobs.Enqueue("Invoice.pdf");
string nextJob = printJobs.Dequeue();  // Get from the front: "Report.pdf"

// Stack -> last in, first out (like a stack of plates)
Stack<char> charStack = new Stack<char>();
charStack.Push('A');  // Put on top
charStack.Push('B');
charStack.Push('C');
char topChar = charStack.Pop();  // Take from top: 'C'

Custom Generic Data Structures

You can also make your own generic stuff when you need something special:

// A basic tree node that can hold any type of data
public class TreeNode<T>
{
    public T Value { get; set; }
    public TreeNode<T> Left { get; set; }
    public TreeNode<T> Right { get; set; }

    public TreeNode(T value)
    {
        Value = value;
    }
}

// Here's how you'd use it
TreeNode<int> root = new TreeNode<int>(10);
root.Left = new TreeNode<int>(5);
root.Right = new TreeNode<int>(15);

Best Practices for Using Generics

Here are some tips I’ve picked up over the years for working with generics:

  1. Name your type parameters sensibly:

    • Just use T if you only have one type parameter
    • If you have multiple types, be more specific: TKey, TValue, TInput, TOutput
    • For domain-specific stuff, use names that make sense in context: TEntity, TProduct, etc.
  2. Be smart about constraints:

    • Add constraints when you need them, but don’t go overboard
    • Remember that each constraint makes your code less reusable
    • Ask yourself: “Do I really need this constraint, or am I just being picky?”
  3. Always use generic collections:

    • Stick with List<T> instead of the older ArrayList
    • Use Dictionary<TKey, TValue> instead of Hashtable
    • Try HashSet<T> when you need a collection of unique items
  4. Think about generic interfaces:

    • Generic interfaces like IRepository<T> are fantastic for data access
    • They make your components much more pluggable and testable

Conclusion

Generics are an absolute must-know feature in C#. They help you write code that’s flexible but still type-safe, which is a killer combination.

When you get comfortable with generics, you’ll:

  • Write code once that works with all kinds of types
  • Catch errors when you compile instead of when your users find them
  • Get better performance, especially with value types
  • Write code that’s easier to understand and maintain
  • Build more sophisticated systems with less effort

Whether you’re just storing some data in a list, writing helper methods, or building out a complex application architecture, generics should be part of your everyday toolkit. Once you get the hang of them, you’ll wonder how you ever managed without them.

The best way to learn is to start using them in your code. Try converting some of your methods to use generics, or experiment with creating your own generic classes. The compiler will guide you along the way, and you’ll quickly see the benefits.