C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
31: Generics in C#
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 returndefault(TValue). - In your
Mainmethod, instantiate two different caches: one forint(to store user IDs) and one forstring(to store session tokens). - Add a value to both and retrieve them to verify the types are maintained without any manual casting.
There are no comments for now.