Tech Point Fundamentals

Sunday, October 10, 2021

CoVariance and ContraVariance Feature C#

CoVariance and ContraVariance Feature C#


There is a lot of confusion in developers for CoVariance and ContraVariance because there are some topics that are related to this feature like Polymorphism, IN vs OUT Type,  Assignment Compatibility, SubTyping PrincipleLiskov Substitute Principle, and More Derived vs. Less Derived Type in C#. In this article, we will walk through all these things in detail.


Please visit our Youtube Channel for Interview Questions and Answers videos by the below link:





The agenda for today is to cover the following:

Q01. What do you understand by Variance in C#?
Q02. What is InVariance in C#?
Q03. What is CoVariance in C#? Do you know any real CoVariance examples available in C#?
Q04. What is ContraVariance in C#? Do you know any real ContraVariance examples available in C#?
Q05. What is the difference between CoVariance and ContraVariance in C#?
Q06. What is Variant Generic Interface?
Q07. What is Variant Generic Delegate?
Q08. What is Variant Array?
Q09. What is the use of IN and OUT Keyword in C#?
Q10. What is Liskov Substitute Principle?
Q11. What is the difference between Dynamic Polymorphism, SubTyping Principle, Liskov Substitute Principle, and Variance feature in C#?
Q12. What do you understand by More Derived vs. Less Derived Type in C#?




Introduction


In this article before directly jumping on CoVariance and ContraVariance, we have to understand some features which are related to this like Polymorphism, IN vs OUT TypeAssignment CompatibilitySubTyping Principle, Liskov Substitute Principle, and More Derived vs. Less Derived Type in C#. This will help to understand the CoVariance and ContraVariance in a better way.



Polymorphic Behaviour


1. Polymorphic behaviour is the ability of a subtype (derived type) to be treated as if it were an instance of the supertype (base type)

2. Any method that accepts an instance of supertype (base type) will also be able to accept an instance of subtype (derived type) without any explicit casting and also without any type sniffing. 

3. Now variance will come into the picture when you introduce another type that might use Supertype and/or Subtype through a generic parameter.

4. In short Covariance and Contravariance are polymorphism extensions to the arrays, delegates, and generics.


Please read more about Polymorphism here.  You can also watch the video for the same here.


class-inheritance




SubTyping Principle and Covariant Returns


1. C# has a relationship defined for types and objects in the form of Inheritance, Polymorphism, and Subtyping.

2. A Derived Class reference can be assigned to the Base Class reference, this is known as SubTyping

For example:  GrandFather_object = Father_object

3. If a method has an input parameter of type GrandFather class, the caller can pass an instance of Father class as input.

4. If a method has a GrandFather class return type, it can return a Father class object also. 

5. But if a method is virtual and is overridden in a derived class, the override method can return any subtype reference but it cannot change the return type from the overriding method signature (until C# 8).

6. Thanks to C# 9 for introducing the Covariant Return Type. So it allows the overriding method to declare a "More Derived" return type than the method it overrides.

7. Covariant Return Type is a feature that enables you to override a method of a base class with a method in the derived class to return a more specific type.  Earlier versions of C# did not allow returning a different type in an overridden method of a derived class.


Please visit the live program for the covariant return type of C# 9 here.


Please read more about the Shadowing in C# where you can change the signature of the hiding member here.  You can also watch the video for the same here.




Liskov Substitute Principle (LSP)


This is one of the most common SOLID Principles of OOPS. It says:

"If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program."

In short, the object of the parent class can be replaced with the object of the child class without any breaking changes.

For example: GrandFather_object = Father_object




More Derived vs. Less Derived Type | More Specific vs Less Specific Type


1. Inheritance is the most common feature of any OOPS language. So in popular object-oriented languages like C#, we can make a transition between related types.

2. In C# language every type derives from the Object class. So the Object class is the Super Base Class in C#.

3. So Object class is always the Less Derived type compared to any other type.

4. A Child type is always more derived compare to the immediate parent.

5. For Example:  (Less Derived) Object < GrandFather < Father < Child (More Derived)

Here:

a) Object is a Less Derived Type or Less Specific Type compare to any other type (GrandFather, Father, and Child).

b) GrandFather is More Derived or More Specific compare to the Object type. At the same time, it is Less Derived or Less Specific compare to Father.

c) Father is More Derived or More Specific compare to the GrandFather or Object type. At the same time, it is Less Derived or Less Specific compare to Child.

d) Finally Child is Most Derived or Most Specific compare to any other parent type (GrandFather, Father, and Object).



Assignment Compatibility: (Conversion, Casting, Boxing and Unboxing)


1. When we do an assignment in C#, the Left Hand Side type must be compatible with the Right Hand Side type otherwise you will get a compile-time error. So you can say that in C# assignments work with covariant mode. 

2. We can assign More Derived types to Less Derived types without any problems. But the reverse is not true.

3. If the LHS type is compatible, the compiler does the implicit conversion and boxing internally for you.

4. Using Covariance, we can convert all types to Object types and used them for assignments.

5. Covariance preserves the assignment compatibility and Contravariance opposite the covariance functionality.


Please watch the Conversion vs Casting video here and Boxing vs UnBoxing video here.

You can also watch the IS vs AS Operator video here and Typeof vs GetType video here for more details.




Generics


1. Generics were introduced in C# 2.0 with Visual Studio 2005 in the year 2005.

2. Generics are used to decouple the type from the method, class, or interface.

3. Generics introduces the concept of type parameters to .NET, which makes it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code. 

4. Generics reduces both the cost or risk of runtime casts and boxing operations.




REF vs OUT vs IN Parameter Types


1. The OUT and IN keywords are introduced in C# 4.0.

2. We have used the REF, OUT, and IN keywords in the methods to define the type of input parameters:
    
i) REF is used to state that the parameter passed may be modified by the method.
ii) OUT is used to state that the parameter passed must be modified by the method.
iii) IN is used to state that the parameter passed cannot be modified by the method.

3. But when OUT and IN are used with generics, they have very special meanings. Since C# 4.0, you can specify IN and OUT parameters on generic types as well.

4. These keywords are used to specify whether generic types should behave CoVariantly or ContraVariantly.

5. These keywords refer to the kind of arguments that can be placed in certain positions. The IN marks input-position while the OUT marks output-position.


Please watch the REF vs OUT vs IN type parameters video here 




Variance Feature or Behaviour


1. In popular OOPS languages like C# and Java, we can make a transition between related types. So variance is nothing but an interchange between types.

2. In C# variance is a reference transition between related types through inheritance.

3. Generally, variance is a term applied to the expected behaviour of subtypes in a class hierarchy containing complex types.

4. Variance concept was first introduced for generics, then for delegates, and finally for arrays.

5. Without involving generics, all inheritance in C# is invariant.

6. The C# programming language provides support for variance features in two ways: covariance and contravariance.

7. And both CoVariance and ContraVariance features are supported only for reference types.

8. In the C# programming language, we make the transition between types using the Variance feature in three ways: InVarianceCoVariance, and ContraVariance.



InVariance Feature or Behaviour


1. The InVariance feature of OOPS says that the type to be assigned (LHS type) and the type assigned  (RHS type) must be the same. 

2. The InVariance behaviour of C# prevents you from assignments of non-compatible type implicitly.

3. In this behaviour there is no switch(interchange) between a Child (More Derived) type and a Parent (Less Derived) type.



CoVariance and ContraVariance Fundamentals


1. CoVariance and ContraVariance features are introduced in C# 2.0 with Visual Studio 2005 in the year 2005.

2. But CoVariance and ContraVariance features for Generics are introduced with C# 4.0 with Visual Studio 2010.

3. Both CoVariance and ContraVariance are supported only for reference types, not value types. It is only allowed for the generic interface, delegate, and array.

4. CoVariance preserves the assignment compatibility and ContraVariance opposite the CoVariance functionality.

5. CoVariance enables you to use a more derived type (more specific) than originally specified while ContraVariance enables you to use a less derived type (less specific). 



CoVariance Fundamental Points: (<out T> Type)


1. CoVariance enables you to pass a derived type where a base type is expected. i.e. Covariance enables you to use a more derived type (more specific) than originally specified.

2. CoVariance allows interface methods to have more derived return types than those defined by the generic type parameters.

3. CoVariance is like the variance of the same kind. Because the derived classes are considered to be the same kind of base class (sub-type) that adds extra functionalities to the base type. 

4. In the CoVariance feature you can switch from a Child type to a Parent type. i.e.
More Derived -> Less Derived Transition

5. For example you can use IEnumerable<string> in place where IEnumerable<object> is expected. 

6. In C# 4.0,  the introduction of the covariance feature was a breaking change to the existing logic.


covariance-example


CoVariance Fundamental Rules


1. CoVariance applies to generic parameters type used as method return type.

2. CoVariance is applied to the output parameters (OUT) only.

3. We must have to use the OUT type for creating Covariance in generics.

4. In the CoVariant generic interface, any method can only use it as a return type (OUT), not as an input type (IN) parameter.

5. But if you have a contravariant generic delegate as a method parameter, you can use the covariant type as a generic type parameter for the delegate. 

In the above example Acknowledge(Action<T> callback) T is used as an input parameter for the contravariant Action delegate.

6. The CoVariant generic type cannot be used as a generic constraint for the interface methods. You can use only contravariant or invariant types for generic constraints.

In the above example, DoSomething<T2>() method will cause the compile-time error.

In the Example: interface ICovariant<out T> 

a) The type T is a covariant parameter.
b) The out keyword means that the interface can only OUTPUT the type T.  i.e it can only be used as a return type for any method of the interface.
c) Any method of the interface cannot take an input of type T.
d) The OUT keyword allows the compiler to use the type T or a more abstract/derived type.

CoVariant Real Examples


1. IEnumerable<out T> and IEnumerator<out T> are Covariant in C#.
2. IQueryable<out T> is Covariant in C#.
3. IReadOnlyList<out T> is Covariant in C# since .NET Framework 4.5.
4. IReadOnlyCollection<out T> is Covariant in C# since .NET Framework 4.5.




ContraVariance Fundamental Points: (<in T> Type)


1. ContraVariance enables you to use a less derived type (less specific).

2. ContraVariance allows interface methods to have argument types that are less derived than that specified by the generic parameters. 

3. CotraVariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.

4. In the Contravariance feature you can switch from a Parent type to a Child type. i.e.
 Less Derived -> More Derived transition

5. For example it allows you to pass IComparable<object> as an argument of a method taking IComparable<string>.

contra-variant-example





ContraVariance Fundamental Rules


1. ContraVariane is applied to the input parameters (IN) only. 

2. We must have to use IN type for creating ContraVariance

3. In the ContraVariant generic interface, any method can only use it as an input parameter, not as a return type parameter. 


In the Example: interface IContravariant<in T>:

a) The type T is a contravariant parameter.
b) The IN keyword means that the interface can only accept an INPUT of the type T.  
c) It cannot return type T from any method.
d) The IN keyword allow the compiler to use the type T or a less abstract/derived type. 

ContraVariant Real Examples

    
1. IComparer<in T> and IComparable<in T> are ContraVariant in C#.
2. IEqualityComparer<in T> is ContraVariant in C#.
3. IGrouping<TKey,TElement> is ContraVariant in C#.




Combined CoVariance and ContraVariance: (<in Tin, out Tout> Type)


1. A generic type interface can be both covariant and contravariant. So a generic interface can have multiple generic types i.e. one covariant and the other contravariant.

2. When writing our own generic interfaces we can control how generic types will be used.

3. A generic interface that has either covariant or contravariant generic type parameters is called variant.

variant-example



In the example: interface IVariant<in Tin, out Tout>:

a) It can take Tin as input parameters type only.
b) It can take Tout as a return type only.
c) It is both CoVariant and ContraVariant.




Variance: (<T> Type)


Why not both? Why do we bother about the IN and OUT?

Of course, the generic type in the interface can be both covariant and contravariant. And that’s what we usually meet in projects.

If you do not specify any IN or OUT type for the generics, they can be used for both IN and OUT types.

Invarient-Example


In the example: interface IRepository<T>:

T can be used for both input parameters and as method return type.

Real Examples


1. IList<T> is Variant in C#.
2. ICollection<T> is Variant in C#.




Variance in Generic Interfaces


1.  By default, generic interfaces are invariant. So the generic passed type must be the same as the generic type to be assigned.

2. A generic interface type can have both covariant and contravariant type parameters.

3. All the generic collections are of the invariant type because they used generic interfaces as their infrastructure. 


Please read the article for interface default methods in C# here.


Fundamental Rules for Extending Variance Generic Interface



1. A classes that implement variant interfaces are always invariant, unlike interfaces. 

For example, although List<T> implements the covariant interface IEnumerable<T>, you cannot implicitly convert List<String> to List<Object>

2. When you extend a variant generic interface, you must have to use the IN and OUT keywords to explicitly specify whether the derived interface supports variance or not.

3. The inherited variant generic interface does not require IN or OUT keyword to be specified again. So you cannot specify the IN and OUT keywords while extending a variant interface.

4. But if a generic type parameter T is declared covariant in one interface, you cannot declare it contravariant in the extending interface, or vice versa. 

5. When you explicitly implement the same variant generic interface with different generic type parameters in one class, it can create ambiguity

The compiler does not produce any error in this case, but it's not specified which interface implementation will be chosen at run time. 




Variance in Generic Delegates


1. Covariance and contravariance are also supported by delegates for matching method signatures.

2. A generic delegate type can have both covariant and contravariant type parameters. But REF, IN, and OUT parameters in C# can't be marked as a variant.

3. Covariance permits a method to have a return type that is a subtype of the one defined in the delegate.

4. Contravariance permits a method to have a parameter type that is a base type of the one defined in the delegate type.

5. This enables you to assign to delegates not only methods that have matching signatures but also methods that return more derived types (covariance) or that accept parameters that have less derived types (contravariance) than that specified by the delegate type.

6. The combined variant delegates are not recommended because it does not support variant delegate conversion and expects delegates to be of exactly the same type.


variant-delegate





Real Variant Delegate Examples


1. Func<T> delegate is a variant generic delegate available in C#. It has both covariant return types and contravariant parameter types.

public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);

2. Action<T> and Predicate<T> delegates are contravariant generic delegates available in C#. It has contravariant parameter types.

public delegate void Action<in T>(T obj);
public delegate bool Predicate<in T>(T obj);




Variance in Array


1. Arrays and collections show covariance for reference types and invariance for value types. 

2. CoVariance for arrays enables implicit conversion of an array of a more derived type to an array of a less derived type. But this operation is not type-safe.

variance-in-array


Live Demo



Conclusion


1. Covariance and contravariance are supported only for reference types, not value types. 

2. It is only allowed for the generic interface, delegate and array.

3. Covariance enables you to use a more derived type (more specific) than originally specified while Contravariance enables you to use a less derived type (less specific). 

4. Without involving generics, all inheritance in C# is invariant. In this case, there is no switch(change) between a Child (More Derived) type and a Parent (Less Derived) type.



No comments:

Post a Comment

Please do not enter any HTML. JavaScript or spam link in the comment box.