C#. Constraints on the constructor, base class, and interface

Constraints on the constructor, base class and interface


Contents


Search other resources:




1. Constructor constraint: where T: new(). Features of use

The type constraint new() allows you to get an instance (object) of a generic type T in a generic class. To get this instance, the type (class) T must contain a parameterless constructor (default constructor).

As you know, a constructor without parameters in a class can be in one of two cases:

  • if no constructors are declared in the class. In this case, the compiler creates its own parameterless constructor, also called the default constructor;
  • if in the class the constructor without parameters is declared explicitly. In this case, in addition to this constructor, the class may contain implementations of other additional constructors with parameters.

A situation is possible when one or more constructors with parameters are implemented in a class, and a constructor without parameters is not declared. In this case, such a class (type) is not suitable for creating an instance (object) of type T.

  

2. An example explaining the use of the new() constraint

Let two classes be given:

  • class MyClass, containing an explicitly specified constructor without parameters;
  • the GenClass<T> class, which sets the bound new() for type T.

The abbreviated program code of the classes is as follows:

// A class that contains an explicitly specified parameterless constructor
class MyClass
{
  // Explicitly specified parameterless constructor
  public MyClass()
  {
    // ...
  }
}

// A generic class that takes the type T as a parameter.
// Type T has a constraint that requires type T to contain
// a parameterless constructor in its implementation.
class GenClass<T> where T : new()
{
  // Internal variable of type T
  T obj;

  // Constructor
  public GenClass()
  {
    // Create the instance of type T
    obj = new T(); // works as new() is specified
  }
}

As you can see from the above code, an instance of a generic type T is created in a generic class. Accordingly, an instance of a generic class is created in a standard way without problems.

...
// Create the instance of generic class
GenClass<MyClass> obj = new GenClass<MyClass>();
...

If you remove the constructor without parameters from the MyClass class, and instead create another constructor with parameters, then when creating an instance of the GenClass<T> class, the compiler will generate an error

// A class that does not contain a parameterless constructor
class MyClass
{
  // Some constructor with parameters,
  // there is no constructor without parameters
  public MyClass(int a)
  {
    // ...
  }
}

// A generic class that takes the type T as a parameter.
class GenClass<T> where T : new()
{
  // ...
}

...

// Create an instance of a generic class
GenClass<MyClass> obj = new GenClass<MyClass>(); // here's a compilation error
...

If you remove the constraint where T: new() from the declaration of the class GenClass<T> as shown below

// The class does not set a new() constraint on the type T
class GenClass<T>
{
  // Internal variable of type T
  T obj;

  // Constructor
  public GenClass()
  {
    // It is not possible to create an instance of type T!
    obj = new T(); // doesn't work here, compile error
  }
}

then it will be impossible to create an instance of type T in the constructor of the GenClass<T> class.

  

3. Base class constraint: where T: BaseClass

When declaring a generic class using a base class constraint, the class name is specified as the constraint. This class can be the base for many classes that form a hierarchy. This gives information to the compiler about a limited set of classes to be used as a type argument.

The base class constraint provides the following advantages:

  • a generic class uses a limited number of members defined in the base class and classes derived from it. All other classes and their elements are not used (“filtered”). The compiler now knows about the types of members that a type argument can have;
  • ensures that the required set of classes is used as a type argument. This set of classes is limited to the name of the base class. If, when creating instances of a generic class, try to specify a type that is not included in the required set of classes, an error will occur at the compilation stage. This approach is correct and allows you to avoid subtle errors that may arise at the stage of program execution.

  

4. An example explaining the constraint on the base class. Figure

Let the program code of classes with the names A, B, C, D, E, F, G, H, I and also the program code of the generalized class MyClass<T> in which the type T is bounded to the base class D be given

// List of classes forming a hierarchy
class A
{
  // ...
}

class B : A
{
  // ...
}

class C : A
{
  // ...
}

class D : A
{
  // ...
}

class E : D
{
  // ...
}

class F : D
{
  // ...
}

class G : F
{
  // ...
}

class H : B
{
  // ...
}

class I : B
{
  // ...
}

// A generic class that takes as a parameter the type T, which is bounded to class D
class MyClass<T> where T : D
{
  // ...
}

According to the above code, the classes A, B, C, D, E, F, G, H, I form a hierarchy as shown in Figure 1.

C#. Generics. Base class constraintsFigure 1. Class hierarchy A, B, C, D, E, F, G, H, I. Restriction on base class D

According to Figure 1, when declaring an instance of the class MyClass<T>, it is allowed to use types (classes) D, E, F, G as an argument of type T

// Declare an instance of the class MyClass <T> in which type D acts as an argument
MyClass<D> obj1 = new MyClass<D>(); // Ok!

// The argument is class E, which is a subclass of class D
MyClass<E> obj2 = new MyClass<E>(); // Ok!

// The argument is the class F, which is a subclass of the class D
MyClass<F> obj3 = new MyClass<F>(); // Ok!

// A subclass of class F can also be an argument of type T of class MyClass
MyClass<G> obj4 = new MyClass<G>(); // Ok!

When trying to declare an instance of the class MyClass<T> with any other type of argument, an error will occur at compile time

// An attempt to use another class that is not included in the hierarchy
// where the base class is class D
MyClass<int> obj = new MyClass<int>(); // compile error

// Class A is the base class for class D, but it is not part of the hierarchy
// in which class D is the base class. Therefore, this is also an error.
MyClass<A> obj5 = new MyClass<A>(); // compile error

// Using the .NET Framework Class Type as an Argument is also error
MyClass<System.IO.TextWriter> obj6 = new MyClass<System.IO.TextWriter>(); // compile error

Thus, the constraint on the base class allows you to filter out classes that do not make sense to use as a type argument for a given generic class.

  

5. Interface constraint: where T: Interface

An interface constraint requires the implementation of one or more interfaces as a type argument of a generic class.
As you know, an interface declares a list of methods (elements) that must be implemented in classes that inherit this interface. If an interface name is specified for some generic class as a bound of type T, then this type must contain the implementation of this interface.

Interface constraints apply in the following cases:

  • when to use interface members in a common class;
  • when you need to ensure that only type arguments that implement the specified interface are used.

It is allowed to specify a list of several interfaces after the where keyword. The general form of such a declaration is approximately as follows

class ClassName<T> where T : Interface1, Interface2, ... InterfaceN
{
  ...
}

here

  • Interface1, Interface2, InterfaceN – a list of interface names that type T must implement.

In the case of multiple interfaces, type T must contain the implementation of the methods of all interfaces. If at least one interface is not fully implemented, the compiler will generate an error.

  

6. An example explaining the bound to the interface

Let the following elements be given:

  • interface IInterface, which declares one Print() method;
  • the InformClass class that implements the IInterface interface;
  • the class MyClass, which does not implement the IInterface interface;
  • generic class GenClass<T> in which the type T is limited by the name of the IInterface interface.

Below is the abbreviated program code of the above elements

...

// Restriction on the interface.
// 1. There is an interface with some method
interface IInformation
{
  // Some method in the interface, the method prints a message
  void PrintInfo(string message);
}

// 2. Class that implements the IInfomation interface
class InformClass : IInformation
{
  // Implementation of PrintInfo() method
  public void PrintInfo(string message)
  {
    Console.WriteLine("InformClass: " + message);
  }
}

// 3. A class that DOES NOT IMPLEMENT the IInformation interface
class MyClass
{
  // This class also contains its own PrintInfo() method
  public void PrintInfo(string message)
  {
    Console.WriteLine("MyClass: " + message);
  }
}

// 4. A generic class that contains a constraint on the Information interface
class GenClass<T> where T : IInformation
{
  // ...
}

...

If you try to declare an instance (object) of the GenClass<T> class, in which the T type argument is the InformClass class

// 1. An attempt to declare an instance of a generic class
// in which the type parameter is the InformClass class
GenClass<InformClass> obj1 = new GenClass<InformClass>(); // Ok!

then this code will work correctly. This is because the InformClass class implements the IInformation interface

// 2. A class that implements the IInfomation interface
class InformClass : IInformation
{
  ...
}

If you try to declare an instance of the GenClass<T> class, in which the type argument is the MyClass type, an error will occur at the compilation stage.

// 2. Attempting to use a type parameter of the class MyClass
GenClass<MyClass> obj2 = new GenClass<MyClass>(); // compile error

This is because the MyClass class does not implement the Information interface

// The class does not implement the IInformation interface
class MyClass
{
  ...
}

In this case, the constraint

where T : IInformation

in the class GenClass<T> declaration is triggered.

  

7. Using constraints to establish a relationship between two type parameters. Example

In generic classes when using constraints on the base class

where T : BaseClass

you can establish a link between two type parameters.

In the most general case, the declaration of such a generic class is as follows

class MyClass<T1, T2> where T1:T2
{
  // ...
}

Such a declaration indicates that:

  • the generic class MyClass receives as parameters two types T1, T2;
  • T1 type is bounded to T2 type.

A constraint on a parameter of type T1 is called an explicit type constraint.

Example. Let the following classes be given:

  • class A, which is the base for classes B, C;
  • classes B, C, which are inherited from class A;
  • a generic class GenClass<T1, T2> that uses the constraint of type T1 to type T2.

The program code for the shorthand implementation of the above classes is as follows

// Some class hierarchy
class A
{
  // ...
}

class B : A
{
  // ...
}

class C : A
{
  // ...
}

// Using constraints to establish a relationship between two type parameters.
// In class GenClass type T1 is bounded by the type T2.
class GenClass<T1, T2> where T1 : T2
{
  // ...
}

Now you can instantiate the class MyClass<T1, T2> as follows

// Instantiating the class: GenClass<T1, T2> where T1: T2
GenClass<B, A> obj = new GenClass<B, A>(); // Ok!

The above snippet is correct since class B is a subclass of class A.

If you try to create an instance of the class in a different way

// here is an error, class A is not a subclass of class B
GenClass<A, B> obj = new GenClass<A, B>(); // compile error

// there is also an error, class C is not a subclass of class B
GenClass<C, B> obj2 = new GenClass<C, B>(); // compile error

// and this is an error, class B is not a subclass of class C
GenClass<B, C> obj3 = new GenClass<B, C>(); // error

then a compile-time error will occur.

  


Related topics