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:

⚠︎ Key Issues:
  • 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 T for a single type parameter: public class LinkList<T>
  • Use T prefix 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:

Constraint Description
Constraint where T : new() Description Must have public parameterless constructor
Constraint where T : <interface> Description Must implement specified interface
Constraint where T : X Description Must be or derive from X
Constraint where T : <base class> Description Must derive from specified base class
Constraint where T : struct Description Must be a value type
Constraint where T : class Description Must be a reference type

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

⚠︎ Important: Static members of a generic class are only shared within single instantiation of the class.
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

Key Takeaways:
  • 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.