Operator overloading in C# is pretty easy and extensible, and there is
plenty of documentation out there on the internet for most kinds of
operator overloading. In fact the MSDN docs
themselves
do a good job of explaining how to do basic operator overloading. But
what is not covered anywhere are the fringe cases of operator
overloading - how overloading interacts with inheritance, and other
things even weirder than that.
Now, you might be asking why anyone would ever want to do anything crazy
like that - and the answer is probably never. Operator overloading by
itself in code is often questionable - there are many cases where it
actually decreases the readability of the code. So crazy operator
overloading, where what actually happens behind the scenes is even more
confusing than normal, is probably something to avoid at all costs. But
hey, we aren't here to debate techniques - we're here to do some crazy
overloading!
Basic Operator Overloading
Just in case you need a quick overview of basic operator overloading in
C#, here you go. To overload a unary operator for a class, you write a
method in that class with the following signature:
public static SomeClass operator ++(SomeClass arg)
For instance, this signature is for overloading the ++ operator for
SomeClass. The declaration is actually pretty simple - most of the stuff
here is normal for a method declaration. The only odd things are the
'operator' keyword (which lets .NET know that this a operator overload
function), and the name of the function, instead of being an actual
name, is the operator being overloaded. The public and static keywords
make sense (in fact it won't compile without them), because it wouldn't
make much sense to have a private or non-static operator overload. There
are some constraints on the arguments and the return types (and the
constraints differ depending on the operator being overloaded), but for
the most part you can treat them like standard method arguments and
return types. The contents can be any sort of calculation - as long as
what is returned matches the return type.
For overloading binary operators, the only difference is that the
operator function takes two arguments. Here, we are overloading the
addition operator:
class SomeClass
{
private int someValue;
public SomeClass(int val)
{ someValue = val; }
public static SomeClass operator +(SomeClass arg1, SomeClass arg2)
{
return new SomeClass(arg1.someValue + arg2.someValue);
}
}
And of course now you can now use this overload like the following:
SomeClass var1 = new SomeClass(1);
SomeClass var2 = new SomeClass(2);
SomeClass var3 = var1 + var2;
And now var3.someValue holds the value 3.
Now that you know the syntax for overloading operators, you might be
wondering what operators you are allowed to overload. Well, with C#,
the answer is almost every single one. For binary operators, you can
overload any of the following:
+, -, !, \~, ++, --, true, false
Most of those probably make sense, but what does it mean to overload
true or false? Well, first, take a look at the following implementation:
class SomeClass
{
private int someValue;
public SomeClass(int val)
{ someValue = val; }
public static bool operator true(SomeClass arg1)
{ return arg1.someValue >= 0 }
public static bool operator false(SomeClass arg1)
{ return arg1.someValue < 0 }
}
You can't overload only true or false - you have to overload them both
if you want one (which makes sense - how could you even mentally define
one without defining the other).
And this is how it can be used:
SomeClass var1 = new SomeClass(1);
if(var1)
{
//do something
}
var1 would be treated as true, because the true operator returned true.
I don't actually think overloading the operators true and false is very
useful - I think a much better way to get the same functionality is to
create an operator overload to implicitly cast your object to a boolean
(we will get into how to do that later on).
Back to what you can actually overload. For binary operators, you can
overload the following:
+, -, *, /, %, &, |, \^, \<\<, >>
All of those should make sense, so I'll move on to comparison operators:
==, !=, \<, >, \<=, >=
The only caveat here is that these operators need to be overloaded in
pairs - i.e., if you overload ==, you must overload !=. This makes
sense, for the same reason that you must overload true and false as a
pair.
You can't directly overload && or || - but not to worry, you can get an
overloaded 'effect' by overloading either the true and false operators,
or by defining an implicit cast to boolean.
The same goes for all the 'assignment plus operator' operators:
+=, -=, *=, /=, %=, &=, |=, \^=, \<\<=, >>=
You can't overload them directly, but if you think about it, every one
of them is a combination of = and some operator that you can overload -
and so by overloading, say, +, you end up overloading +=.
You can't overload any of these at all:
=, ., ?:, ->, new, is, sizeof, typeof
But please, I would hope you didn't actually want to do that in the
first place.
One cool operator that doesn't fit in any of these categories is the
index operator: []
You can actually 'overload' it (Microsoft doesn't call it overloading -
they call it 'declaring an indexer'), and it comes in handy sometimes.
It doesn't use operator overload syntax - instead, it uses the C#
property syntax. For instance:
class SomeClass
{
private int[] someValues = new int[3]{1,2,3};
public int this [int index]
{
set{ someValues[index] = value; }
get{ return someValue[index]; }
}
}
And now you can use it like this:
SomeClass var1 = new SomeClass();
var1[0] = var1[2];
And after that operation, the array in var1 looks like: {3,2,1}.
There is one other set of overloading operations to talk about before we
start pushing operator overloading to its limits - and that is cast
overloading. There are two types of cast overloads: implicit and
explicit. Explicit casting is when you write code like:
double val = 4.555;
int intVal = (int)val;
That (int) is an explicit cast.
Implicit casting is what occurs in the following situation:
int intVal = 1;
double val = intVal;
Here, the int intVal is implicitly casted to a double - you as the
programmer don't even have to think about it.
So lets take a look at some cast overloads:
class SomeClass
{
private int someValue;
public SomeClass(int val)
{ someValue = val; }
public static implicit operator bool(SomeClass arg1)
{
return arg1.someValue >=0;
}
public static explicit operator int(SomeClass arg1)
{
return arg1.someValue;
}
}
Here, we have an implicit overload for casting to bool, and an explicit
overload for casting to int. The syntax is close to other operator
overloads, but not quite the same. We don't have to declare a return
type in the normal spot, because the name of the cast is the return
type.
Now that we have these casts defined, they can be used like this:
SomeClass var1 = new SomeClass(1);
int val = -1;
if(var1)
val = (int)var1;
Here, var1 will evaluate to true in the if, thanks to the implicit
boolean cast. And then, val will get the value 1, because of the
explicit int cast.
But enough with the simple stuff - what happens if we do the unexpected
with operator overloading?
Some Strange Arguments
So operator overloads are just methods, right? So that means we should
be able to do all sorts of crazy things with them. Sadly (or
fortunately, depending on your perspective) that is not the case. For
instance, you can't use the keywords ref or out with the arguments. For
unary operators (like ++ and --) both the argument and the return type
have to be the class that the overload is declared for. So that means
that the following won't compile:
class SomeClass
{
public static SomeClass operator ++(int arg1)
{ return new SomeClass(); }
}
//Error: The parameter type for ++ or -- operator must be the containing type
or this:
class SomeClass
{
public static int operator ++(SomeClass arg1)
{ return new 0; }
}
//Error: The parameter type for ++ or -- operator must be the containing type
But it makes sense that that wouldn't compile, because the 'input' and
the 'output' of ++ when your actually using it in code need to be the
same type.
Don't lose hope, though - because you can do odd things with the
arguments and return types of binary operators. For instance, this is
legal:
class SomeClass
{
public static int operator +(SomeClass arg1, int arg2)
{ return new 0; }
}
And will work in the following statement:
SomeClass var1 = new SomeClass();
int i = var1 + 2;
However it will not work with:
int i = 2 + var1;
In this second case it will actually try and add var1 as an integer to
'2' - i.e., use the regular integer '+'. This is because the type of the
first argument to the overload function needs to match the type of the
value before the '+', and the second argument type has to match what is
after the '+'. To get it to work in both directions you need to write
two functions:
class SomeClass
{
public static int operator +(SomeClass arg1, int arg2)
{ return new 0; }
public static int operator +(int arg1, SomeClass arg2)
{ return new 0; }
}
Be aware that declaring overloads like this has the potential to mess
with your use of the 'assignment plus operator' operators, like +=. In
this case, because of how we declared these overloads, the following
code will cause a compiler error:
SomeClass var1 = new SomeClass();
var1 += 5;
//Error: Cannot implicitly convert type 'int' to 'SomeClass'
It throws this error because the overloaded operator is returning an
int, and we are trying to place it into a SomeClass variable.
One thing to note - the type of at least one of the arguments to the
overload function must be the type of the class that you are declaring
it in. So something like the following will not compile:
class SomeClass
{
public static SomeClass operator +(int arg1, int arg2)
{ return null; }
}
//Error: One of the parameters of a binary operator must be the containing type
Another thing to be aware of - you don't want to declare the 'same'
overload in two different classes. By the 'same', I mean two overloads
that have identical signatures. For instance, the following won't
compile:
class OneClass
{
public static OneClass operator +(OneClass arg1,
TwoClass arg2)
{ return null; }
}
class TwoClass
{
public static OneClass operator +(OneClass arg1,
TwoClass arg2)
{ return null; }
}
//Error: The call is ambiguous between the following methods or properties:
'OneClass.operator +(OneClass, TwoClass)' and 'TwoClass.operator +(OneClass, TwoClass)'
It won't compile because the compiler can't figure out which of the
overload methods to use.
That covers it for part one of crazy operator overloading. Check back
soon for part two, where we will deal with polymorphism and inheritance,
as well a number of other weird twists which no one in their right mind
would use in their code.
Comments
Post a Comment