Generics

Generics were introduced in .NET 3.5 and C# 2 (e.g. a Long Time Ago). They are a powerful feature that allows you to define classes, methods, and interfaces with a placeholder for the data type.

This feature is fundamental in many modern programming languages and is one of the pieces of magic that makes LINQ (Language Integrated Query) possible. What is LINQ, you ask? Wait young grasshopper, you'll learn soon enough.

(Generics are a great example of polymorphism, another object-oriented pillar!)

Very Basics of Generic Types

Generics provide a way to define classes, methods, and interfaces with a placeholder for the type of data they operate on. This allows for more flexible and reusable code, as you can define a generic class or method once and use it with any data type.

Example

public class Box<T>
{
    private T _item;

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

    public T GetItem()
    {
        return _item;
    }
}

Box<int> intBox = new Box<int>();
intBox.SetItem(10);
Console.WriteLine(intBox.GetItem()); // 10

Box<string> stringBox = new Box<string>();
stringBox.SetItem("Hello");
Console.WriteLine(stringBox.GetItem()); // Hello

stringBox.SetItem(123); // Compile-time error, yay!

Creating Generic Classes

Example

public class Storage<T>
{
    public T Item { get; set; }
}

In this example, Item is a property of type T, where T can be any type specified when the Storage<T> class is instantiated.

Demonstrating Using Generically-Typed Methods

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

In this example, GetDefaultValue is a method that returns the default value for the specified type T.

Demonstrating Type Constraints

public class DataManager<T> where T : class, IDisposable
{
    public void Manage(T item)
    {
        // Logic for managing disposable objects
    }
}

In this example, the where T : class constraint specifies that T must be a reference type AND must implement IDisposable. Constraining the type is extremely useful as it allows you to define how an interface is used. It also means that you can use members specific to that type or interface:

public interface ITrackedEntity
{
    Guid Id { get; set; }
}

public class DataManager<T> where T : class, ITrackedEntity
{
    public void Manage(T item)
    {
        Console.WriteLine($"Managing entity with ID: {item.Id}");
    }
}

Multiple Type Generics

Generics can have multiple type parameters (I think the language limit is 16, but I have never gone higher than 2 in practice).

public class KeyValuePairs
{
    public Dictionary<int, string> CreateDictionary()
    {
        return new Dictionary<int, string>
        {
            { 1, "One" },
            { 2, "Two" }
        };
    }
}

One of the most common uses of multiple generic types is with collections like Dictionary<TKey, TValue>. In this example, Dictionary<int, string> holds key-value pairs, where the key is an int and the value is a string.

Demonstrating the Repo Patterns with Generics

While I don't love talking about "software patterns", there are a few common and important ones. We'll explore this one, the Repository pattern, in more detail in the next course on APIs.

That said, imagine that you wanted to define and store the types you use in your application (spoiler alert, this is an extremely common thing to do!) Things like Customer, Order, Product, etc. When building that system, you might start with something like this:

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

public class Repository<T> : IRepository<T> where T : class
{
    private readonly List<T> _entities = new List<T>();

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

    public T GetById(int id)
    {
        // Simulate fetching entity by ID
        return _entities[id];
    }

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

    public void Update(T entity)
    {
        // Logic for updating entity
    }

    public void Delete(T entity)
    {
        _entities.Remove(entity);
    }
}

var customerRepository = new Repository<Customer>();
var productRepository = new Repository<Product>();

customerRepository.Add(new Customer { Id = Guid.NewGuid(), Name = "John Doe" });
productRepository.Add(new Product { Id = Guid.NewGuid(), Name = "Product A" });

In this example, Repository<T> is a generic class that implements the IRepository<T> interface. This allows for the reuse of the repository logic across different types of entities, such as Customer, Product, etc.