Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a shipping company. You have these massive steel shipping containers. The beauty of a container is that the crane doesn't care what's inside—whether it's 500 laptops, a vintage car, or a thousand rubber ducks—the crane just knows how to hook onto the corners and move the box from the ship to the truck. The "container" logic is the same regardless of the cargo.

Now, imagine if you didn't have standardized containers. Instead, you had to build a specific crane for laptops, a different crane for cars, and another for ducks. That's a nightmare. Or, worse, imagine you just threw everything into one giant pile and had to guess what each item was every time you picked it up. You'd spend half your day asking, "Is this a car or a rubber duck?" and occasionally you'd try to drive a rubber duck, which obviously doesn't work.

In C#, Generics are those shipping containers. They let us write a class or a method that says, "I don't know exactly what type of data I'm handling yet, but whatever it is, I'm going to treat it consistently."

Stopping the "Object" Madness

Before generics existed (or in languages that don't handle them well), we used the object type. Since every class in C# inherits from object, you could put anything in an object variable. But there was a catch: to get your data back, you had to "cast" it. I can't tell you how many hours of my early career I wasted debugging InvalidCastException because I thought a list contained strings when it actually contained integers.

Generics solve this by letting you use a type parameter—usually written as <T>. The T is just a placeholder. When you actually create the object, you replace T with a real type, and the compiler locks that in. No more guessing, no more casting, and no more runtime crashes because you confused a duck for a car.

Building a Flexible Result Wrapper

Let's look at a real-world scenario. Whenever I build an API, I don't just return a raw value. I return a "Response" object that tells the caller if the operation succeeded and what the data is. Without generics, I'd have to create a UserResponse, a ProductResponse, and an OrderResponse. That's a lot of redundant code.

Instead, I'll write one generic class:


public class Result<T>
{
    public bool IsSuccess { get; set; }
    public T Data { get; set; }
    public string ErrorMessage { get; set; }

    public Result(T data)
    {
        IsSuccess = true;
        Data = data;
    }

    public Result(string error)
    {
        IsSuccess = false;
        ErrorMessage = error;
        Data = default(T); 
    }
}

Now, I can use this for literally anything. If I need to return a user, I use Result<User>. If I need a list of products, I use Result<List<Product>>. The logic for IsSuccess stays exactly the same, but the Data property magically becomes the correct type.

Putting Guardrails on Your Types

Sometimes, "any type" is too broad. What if your generic class needs to call a specific method on T? If T is just any object, you can't do much with it. This is where constraints come in. You use the where keyword to tell C#, "You can use any type here, as long as it meets these criteria."

For example, let's say we want a generic Repository class, but it should only work with classes that have an Id property. We can create an interface and constrain the generic type to it:


public interface IEntity 
{
    int Id { get; set; }
}

public class Repository<T> where T : IEntity
{
    public void Save(T entity)
    {
        Console.WriteLine($"Saving entity with ID: {entity.Id}");
    }
}

If you try to use Repository<string> now, the code won't even compile. The compiler will yell at you because string doesn't implement IEntity. I love this because it catches bugs while you're typing, rather than letting them explode in production.




📋 Practical Task

Build a Generic Cache System

You need to implement a simple in-memory cache that stores a value associated with a key. Instead of making separate caches for different data types, you will build one generic cache.

Requirements:

  • Create a class called Cache<TValue>.
  • Inside the class, use a Dictionary<string, TValue> to store the data.
  • Implement a method void Add(string key, TValue value) to add items to the cache.
  • Implement a method TValue Get(string key) that returns the value for the given key. If the key doesn't exist, it should return default(TValue).
  • In your Main method, instantiate two different caches: one for int (to store user IDs) and one for string (to store session tokens).
  • Add a value to both and retrieve them to verify the types are maintained without any manual casting.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.