Skip to main content

C# - Operator Overloading Part 2 [Intermediate]


Welcome back to the world of psychotic things you should never do if you want readable code! And in this particular case, I'll be talking about what happens in C# when you mix operator overloading with inheritance and polymorphism. If you need a refresher on C# operator overloading, you should check out part 1 of this tutorial, at Overloading Part1. Now, without further ado, lets jump right in.

To start this off, first we are going to need a bunch of code to test the different cases that occur because of polymorphism. Below are three (rather empty) classes with some overloaded operators in them:

public class BaseClass
{
  public static BaseClass operator +(BaseClass arg1, BaseClass arg2)
  {
    Console.WriteLine("Addition in: BaseClass");
    return new BaseClass();
  }

  public static BaseClass operator /(BaseClass arg1, BaseClass arg2)
  {
    Console.WriteLine("Division in: BaseClass");
    return new BaseClass();
  }
}

public class ExtendedClassA : BaseClass
{
  public static ExtendedClassA operator +(ExtendedClassA arg1, ExtendedClassA arg2)
  {
    Console.WriteLine("Addition in: ExtendedClassA");
    return new ExtendedClassA();
  }

  public static ExtendedClassA operator -(ExtendedClassA arg1, BaseClass arg2)
  {
    Console.WriteLine("Subtraction in: ExtendedClassA");
    return new ExtendedClassA();
  }

  public static ExtendedClassA operator *(ExtendedClassA arg1, ExtendedClassA arg2)
  {
    Console.WriteLine("Multiplication in: ExtendedClassA");
    return new ExtendedClassA();
  }
}

public class ExtendedClassB : BaseClass
{
  public static ExtendedClassB operator +(ExtendedClassB arg1, ExtendedClassB arg2)
  {
    Console.WriteLine("Addition in: ExtendedClassB");
    return new ExtendedClassB();
  }
}
 
So here we have three classes: BaseClass, ExtendedClassA, and ExtendedClassB. As you probably guessed from the names, ExtendedClassA and ExtendedClassB extend the class BaseClass. Now, you might notice that these operator overloads are a little odd. But thats ok, because for this testing, all we really care about is which operator function gets called - and writing to the console is an easy way to see that.

Here is the first peice code we will be using to test these operator overloads:
public void TestOverloads()
{
  BaseClass baseClass = new BaseClass();
  ExtendedClassA extA = new ExtendedClassA();
  ExtendedClassB extB = new ExtendedClassB();
  BaseClass extAinBase = new ExtendedClassA();
  BaseClass resultholder;

  resultholder = baseClass + baseClass;
  resultholder = extA + extA;
  resultholder = baseClass + extA;
  resultholder = extA - extA;
  resultholder = extA / extA;
  resultholder = extA + extB;
  resultholder = extA + extAinBase;
  resultholder = extAinBase + extAinBase;
}
 
The first couple lines are just setup, where we make a couple instances of the test classes to use. What we really care about is the output of the resultHolder = lines, which looks like:
Addition in: BaseClass
Addition in: ExtendedClassA
Addition in: BaseClass
Subtraction in: ExtendedClassA
Division in: BaseClass
Addition in: BaseClass
Addition in: BaseClass
Addition in: BaseClass
 
So lets go through these lines one by one and see what they mean. First, we have the line resultholder = baseClass + baseClass producing the result Addition in: BaseClass. This makes perfect sense - adding two instances of BaseClass will call the addition overload defined in BaseClass. The next line, resultholder = extA + extA producing Addition in: ExtendedClassA also makes sense, it shows that adding two instances of ExtendedClassA will call the addition overload defined in ExtendedClassA.

The third line is where it starts to get interesting, where we have resultholder = baseClass + extA printing out the line Addition in: BaseClass. There is no addition overload in either ExtendedClassA or BaseClass that has the signature (BaseClass arg1, ExtendedClassA arg2), so your first guess might be that it should fail. But instead, it treats the ExtendedClassA instance like a BaseClass instance, calling the addition overload in BaseClass.

The fourth line - resultholder = extA - extA printing Subtraction in: ExtendedClassA - shows that even though ExtendedClassA does not have an overload that matches the signature exactly, it will try down casting the arguments in order to find a signature that matches. And in this case it did, it called the subtract overload in ExtendedClassA, which takes an instance of ExtendedClassA and an instance of BaseClass.

A side note on the fourth line - because of what C# does here, there is sometimes potential ambiguity. For instance, take a look at the following code:
public class MessedUpClass : BaseClass
{
  public static MessedUpClass operator -(MessedUpClass arg1, BaseClass arg2)
  {
    Console.WriteLine("Subtraction in: MessedUpClass. BaseClass arg2.");
    return new MessedUpClass ();
  }

  public static MessedUpClass operator -(BaseClass arg1, MessedUpClass arg2)
  {
    Console.WriteLine("Subtraction in: MessedUpClass. BaseClass arg1.");
    return new MessedUpClass ();
  }
}
 
If you try actually use that subtraction overload with two instances of MessedUpClass, you will get a compiler error:
MessedUpClass a = new MessedUpClass();
a = a + a;
//Error: The call is ambiguous between the following methods or properties: 'MessedUpClass.operator -(MessedUpClass, BaseClass)' and 'MessedUpClass.operator -(BaseClass, MessedUpClass)'

Which makes sense - how is the compiler supposed to know which object it should downcast in order to make the call fit one of those signatures?

OK, now back to the output of our test function. On line five, we have resultholder = extA / extA giving us the result Division in: BaseClass. This is an extension of line 4 - here there was no overload in ExtendedClassA that matched, but it down casted both arguments and used the division overload in BaseClass.

On line six, we add an instance of ExtendedClassA with an instance of ExtendedClassB: resultholder = extA + extB which gives us the line Addition in: BaseClass. Here, we have another example of down casting. There is no addition overload in either ExtendedClass that will take one argument as ExtendedClassA and one as ExtendedClassB. But the compiler is able to downcast both and use the addition overload in BaseClass. Again, like with variations of line four, if you go crazy with overloads in this type of case, the compiler is likely to throw ambiguity errors.

Next is line seven: resultholder = extA + extAinBase with the result Addition in: BaseClass. Here we have an instance of ExtendedClassA being held in a BaseClass variable. And it is here we learn that operator overloading is not actually polymorphic. If it was polymorphic, the addition overload in ExtendedClassA would have been called, because at runtime the virtual machine would have seen that extAinBase is actually an instance of ExtendedClassA. But because operator overloads are static (i.e., decided at compile time), and the compiler does not know that extAinBase is anything other than an instance of BaseClass, it has no choice other than using the addition overload defined in BaseClass.

In some ways, you should probably be thankful that operator overloading is not a dynamic (i.e., runtime) operation. In many ways, operator overload is confusing enough already - making it dynamic would make code that used it even less readable than standard overloading.

Last we have line eight adding extAinBase with itself (resultholder = extAinBase + extAinBase) and as expected using the overload defined in BaseClass (Addition in: BaseClass). Nothing surprising about that here, it follows from what was just stated about line seven.

And as yet another reiteration of the fact that overloading is not dynamic, here is a line of code that generates an error:
resultholder = extAinBase * extAinBase;
//Error: Operator '*' cannot be applied to operands of type 'BaseClass' and 'BaseClass'

There is an overload for multiplication in ExtendedClassA, but as you can see here, it ignores that completely, because again, all the compiler knows is that we are trying to multiply two instances of BaseClass (which has no multiplication overload).

And that about covers how operator overloading works with polymorphism and inheritance in C#. One side note - you can't declare operator overloads in an interface (again, because the overloads are decided at compile time, and if its just pointing at an interface, what actual function is the compiler supposed to map the overload to?).

Hope you found all this craziness interesting!

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# Snippet - The Many Uses Of The Using Keyword [Beginner]

What is the first thing that pops into your mind when you think of the using keyword for C#? Probably those lines that always appear at the top of C# code files - the lines that import types from other namespaces into your code. But while that is the most common use of the using keyword, it is not the only one. Today we are going to take a look at the different uses of the using keyword and what they are useful for. The Using Directive There are two main categories of use for the using keyword - as a "Using Directive" and as a "Using Statement". The lines at the top of a C# file are directives, but that is not the only place they can go. They can also go inside of a namespace block, but they have to be before any other elements declared in the namespace (i.e., you can't add a using statement after a class declaration). Namespace Importing This is by far the most common use of the keyword - it is rare that you see a C# file that does not h

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as