Genéricos e Programação Genérica em C# (.NET): Um Guia Detalhado
What is it and why? (a real-life analogy)
Imagine you are the owner of a small warehouse. In your warehouse, there are many different boxes. In some boxes you put books, in others — toys, in others — tools. To avoid confusion, you label each box: "Books", "Toys", "Tools". But what if you had to make a separate type of box for each kind of thing? For books — "BookBox", for toys — "ToyBox", for tools — "ToolBox". That would be terribly inconvenient! You would have to learn to work with each type of box differently.
Now imagine there is a universal box. It's just a "Box". You can write on it with a marker what's inside: "Books", "Toys", or "Tools". And, most importantly, when you take this box, you know exactly what you will get out of it. If the box says "Books", you won't pull a hammer out of it. The computer (or, in our case, the compiler) monitors this and won't let you make a mistake.
Generics in C# are exactly these kinds of "smart boxes." They allow us to write code once and then use it to work with different data types (numbers, strings, your own classes), while maintaining type safety. Without generics, we would have to either write the same code for each type (which is very long and tedious) or use the object type (which is dangerous because you could accidentally put a cat in a box for dogs).
Generics are a way to tell the compiler: "I will write an algorithm, and you substitute the specific type there when I say so." It's like a baking mold: the mold itself is one, but the dough (data type) can be different — shortcrust, puff, sponge.
Theory: how it works internally
Let's look "under the hood" and see how C# manages generics. This is not magic, but a well-thought-out job of the compiler and the virtual machine (.NET CLR).
Type Parameter
When we write List<T>, the letter T is the type parameter. It's like a variable, but for a data type. We are saying: "Listen, compiler, I don't know yet what I'll be working with. I'll specify the type T later." Usually, the letters T (from Type), TKey, TValue are used, but you can name it anything: TItem, TData.
Type Argument
When we write List<int> or List<string>, we substitute a specific type argument (int or string) in place of the parameter T. At this moment, the compiler "freezes" our generic code to work specifically with this type.
How does it work at the memory level?
There are two important concepts here: open type and closed type.
- Open type:
List<T>is just a "blueprint." The compiler cannot create an object of this type because it doesn't know how much memory to allocate forT. - Closed type:
List<int>is a concrete type. The compiler knows thatinttakes 4 bytes and can create a full-fledged array for storing elements.
The most interesting thing happens in the JIT compiler (Just-In-Time). When your program first uses List<int>, the JIT compiler takes the common IL code (intermediate language) for List<T> and substitutes int into it. For all reference types (classes, strings), the JIT creates one common code because all references have the same size (8 bytes on a 64-bit system). For each value type (int, double, struct), its own unique code is created to optimize memory operations.
This is a key advantage over the old approach withArrayList(a collection based onobject): there was no type control, and every time you retrieved a number, it had to be cast fromobjecttoint(boxing/unboxing). Generics save us from this headache.
Syntax in C# (.NET): all forms and variations
Let's break down all possible ways to declare and use generics. This is like an alphabet you need to know to read and write generic code.
1. Generic Classes
The most common case. A class is declared with one or more type parameters.
// Declaring a generic class "Storage" with one type parameter T
public class Storage<T>
{
// Field of type T. We don't know what it will be, but the compiler knows it's some type.
private T _data;
// Constructor that takes a value of type T
public Storage(T value)
{
_data = value;
}
// Method that returns a value of type T
public T GetData()
{
return _data;
}
// Method that sets a new value
public void SetData(T newValue)
{
_data = newValue;
}
}
// Usage:
// Creating a storage for integers
Storage<int> numberStorage = new Storage<int>(42);
int number = numberStorage.GetData(); // All good, number = 42
// Creating a storage for strings
Storage<string> stringStorage = new Storage<string>("Hello, world!");
string text = stringStorage.GetData(); // All good, text = "Hello, world!"
// But this CANNOT be done:
// int error = stringStorage.GetData(); // Compilation error! Types do not match.
2. Generic Methods
A method can have its own type parameters, even if the class itself is not generic.
```