C#. Covariance in generic interfaces. The out keyword

Covariance in generic interfaces. The out keyword


Contents


1. Covariance. Features of using in generic interfaces. The out keyword

Starting with version 4.0, an extension was introduced to the C# language that allows you to apply covariance and contravariance for the generic type parameter T in interfaces and delegates.

Covariance makes it possible to return a derived type from the base type that is specified in the type parameter.

To indicate to the compiler that the type T supports covariance, the out keyword is used in the following form

out T

Thus, the general form of declaring a generic interface that supports covariance is

interface ICoVarInterface<out T>
{
  // Declaring methods that return type T
  ...
}

here

  •  ICoVarInterface – the name of the generic interface.

As you can see from the above declaration, the type T in the interface supports covariance because it is preceded by the out keyword.

In the ICoVarInterface<T> interface, you need to declare only methods that return type T. The general form of declaring such a method is as follows

T MethodName(parameters);

here

  • MethodName – method’s name;
  • parameters – list of method parameters. Parameters can be of any type other than the generic type T.

After declaring an interface, you can declare a class. The declaration of a class that implements the ICoVarInterface interface is carried out in the usual way

class CoVarClass<T> : ICoVarInterface<T>
{
  ...

  // Implementation of methods of ICoVarInterface interface
  // ...

  ...
}

The above interface-class pair support covariance. Now this pair can be applied to some hierarchy of classes that will act as type T. If two classes are BaseClass, DerivedClass form a hierarchy

class BaseClass
{
  ...
}

class DerivedClass : BaseClass
{
  ...
}

then when declaring a reference to the generic interface ICoVarInterface<out T>, the type of the base class is always specified as a parameter of type T

ICoVarInterface<BaseClass> refI;

You can create an instance of the CoVarClass<T> class in the usual way, specifying the base class BaseClass as a type parameter, more precisely an instance of the base class

BaseClass objBase = new BaseClass(...); // the instance of base class
refI = new CoVarClass<BaseClass>(objBase);

Also, due to covariance, you can specify the type parameter as an instance of a derived class

DerivedClass objDerived = new DerivedClass(...); // the instance of derived class
refI = new CoVarClass<DerivedClass>(objDerived); // works - covariance is provided

If the out keyword is removed from the ICoVarInterface declaration

interface ICoVarInterface<T>
{
  ...
}

then it will not be possible to create an instance of the CoVarClass class in which the derived class DerivedClass is the type of T

DerivedClass objDerived = new DerivedClass(...); // derived class instance
refI = new CoVarClass<DerivedClass>(objDerived); // here is an error - there is no covariance

 

2. An example showing how the covariance mechanism works

The example declares:

  • the generalized interface IMyCoVarInterface<out T>, which takes as a parameter the type T. This type T supports covariance thanks to the out modifier;
  • a generic class MyClass<T> that implements the IMyCoVarInterface<out T> interface. In a class, the type T also supports covariance because the underlying interface supports covariance;
  • two classes A, B which form a hierarchy.

The IMyCoVarInterface<out T> interface declares a single GetT() method that returns a reference of type T.

The MyClass<T> class contains the following elements:

  • the internal field value of the generic type T. In what follows, instead of the type T, one of the instances of classes A and B, which form a hierarchy, will be substituted;
  • a one-parameter constructor that initializes value;
  • the Get() method, which accordingly implements the interface method.

To demonstrate covariance, the example created a hierarchy of two classes A and B. In the base class A, the following components are declared:

  • internal field a, which is class data;
  • constructor with one parameter, initializing field a;
  • virtual Get() method for accessing the internal value field.

In inherited class B, the following elements are declared:

  • internal field b;
  • constructor with two parameters. This constructor initializes field a of the base class and field b of this class;
  • an overridden Get() method to access the b field.

 

using System;

namespace ConsoleApp19
{
  // Interface covariance.
  // Determined by the out keyword.
  public interface IMyCoVarInterface<out T>
  {
    // Method that returns type T
    T GetT();
  }

  // A class that implements a covariant interface
  class MyClass<T> : IMyCoVarInterface<T>
  {
    // Internal field
    T value;

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

    // Method declared in interface IMyCoVarInterface<T>
    public T GetT() { return value; }
  }

  // Some class hierarchy: A - base class, B - derived class.
  // Base class
  class A
  {
    // Class A data
    int a;

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

    // Access method
    public virtual int Get() { return a; }
  }

  // Derived class
  class B : A
  {
    // Class B data
    int b;

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

    // Access method
    public override int Get() { return b; }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Demonstration of covariance.
      // 1. Declare a reference to the interface,
      // which receives as a parameter of the type the name of the base class A
      IMyCoVarInterface<A> refI;

      // 2. Create an instance of base class A
      A objA = new A(25);

      // 3. Create an instance of the class MyClass, in which the type is class A
      refI = new MyClass<A>(objA); // it works
      Console.WriteLine("objA.value = " + refI.GetT().Get());

      // 4. Create an instance of the derived class B
      B objB = new B(30, 40);

      // 5. Assign an instance of the class MyClass to the refI reference,
      //   in which the type is class B
      refI = new MyClass<B>(objB); // works due to covariance

      Console.WriteLine("objB.value = " + refI.GetT().Get()); // 40
      Console.ReadKey();
    }
  }
}

Let’s explain some code snippets. In the main() function, two instances of objA and objB are created, respectively of classes A and B

...

// 2. Create an instance of base class A
A objA = new A(25);

...

// 4. Create an instance of the derived class B
B objB = new B(30, 40);

These instances are substituted as placeholder types when creating an instance of the MyClass class

...

// 3. Create an instance of the class MyClass, in which class A acts as a type
refI = new MyClass<A>(objA);

...

// 5. Assign an instance of the class MyClass to the refI reference,
//   in which the type is class B
refI = new MyClass<B>(objB); // works due to covariance

Instantiation in which the placeholder type is class A needs no explanation as the refI is declared for type A

...

// 1. Declare an interface reference
//   that receives as a parameter of type the name of the base class A
IMyCoVarInterface<A> refI;

...

And in order to create an instance of class B, it is imperative that when declaring an interface for type T, the out modifier is specified, as in our case

// out - covariance support
public interface IMyCoVarInterface<out T>
{
  ...
}

If you remove the out keyword from the interface declaration, then you will not be able to create an instance of the MyClass<B> class.

// out is absent - covariance is not supported
public interface IMyCoVarInterface<T>
{
  ...
}

static void Main(string[] args)
{
  ...

  refI = new MyClass<B>(objB); // compilation error, the word out is missing in the interface

  ...
}

After running for execution, the program produces the following result

objA.value = 25
objB.value = 40

 

3. Figure explaining covariance

The following figure explains the mechanism for applying covariance for a generic interface. The importance of using the out modifier is emphasized.

 

C#. Generics. Covariance. The out modifierFigure 1. Covariance. The out modifier allows you to create an instance of a generic class in which a derived class is a type

 

4. Inheritance of covariant interfaces

Covariant interfaces can be inherited. In the most general case, inheritance of two interfaces looks like this

// Inheritance of covariant interfaces.
// Determined by the out keyword.
interface ICoVarInterface<out T>
{
  ...
}

// Interface that inherits ICoVarInterface<T>
interface ICoVarInterface2<out T> : ICoVarInterface<T>
{
  ...
}

As you can see from the snippet, the out modifier is used to ensure covariance in the inherited interface. If you do not specify out, then the inherited ICoVarInterface2 interface will not use covariance (which cannot be said about the base interface).

When declaring the inherited interface ICoVarInterface2<out T>, you do not need to specify the out modifier in the base interface ICoVarInterface<T>.

 

5. Restrictions on the covariance

The following restrictions are imposed on covariance.

1. The covariance of the type parameter T extends only to methods that return the type T. For example, a method with the following signature is suitable for implementing covariance

T GetItem();

and the next is not suitable

int Method(T value);

since it does not return type T.

2. The covariance of a parameter of type T cannot be applied to methods that receive at least one parameter of type T.

For example, the following methods are not suitable for implementing covariance

T GetItem(T value);
T Calc(int x1, T x2);

since the type T appears in the methods parameters.

3. Covariance is only suitable for reference types. If you try to implement covariance for structures, the compiler will generate an error.

4. When declaring an interface, a covariant type cannot be used as a constraint. For example, you cannot declare an interface in which the covariant type T is restricted to another type

interface IMyInterface<out T>
{
  T GetItem<V>() where V : T; // here's a compilation error
}

 


Related topics