Tech Point Fundamentals

Sunday, December 5, 2021

Method Overloading and Overloading Dilemma C# - Part 3

Method Overloading and Overloading Dilemma C# - Part 3

method-overloading-part3

This is the continuation of the "Method Overloading and Overloading Dilemma" article series. This is the third and last part of this series. Please read the previous parts by below link:


I will recommend you to please read the previous two parts before continuing this final part here.





Introduction


Till now we have seen below Overloading Dilemmas in Part-1 and Part-2:

      1. Case 1: Implicit Conversion and Overloading Rule
      2. Case 2: Implicit Boxing and Overloading Rule
      3. Case 3: Dynamic Parameter and Overloading Rule
      4. Case 4: Object Parameter and Overloading Rule
      5. Case 5: Optional Parameter and Overloading Rule
      6. Case 6: Named Argument and Overloading Rule
      7. Case 7: Params Parameter and Overloading Rule
      8. Case 8: Reference Parameter and Overloading Rule
      9. Case 9: Return Types and Overloading Rule
      10. Case 10: Method Modifiers and Overloading Rule


In this part, we will continue with Case 11.



Case 11. Access Modifiers and Overloading Rule


In C# there are multiple access modifiers like private, protected, public, internal, private protected, etc. They are used to define the accessibility level of the methods.

We know that the access modifiers are a crucial part of a method while defining any method. There are some special scenarios also where we cannot specify access modifiers explicitly.

But these access modifiers are not considered as part of the method signature, so two methods cannot be overloaded only based on the access modifiers.

case-11


That's why we are getting a compile-time error here:  

Error CS0111 Type 'Program' already defines a member called 'Example11' with the same parameter types



Case 11A: Int vs Int and Overloading Rule


Let me tweak the above code and help the compiler to resolve the above issue.

case-11a


In the above example for the first call i.e Example11A(10) why the second method taking the Int16 parameter is not being called? 

Let's see the IL code of the above example first:

case-11a-ilcode


Here both the values 10 and 100 are treated as short int (ldc.i4.s means Push num onto the stack as int32, short form.). However, for the value 10 compile binds the first method that takes an Int32 as a parameter, while for the value 100 compiler binds the second method taking Int16 as a parameter because we have defined the variable num as short explicitly. Isn't strange?



Case 12. Inheritance and Overloading Rule


A method that is defined in a class can also be overloaded by the derived class. It is known as inheritance-based overloading

In VB.Net when you are overloading a method of the base class in the derived class, then you must use the keyword “Overloads”. But in C# there is no need to use any keyword while overloading a method either in the same class or in the derived class.

But this inheritance-based overloading can cause an un-deterministic effect. It may also lead to the Base Class Fragile Problem. Please learn more about the Fragile Base Class Problem here and the solution available in the .Net Framework for the Fragile Base Class here.

When the compiler is looking for an overloaded instance method, it considers the compile-time class of the "target" call and looks at methods declared there. If it can't find anything suitable match, it then looks at the parent class and at last the grandparent class.

So if there are two matching methods at different levels of the hierarchy, the "deeper" one will be chosen first, even if it is not a "better function member" for the call.

In the case of inheritance-based overloading, Implicit Type Conversion takes precedence over the parent class method.


case-12



Here the target of the method call is an expression of type Derived, so the compiler first looks at the Derived class. Although there's an implicit conversion involved from int to double in order to match the method, still the compiler binds the derived class method, it doesn't consider the Base class method at all.

The reason for this is to reduce the risk of the brittle or fragile base class problem, where the introduction of a new method to a base class could cause problems for consumers of classes derived from it.



Case 12A: Named Argument and Inheritance Based Overloading


Let me force the compiler to call the base class method instead of the derived class. Please note here I am not creating the base class object here.

case-12a


In the above example for the first call, now the compiler is calling the base class method instead of the derived class due to the named argument rule.



Case 12B: Shadowing and Overloading Rule


Shadowing is a VB.Net concept while hiding is a C#.Net concept. Both terms "Shadowing" and "Hiding" are the same in C#. When you say "shadow" you're usually talking about scope while when you say "hiding" you're usually talking about inheritance. 

When two-member use the same name, one of them can hide, or shadow, the other one. By shadowing the child class can create its own version of the base class method.

Please read more about shadowing here.

case-12b


In the above example for the second call (20.5), the compiler is calling the base class method because there is no match in the derived class at all.



Case 12C: Complete Shadowing and Overloading Rule


Let me do the complete shadow of the base class method in the derived class.

case-12c


In the above example for the first call (10), the compiler is calling the derived class method and for the third call (30), the compiler is calling the base class method because we have used a named argument.



Case 13. Overriding Method and Overloading Rule


Virtual methods are used to implement dynamic polymorphism. The virtual method may be implemented differently in different inherited classes, and the call to these methods are decided at runtime.

When a virtual method is invoked, the run-time type of the object is checked for an overriding member. The overriding method in the most derived class is called, which might be the original method if no derived class has overridden the virtual method.

Please read more about method overriding here.

There's one aspect of this behavior that is particularly surprising though. What counts as a method being "declared" in a class for the compiler? It turns out that if you override a base class method in a child class, that doesn't count as declaring it. 

case-13


Why compiler is generating compile-time errors here? 

Is it due to the reason that we cannot do both overriding and hiding for the same method at the same time or Is it due to the compiler understanding that the overriding method is being considered as a declared member of the derived class itself?

Whatever it is, but for the above example, the compiler is generating a compile-time error for now:

Error CS0111 Type 'Program.Derived' already defines a member called 'Example13' with the same parameter types



Case 13A: Overloading the Overriding Method


Now let me help the compiler to solve the problem here.

case-13a


Here, why the compiler is not binding the derived class overriding method or the base class virtual method for the first call (10)?

We have seen in the previous Case 13, that the overriding method of the derived class has participated in the overloading, that's why the compiler was generating a compile-time error. This compiler behavior isn't strange?

Yes, this compiler behavior is strange, but for the first call (10), the derived class non-overriding method is being called because implicit type conversion takes precedence over the parent class method. And why the overriding method is not being called we will see in the next Case 13B.



Case 13B: Overloading Resolution for Overriding Method


Let me resolve your curiosity here. Allow me to reframe the parameters for the above-overloaded methods.

case-13b


Finally, you win!! Here the compiler is calling the best matched overloaded methods for both base class and derived class contexts. Is it?

Wait...Wait!!!  There is something you have missed here. Let me show you the IL code first:

case-13b-ilcode


For the calls which are made using the base class object context, we are fine because there is only one method so the compiler has performed implicit conversion and bind that method for the int type value (10) also.

But the gotcha is in the case of the derived class context call. For the int value 10, the compiler has bound the derived method taking the int parameter. What about the double value 20.5 i.e new Derived().Example13B(20.5)?

The IL code is showing it has bound the base class virtual method taking double type parameter:

IL_0036:  newobj     instance void MethodOverloading.Program/Derived::.ctor()
IL_003b:  ldc.r8     20.5
IL_0044:  callvirt   instance void MethodOverloading.Program/Base::Example13B(float64)

Yes, you are thinking right!!! For the derived class, the calling context is resolved at run-time instead of compile-time, and that's what our Run-Time Binding or Run-Time Polymorphism.

So technically the two methods of the derived class are not overloaded basically.



Case 13C: Dynamic Parameter and Inheritance Based Overloading


Let me tweak the above example with a dynamic parameter instead of int in the derived class.

case-13c


Here all the calling contexts are resolved at compile time itself. Though there is a need of boxing for both the calls of the derived class context, but still compiler does not consider the overriding method taking a double type parameter at all.

Let me show you the IL code, which will prove my point here.

case-13c-ilcode





Case 14. Delegates and Overloading Rule


Finally, we are at the last case of overloading dilemmas.  Since delegates are the way to call any method in C#. How can we miss the delegates here? Let's see the delegate first a little bit here:

  1. Delegates are used to pass methods as arguments to other methods. 
  2. A delegate is nothing but a typesafe function pointer in C#. So a delegate is a type that represents references to methods with a particular parameter list and return type.
  3. When you instantiate a delegate, you can associate its instance with any method having a compatible signature and return type.
  4. You can then invoke (or call) the method through the delegate instance.
  5. Delegate types are all incompatible with each other, even though their signatures are the same.
  6. So overloading delegates are not possible, but the overloaded method can be passed to the delegates.
  7. Two delegate instances are considered to be equal if they have the same method targets.
  8. With C# 4, delegates can also be covariant and contravariant

Please read more about variant delegates here.

case-14


Here we have passed an overloaded method to the delegate, but it is invoking only the first overloaded method taking double type parameter for both int and double type parameters because we have defined the delegate with the double type input parameter. 



Case 14A: Overloading Delegates


Let's try to overload the delegate also.

case-14a


Here compiler is generating compile-time errors because delegates cannot be overloaded in C#:

Error CS0102 The type 'Program' already contains a definition for 'Example14ADelegate'



Case 15. Generic Delegates and Overloading Rule


Now there is a problem here, we are unable to use the polymorphic behavior of overloaded methods with delegates and also we cannot overload delegates. 

Don't worry, this problem is resolved by Microsoft in C# 4 with the introduction of the variant generic delegates.  Please read more about the variant generic delegate here.

case-15


Here we have defined a contravariant generic delegate. So finally, now our problem is resolved here. 

Let's see the IL code of the above example.

case-15-ilcode


Here for the int type (10), the second method having an int type parameter is bounded while for double type (10.5), the first method is bound.



Overloading Risks and Breaking Changes


We have seen too many overloading gotchas in this article series. With the passage of time, new changes are coming into the .Net Framework and C# itself, which may cause a breaking change to your code.

For example, the introduction of covariance and contravariance feature in C# 4 was a breaking change to the existing codes.

A developer has written below overloaded method in C# 3, which is working and running fine as expected. For all the calls it is picking the first method taking object parameter.

case-xa


Now Microsoft has released the new version of C# i.e C# 4.0. Our code is still working fine with the .Net Framework 3.5 because till then variant feature is not introduced by the .Net team.


case-xb


Now suddenly Microsoft has released the new version of the .Net Framework i.e .Net 4.0 with covariance and contravariance features for C# 4.



Boom!!! Now, this pretty feature killed your working logic badly, without throwing any warning or error to the developer.

case-xc


Now, since IEnumerable<T> is covariant in .Net 4 the compiler will pick the second method for the second and third call because the conversion to that is better than the conversion to object. And this change occurs with no warnings of any kind. For the first call, it is picking the first method because the variant feature is not applied for the value types.

A similar breaking change occurs in C# 2 with delegates also.



Summary


Two methods are overloaded only if they have different signatures at compile time. Two method signatures are considered as different only if they have:  

  1. Different number of parameters
  2. Different types of parameters
  3. Different order of parameters

Two methods are not considered as overloaded if they are different only in terms of:  

  1. Different return types (int, string, double, etc.)
  2. Different access modifiers (private, public, protected, internal, etc.)
  3. Different method modifiers ( static, virtual, sealed, abstract, partial, etc.)  
  4. Different reference parameter modifiers (ref, out, in)
  5. Different boxing parameter types (object, dynamic)
  6. Different parameter types (params, optional, mandatory)
  7. Overriding Method and Normal Method (Inheritance Based Overloading)

Optional Parameters take precedence over Implicit Type conversion when deciding which method definition to bind by the compiler.

In the case of Inheritance Based Overloading, Implicit Type Conversion takes precedence over the parent class method.




Examples


 

Live Demo



Conclusion


Overloading is a minefield with lots of rules and regulations which can interact in evil ways. It is very difficult to predict the overload resolution sometimes. So be careful while proceeding with overloading.

This is particularly useful for constructors. The polymorphism feature is one of the main pillars of OOPS, but it involves a lot of risks also. So if possible it's better to create alternative methods with clear names instead.

No comments:

Post a Comment

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