C#. Support for contravariance in generic interfaces

Support for contravariance in generic interfaces. The in keyword

Before exploring this topic, it is recommended that you familiarize yourself with the following topics:


Contents


1. Contravariance concept. The in keyword

In C#, starting with version 4.0, covariance and contravariance were introduced in generics. You can read more about the peculiarities of using covariance for generic types here. This topic reveals the concept of contravariance.

Contravariance (as well as covariance) is considered in the context of the hierarchy of some classes. Contravariance in generic interfaces is a mechanism that provides the ability to use any class along a certain class hierarchy in generic classes that implement this interface.

To implement the contravariance mechanism, the following elements must be declared:

  • a generic interface that takes as a parameter some type T and implements a method (s), which is subject to a number of requirements in the signature (see below). In what follows, this interface will be referred to as the contravariant interface. An argument of type T in a contravariant interface is declared using the in keyword;
  • a generic class that implements a contravariant interface. According to the syntax, the class must implement all the methods of the interface;
  • two or more classes forming a hierarchy. These classes act as arguments of type T when declaring a reference to a contravariant interface and instantiating generic classes.

The general form of declaring a contravariant interface that receives as an argument some type T is

interface InterfaceName<in T>
{
  ...
}

here

  • InterfaceName – the name of the generic interface. The in modifier indicates that the interface supports contravariance;
  • T is the name of the type that is used in the declaration of interface methods InterfaceName.

As you can see from the above code, to provide contravariance, the in keyword is set before the name of the type T.

A class that implements a contravariant interface is declared in the standard way

class ClassName<T> : InterfaceName<T>
{
  ...
}

here

  • ClassName – the name of the generic class that receives the type T as a parameter and implements the InterfaceName<T> interface.

As you can see from the above snippet, the in modifier is not placed before the type declaration of the class T.

The interface-class pair allows you to implement the contravariance mechanism for the generic type T. Classes that form a hierarchy can be used as type T. For two classes Base and Derived that form a hierarchy

class Base
{
  ...
}

class Derived : Base
{
  ...
}

you can write the following code that works due to contravariance

// Declare interface reference with placeholder type Derived
InterfaceName<Derived> refDerived;

// Create an instance of the ClassName class with the Base placeholder type
ClassName<Base> obj = new ClassName<Base>(...);

// This assignment works due to contravariance
refDerived = obj;

As you can see from the above code, the in modifier in the interface declaration InterfaceName<in T> allows the instance of the class that receives the base class Base as an argument to be assigned a reference to the interface that the derived class Derived receives as a parameter.

In other words, if the in keyword is removed from the interface declaration, then in the line

refDerived = obj;

an error will occur at compile time. Contravariance WILL NOT be supported, and a standard type incompatibility error will be thrown as a result. This is because the compiler requires strict matching of type arguments to ensure type safety. Contravariance mitigates this requirement.

 

2. Requirements for methods that are declared in a contravariant interface

Methods declared in a contravariant interface must have a signature in which the method does not return a generic type T

interface IContraInterface<in T>
{
  ...

  type MethodName(parameters);

  ...
}

here

  • type – any type in the program except type T. It can be one of the basic types (int, float, char, etc.) or any other type of program (class, structure, etc.).

 

3. An example demonstrating contravariance

The example demonstrates the contravariance mechanism for refA and refB references.

using System;

namespace ConsoleApp1
{
  // Contravariance in a generic interface.
  // A contravariant generic interface that takes the type T as a parameter.
  interface IContraInterface<in T>
  {
    void Print(); // without parameters
    void PrintT(T value);
  }

  // Generic class that implements generic interface
  class GenClass<T> : IContraInterface<T>
  {
    // Internal class field
    T value;

    // Constructor
    public GenClass(T _value)
    {
      value = _value;
    }

    // The method of access to the internal field
    public T GetT() { return value; }

    // Implementing the PrintT() method to the interface.
    // The method sets a new value in the value variable.
    public void PrintT(T _value)
    {
      Console.WriteLine("{0}", _value);
    }

    public void Print()
    {
      Console.WriteLine(value.ToString());
    }
  }

  // Some hierarchy of classes A, B.
  // Base class.
  class A
  {
    // Internal field of class A
    int a;

    // Constructor
    public A(int _a) { a = _a; }

    // Field accessor
    public int GetA() { return a; }

    // Method of accessing the field a in string form through the Object class
    public override string ToString()
    {
      return "a = " + Convert.ToString(a);
    }
  }

  // Derived class
  class B : A
  {
    // Internal field
    int b;

    // Constructor
    public B(int _a, int _b) : base(_a)
    {
      b = _b;
    }

    // Field accessor
    public int GetB() { return b; }

    // Method of accessing the field b in string form through the Object class
    public override string ToString()
    {
      return "b = " + Convert.ToString(b);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Demonstration of contravariance
      // 1. Create instances of classes A, B
      A objA = new A(10);
      B objB = new B(15, 20);

      // 2. Create two references to the contravariant interface,
      //    that take classes A and B as parameters.
      IContraInterface<A> refA;
      IContraInterface<B> refB;

      // 3. Create instances of the class MyContraclass <T>,
      //    in which the type parameters are classes A, B.
      //    Accordingly, the constructors of the class receive objects objA, objB.
      GenClass<A> objGenClassA = new GenClass<A>(objA);
      GenClass<B> objGenClassB = new GenClass<B>(objB);

      // 4. Demonstration of contravariance using the refA, refB references
      refA = objGenClassA; // it is possible in any case
      refA.Print(); // a = 10

      refB = objGenClassB; // it is possible in any case
      refB.Print(); // b = 20

      // it is possible due to contravariance
      refB = objGenClassA; // there is no strict type enforcement here
      refB.Print(); // a = 10

      // this is not possible, to do this you need to implement covariance
      // refA = objGenClassB; // here's a compilation error

      // Call another method of the PrintT() interface.
      GenClass<A> objGenClassA2 = new GenClass<A>(new A(777));

      // this is possible due to contravariance
      refB = objGenClassA2; // GenClass<B> <= GenClass<A>
      refB.Print(); // a = 777

      // Call another method of the PrintT() interface.
      refB.PrintT(new B(200, 300)); // b = 300

      Console.WriteLine("Ok");
      Console.ReadKey();
    }
  }
}

Program execution result

a = 10
b = 20
a = 10
a = 777
b = 300
Ok

 

4. Figure explaining contravariance

Figure 1 depicts one interface and a minimal set of classes that are required to expose the details of implementing contravariance. The corresponding code fragments are marked with red arrows.

C#. Generics. Contravariance in generic interfaces. The keyword in

Figure 1. Applying contravariance to the class hierarchy A, B

 

5. Inheritance in contravariant interfaces

Contravariant interfaces can inherit from each other. The general syntax for inheritance of contravariant interfaces taking a type T as a parameter is as follows

interface IBaseInterface<in T>
{
  ...
}

interface IDerivedInterface<in T> : IBaseInterface<T>
{
  ...
}

here

  • IBaseInterface<in T> – basic contravariant interface;
  • IDerivedInterface<in T> – inherited contravariant interface. If you omit the in modifier in the inherited interface, then this interface will not support contravariance.

 

6. Restrictions on contravariance

The following restrictions are imposed on contravariance:

  • contravariance can be applied only for reference types (class);
  • methods that are declared in contravariant generic interfaces must not return the generic type T;
  • methods that are declared in contravariant interfaces can receive the type T as a parameter.

 


Related topics