The main point of this article is about covariance and contravariance, which is one of the topics I encountered in the .NET interview process, and most cases, I could not get a complete answer.
I'll try to explain this topic, which was added in .NET 4.0 and is widely used in the .NET library, in as simple a language as possible.

Covariance and Contravariance appear mainly in the form of generic delegates and interfaces.
The main purpose of covariance and contravariance is object substitution, that is, to set a restriction on the derivation hierarchy of the generic type that we will create.
Before moving on to the topic, let's consider the conversion of types by a simple process of derivation.
Let's say we have a class hierarchy of the form given below:
public class Database {
public int DatabaseId {
get;
set;
}
public string ConnectionString {
get;
set;
}
public string DatabaseName {
get;
set;
}
public int DatabaseVersion {
get;
set;
}
public string Server {
get;
set;
}
}
/// <summary>
/// For T-SQL
/// </summary>
public class SqlDatabase: Database {
//additional properties/methods go here...
}
/// <summary>
/// For PostgreSql. Supports only versions < 10.0.0
/// </summary>
public class PostgreSqlDatabase: Database {
//additional properties/methods go here...
}
/// <summary>
/// For PostgreSql.Supports only versions > 10.0.0
/// </summary>
public class PostgreSqlDatabase2: Database {
//additional properties/methods go here...
}
Based on this hierarchy, let's look at the conversion rules between parent and child classes:
//from the inheritance perspective it is correct
//because parent knows whos are its children
Database db1 = new SqlDatabase();
Database db2 = new PostgreSqlDatabase();
Database db3 = new PostgreSqlDatabase2();
//no conversion between children. Requires to implement explicit or implicit conversion
SqlDatabase sqlDatabase = new PostgreSqlDatabase();
//from the inheritance perspective it will not be compiled
//because child doesn't "know" about is its parent
SqlDatabase sqlDb = new Database();
As you can see from the examples above, any child class can be "raised" to the level of the parent class, but not the other way around.
Now let's look at such a code example:
static void Main(string[] args) {
object[] array = new string[3];
array[0] = "some text";
array[1] = "second text";
}
As most of us know, the above code example is fully functional. According to the derivation hierarchy, the object acts as the parent type of all types. In this case, we can "raise" the string array to the level of the parent class array (object[]). Here, the process of converting and encapsulating Child types to the parent type level is called covariance. All arrays are covariant.
In covariant processes, the left side of the assignment operation is the parent type, and the right side is the child type. However, covariance is not always a recommended form of behavior. Let's evaluate the following code:
public class User {
public string Name {
get;
set;
}
public byte Age {
get;
set;
}
public string Email {
get;
set;
}
}
class Program {
static void Main(string[] args) {
//covariance goes here...
object[] array = new User[3];
array[0] = new User() {
Age = 45, Email = "[email protected]", Name = "Test"
};
// it is not always a good idea to use covariance....
// facing with error...
array[1] = "and booom....";
}
}
Many interfaces appear in a covariant form on the .NET platform. If you see the keyword out in the generic type section of an interface or delegate, this indicates that the interface or delegate is covariant. For example, look at the System.Collections.Generic namespace through the Object Browser.

As we can see, the familiar IEnumerable and IEnumerator generic interfaces are also covariant!!
//covariance works from CHILD to PARENT
IEnumerable < Database > covariantdatabases = new List < SqlDatabase > ();
//covariance works from CHILD to PARENT. It will not be compiled!
IEnumerable < SqlDatabase > covariantdatabases2 = new List < Database > ();
//contravariance works from PARENT to CHILD
IEnumerable < Database > covariantdatabases = new List < SqlDatabase > ();
//covariance works from CHILD to PARENT. It will not be compiled!
IEnumerable < SqlDatabase > covariantdatabases2 = new List < Database > ();
To better illustrate the point, let's create a covariant interface and see the constraints that apply to it:
public interface IDataProducer < out T > {
T ProduceData();
}
//covariance works from CHILD to PARENT ! no way down!
IDataProducer<Database> producer = null!;
Database db_1 = producer.ProduceData(); //same level
SqlDatabase sqldb_1 = producer.ProduceData(); // moving down to CHILD fails
//covariance works from CHILD to PARENT.
//If type is derived then it is possible to step up to PARENT
IDataProducer<SqlDatabase> secondProducer = null!;
Database db_2 = secondProducer.ProduceData(); // moving to parent
SqlDatabase sqldb_2 = secondProducer.ProduceData(); // same level
In the covariance process, you cannot use a type as an argument to the method because the purpose is to produce the type, not to consume it!






Join the conversation! Your thoughts help the community grow.