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
Post a Comment