DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Understanding Variance in C#

Variance in C# is a powerful concept that determines how types can be substituted for one another when using generics. This article simplifies variance and demonstrates its practical use with clear examples.


What is Variance?

Variance answers the question: "Can one type be assigned to another?"

This is especially important when working with generics, where you need to decide whether one generic type (e.g., IEnumerable<string>) can replace another (e.g., IEnumerable<object>).

Variance in C# is divided into three categories:

  1. Covariance: Allows assigning a more specific type to a less specific type.
  2. Contravariance: Allows assigning a less specific type to a more specific type.
  3. Invariance: Requires exact type matches (no substitution allowed).

1. Covariance (out)

  • Definition: Allows assigning a more specific type to a less specific type (e.g., string to object).
  • When to Use: For output-only scenarios, where a generic type only produces data.

Example: Covariance with IEnumerable<T>

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        IEnumerable<string> strings = new List<string> { "Hello", "World" };
        IEnumerable<object> objects = strings; // Covariance works!

        foreach (object obj in objects)
        {
            Console.WriteLine(obj); // Outputs: Hello, World
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why it works:

The IEnumerable<string> produces strings, which are assignable to object since string is derived from object.


2. Contravariance (in)

  • Definition: Allows assigning a less specific type to a more specific type (e.g., object to string).
  • When to Use: For input-only scenarios, where a generic type only consumes data.

Example: Contravariance with IComparer<T>

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        IComparer<object> objectComparer = Comparer<object>.Default;
        IComparer<string> stringComparer = objectComparer; // Contravariance works!

        List<string> words = new List<string> { "Apple", "Banana", "Cherry" };
        words.Sort(stringComparer);

        foreach (string word in words)
        {
            Console.WriteLine(word); // Outputs: Apple, Banana, Cherry
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why it works:

The IComparer<object> can compare any objects, so it can also handle strings since every string is an object.


3. Invariance

  • Definition: No type substitution is allowed; types must match exactly.
  • When to Use: For collections or scenarios where substitution could lead to runtime errors.

Example: Invariance with List<T>

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> strings = new List<string> { "Hello", "World" };
        // List<object> objects = strings; // ❌ This won't compile.

        List<object> objects = new List<object> { "Hello", "World" };
        objects.Add(42); // Works fine since it's a List<object>.

        foreach (object obj in objects)
        {
            Console.WriteLine(obj); // Outputs: Hello, World, 42
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why it works:

List<T> is invariant to maintain type safety. Allowing List<string> to act as List<object> would risk runtime errors if the wrong type is added.


4. Array Covariance (The Pitfall)

Arrays in C# are covariant, but this can lead to runtime errors.

Example: Unsafe Array Covariance

using System;

class Program
{
    static void Main()
    {
        string[] strings = { "Hello", "World" };
        object[] objects = strings; // Array covariance works.

        try
        {
            objects[0] = DateTime.Now; // ❌ Runtime error.
        }
        catch (ArrayTypeMismatchException ex)
        {
            Console.WriteLine("Error: " + ex.Message); // Outputs an error message.
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why it fails:

The array expects only string values, but assigning a DateTime causes a runtime exception.


5. Covariance and Contravariance with Delegates

Variance also applies to delegates, allowing flexible type assignments.

Covariant Delegate Example

using System;

delegate object CovariantDelegate();

class Program
{
    static void Main()
    {
        CovariantDelegate delString = GetString;
        CovariantDelegate delObject = delString; // Covariance works!

        Console.WriteLine(delObject()); // Outputs: Hello, Covariance!
    }

    static string GetString() => "Hello, Covariance!";
}
Enter fullscreen mode Exit fullscreen mode

Contravariant Delegate Example

using System;

delegate void ContravariantDelegate(string input);

class Program
{
    static void Main()
    {
        ContravariantDelegate delObject = PrintObject;
        ContravariantDelegate delString = delObject; // Contravariance works!

        delString("Hello, Contravariance!"); // Outputs: Received: Hello, Contravariance!
    }

    static void PrintObject(object obj)
    {
        Console.WriteLine("Received: " + obj);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

Type Direction Use Case Example
Covariance More → Less Specific Reading data IEnumerable<T>
Contravariance Less → More Specific Writing data IComparer<T>
Invariance No substitution Collections List<T>

Summary

  1. Use covariance (out) when types are used as outputs.
  2. Use contravariance (in) when types are used as inputs.
  3. Generics are invariant by default for safety and consistency.
  4. Be cautious with array covariance, as it can lead to runtime errors.

Top comments (0)