Introduction
Generics are a powerful feature introduced in .NET 2.0 (Visual Studio 2005) that allow you to create type-safe and reusable code. This article explores all aspects of generics in C#, covering everything from basic concepts to advanced implementations.
Overview of Generics
Generics are deeply integrated with the Intermediate Language (IL) and can be used in any type of .NET application - Console, Windows, Web, or Service applications. The .NET Framework provides the System.Collections.Generic namespace with several generic-based collection classes. Microsoft recommends using generic classes instead of older non-generic collection classes like ArrayList.
Why Generic Classes and Methods?
Generic classes are especially useful for creating algorithms that provide:
- Reusability: Write once, use with multiple types
- Type Safety: Compile-time type checking
- Efficiency: No boxing/unboxing overhead
The Problem with Non-Generic Collections
Non-generic collection classes like ArrayList have significant limitations. ArrayList uses the Object class internally to store values, which means:
- Value types must be boxed when stored and unboxed when retrieved
- Boxing and unboxing causes performance loss
- No compile-time type safety
- Runtime errors instead of compile-time errors
For example, you can store mixed types in an ArrayList:
ArrayList list = new ArrayList();
list.Add(2); // int
list.Add("text"); // string - No compile error!
// This will throw InvalidCastException at runtime
foreach (int element in list)
{
Console.WriteLine(element);
}
Creating Generic Classes
Here's an example of a generic LinkedList implementation that solves the problems mentioned above:
public class LinkedListNode<T>
{
public LinkedListNode(T _Value)
{
this.Value = _Value;
}
public T Value { get; internal set; }
public LinkedListNode<T> Next { get; internal set; }
public LinkedListNode<T> Last { get; internal set; }
}
public class LinkedList<T> : IEnumerable<T>
{
public LinkedListNode<T> First { get; private set; }
public LinkedListNode<T> Last { get; private set; }
public void AddList(T Node)
{
LinkedListNode<T> NewNode = new LinkedListNode<T>(Node);
if (First == null)
{
First = NewNode;
Last = First;
}
else
{
Last.Next = NewNode;
Last = NewNode;
}
}
public IEnumerator<T> GetEnumerator()
{
LinkedListNode<T> current = First;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Using Generic Collections
// Type-safe int collection
LinkedList<int> List1 = new LinkedList<int>();
List1.AddList(2);
List1.AddList(3);
List1.AddList(5);
// List1.AddList("4"); // Compile error!
// Type-safe string collection
LinkedList<string> List2 = new LinkedList<string>();
List2.AddList("2");
List2.AddList("3");
Generic Parameters Type
When instantiating a generic type, you specify the type argument:
LinkedList<int> obj1 = new LinkedList<int>();
LinkedList<string> obj2 = new LinkedList<string>();
LinkedList<MyClass> obj3 = new LinkedList<MyClass>();
Naming Conventions for Type Parameters
- Use
Tfor a single type parameter:public class LinkList<T> - Use
Tprefix for multiple parameters:public delegate TOut Con<TIn, TOut>(TIn from)
Constraints on Type Parameters
Constraints restrict what types can be used as type arguments. There are 6 types of constraints:
Example with Constraints
// Only value types allowed
public class LinkedList<T> : IEnumerable<T> where T : struct
{
// Implementation
}
// Usage
LinkedList<int> list1 = new LinkedList<int>(); // Works
LinkedList<string> list2 = new LinkedList<string>(); // Compile error!
Inheritance with Generics
Generic types can implement interfaces and derive from base classes:
// Generic class implementing generic interface
public class LinkedList<T> : IEnumerable<T> where T : struct { }
// Generic class derived from generic base
public class ParentClass<T> { }
public class ChildClass<T> : ParentClass<T> { }
// Abstract generic class
public abstract class ParentClass<T>
{
public abstract T Add(T x, T y);
}
public class ChildClass : ParentClass<int>
{
public override int Add(int x, int y)
{
return x + y;
}
}
Static Members in Generic Classes
public class First<T>
{
public static int A;
}
// Usage
First<string>.A = 50;
First<int>.A = 100;
Console.WriteLine(First<int>.A); // Prints 100
// Two separate static fields exist for different type arguments
Generic Interfaces
Generic interfaces help avoid boxing/unboxing operations:
interface ISwap
{
void Swap<T>(ref T First, ref T Sec);
}
class Program : ISwap
{
public void Swap<T>(ref T First, ref T Sec)
{
T Temp = First;
First = Sec;
Sec = Temp;
}
static void Main(string[] args)
{
Program p = new Program();
int a = 10, b = 20;
p.Swap<int>(ref a, ref b);
Console.WriteLine($"a = {a}, b = {b}");
string s1 = "Hello", s2 = "World";
p.Swap<string>(ref s1, ref s2);
Console.WriteLine($"s1 = {s1}, s2 = {s2}");
}
}
Generic Methods
Generic methods allow you to write a single method that works with multiple types:
public static void Swap<T>(ref T First, ref T Last)
{
T Temp = First;
First = Last;
Last = Temp;
}
// Usage with explicit type parameter
Swap<int>(ref firstValue, ref lastValue);
// Usage with type inference (compiler infers type)
Swap(ref firstName, ref lastName);
Type Inference
Type inference is the compiler's ability to automatically deduce the type parameter based on method arguments. It occurs at compile time before resolving overloaded methods.
Generic Delegates
Generic delegates allow you to create type-safe function pointers that work with multiple types:
public delegate T fun<T>(T Value);
public int MultiplyByTwo(int x) { return x * 2; }
public string Concatenate(string xx) { return xx + " world"; }
// Usage
fun<int> obj = new fun<int>(MultiplyByTwo);
Console.WriteLine(obj(20)); // Output: 40
fun<string> obj1 = new fun<string>(Concatenate);
Console.WriteLine(obj1("Hello")); // Output: Hello world
The Default Keyword
Use the default keyword to get the default value for a type parameter:
public static void Swap<T>(ref T First, ref T Sec)
{
T Temp = default(T); // null for reference types, 0 for value types
Temp = First;
First = Sec;
Sec = Temp;
}
Default Values
- Reference types:
null - Numeric types:
0 - Boolean:
false - Structures: All fields set to their defaults
Generics at Runtime
The .NET runtime generates different classes based on value types and reference types:
- Value Types: Runtime generates separate classes for each value type
- Reference Types: Runtime reuses the same class for all reference types
MyClass<int> obj1 = new MyClass<int>();
MyClass<int> obj2 = new MyClass<int>(); // Reuses same generated class
MyClass<byte> obj3 = new MyClass<byte>(); // New class generated
class A { }
class B { }
MyClass<A> obj4 = new MyClass<A>();
MyClass<B> obj5 = new MyClass<B>(); // Reuses same class!
Best Practices
- Always prefer generic collections over non-generic ones
- Use constraints when you need specific functionality from type parameters
- Use meaningful names for type parameters (T prefix convention)
- Consider performance implications of value vs reference types
- Leverage type inference when possible for cleaner code
- Use
default(T)for initializing generic type variables
Conclusion
Generics are a fundamental feature in C# that enable type-safe, reusable, and efficient code. By understanding how to properly use generic classes, methods, interfaces, and delegates, you can write more maintainable and performant applications. The elimination of boxing/unboxing overhead and compile-time type safety make generics an essential tool in every C# developer's toolkit.