Operator Overloading
This section looks at another type of member that
you can define for a class or a struct: the operator overload.
Operator overloading is something that will be
familiar to C++ developers. However, because the concept will be
new to both Java and Visual Basic developers, we explain it here.
C++ developers will probably prefer to skip ahead to the main
example.
The point of operator overloading is that you don’t
always just want to call methods or properties on objects. Often
you need to do things like adding quantities together, multiplying
them, or performing logical operations such as comparing objects.
Suppose that you had defined a class that represents a mathematical
matrix. Now in the world of math, matrices can be added together
and multiplied, just like numbers. So it’s quite plausible that
you’d want to write code like this:
By overloading the operators, you can tell the
compiler what + and * do when used in conjunction with a Matrix object, allowing you to write code like the
above. If you were coding in a language that didn’t support
operator overloading, you would have to define methods to perform
those operations. The result would certainly be less intuitive, and
would probably look something like this:
With what you’ve learned so far, operators like
+ and * have
been strictly for use with the predefined data types, and for good
reason: the compiler knows what all the common operators mean for
those data types. For example, it knows how to add two longs or how to divide one double by another double,
and can generate the appropriate intermediate language code. When
you define your own classes or structs, however, you have to tell
the compiler everything: what methods are available to call, what
fields to store with each instance, and so on. Similarly, if you
want to use operators with your own types, you’ll have to tell the
compiler what the relevant operators mean in the context of that
class. The way you do that is by defining overloads for the
operators.
The other thing we should stress is that
overloading isn’t just concerned with arithmetic operators. You
also need to consider the comparison operators, ==, <, >, !=, >=, and <=. Take
the statement if (a==b). For classes,
this statement will, by default, compare the references
a and b. It
tests to see if the references point to the same location in
memory, rather than checking to see if the instances actually
contain the same data. For the string
class, this behavior is overridden so that comparing strings really
does compare the contents of each string. You might want to do the
same for your own classes. For structs, the == operator doesn’t do anything at all by default.
Trying to compare two structs to see if they are equal produces a
compilation error unless you explicitly overload == to tell the compiler how to perform the
comparison.
A large number of situations exist in which being
able to overload operators will allow you to generate more readable
and intuitive code, including:
-
Almost any mathematical object such as
coordinates, vectors, matrices, tensors, functions, and so on. If
you are writing a program that does some mathematical or physical
modeling, you will almost certainly use classes representing these
objects.
-
Graphics programs that use mathematical or
coordinate-related objects when calculating positions onscreen.
-
A class that represents an amount of money
(for example, in a financial program).
-
A word processing or text analysis program
that uses classes representing sentences, clauses and so on; you
might want to use operators to combine sentences (a more
sophisticated version of concatenation for strings).
However, there are also many types for which
operator overloading would not be relevant. Using operator
overloading inappropriately will make code that uses your types far
more difficult to understand. For example, multiplying two
DateTime objects just doesn’t make any
sense conceptually.
How Operators Work
To understand how to overload operators, it’s
quite useful to think about what happens when the compiler
encounters an operator. Using the addition operator (+) as an example, suppose that the compiler
processes the following lines of code:
What happens when the compiler encounters the
following line?
The compiler identifies that it needs to add two
integers and assign the result to a long. However, the expression myInteger + myUnsignedInt is really just an
intuitive and convenient syntax for calling a method that adds two
numbers together. The method takes two parameters, myInteger and myUnsignedInt, and returns their sum. Therefore, the
compiler does the same thing as it does for any method call. It
looks for the best matching overload of the addition operator based
on the parameter types; in this case, one that takes two integers.
As with normal overloaded methods, the desired return type does not
influence the compiler’s choice as to which version of a method it
calls. As it happens, the overload called in the example takes two
int parameters and returns an
int; this return value is subsequently
converted to a long.
The next line causes the compiler to use a
different overload of the addition operator:
In this instance, the parameters are a double and an int, but as
it happens there isn’t an overload of the addition operator that
takes this combination of parameters. Instead, the compiler
identifies the best matching overload of the addition operator as
being the version that takes two doubles as its parameters, and
implicitly casts the int to a
double. Adding two doubles requires a
different process than adding two integers. Floating-point numbers
are stored as a mantissa and an exponent. Adding them involves
bit-shifting the mantissa of one of the doubles so that the two exponents have the same
value, adding the mantissas, then shifting the mantissa of the
result and adjusting its exponent to maintain the highest possible
accuracy in the answer.
Now, you’re in a position to see what happens if
the compiler finds something like this:
Here, Vector is the
struct, which is defined in the following section. The compiler
will see that it needs to add two Vector
instances, vect1 and vect2, together. It’ll look for an overload of the
addition operator, which takes two Vector instances as its parameters.
If the compiler finds an appropriate overload,
it’ll call up the implementation of that operator. If it can’t find
one, it’ll look to see if there is any other overload for
+ that it can use as a best match -
perhaps something that has two parameters of other data types that
can be implicitly converted to Vector
instances. If the compiler can’t find a suitable overload, it’ll
raise a compilation error, just as it would if it couldn’t find an
appropriate overload for any other method call.
Operator Overloading Example: The Vector Struct
This section demonstrates operator
overloading through developing a struct named Vector that represents a 3-dimensional mathematical
vector. Don’t worry if mathematics is not your strong point - we’ll
keep the vector example very simple. As far as you are concerned, a
3D-vector is just a set of three numbers (doubles) that tell you
how far something is moving. The variables representing the numbers
are called x, y, and z: x tells you how far something moves east,
y tells you how far it moves north, and
z tells you how far it moves upward (in
height). Combine the three numbers and you get the total movement.
For example, if x=3.0, y=3.0, and z=1.0 (which
you’d normally write as (3.0, 3.0, 1.0),
you’re moving 3 units East, 3 units North, and rising upward by 1
unit.
You can add or multiply vectors by other vectors or
by numbers. Incidentally, in this context, we use the term
scalar, which is
math-speak for a simple number - in C# terms that’s just a
double. The significance of addition
should be clear. If you move first by the vector (3.0, 3.0, 1.0) then you move by the vector
(2.0, -4.0, -4.0), the total amount you
have moved can be worked out by adding the two vectors. Adding
vectors means adding each component individually, so you get
(5.0, -1.0, -3.0). In this context,
mathematicians write c=a+b, where
a and b are
the vectors and c is the resulting
vector. You want to be able to use the Vector struct the same way.
|
|
Tip |
The fact that this example will be developed
as a struct rather than a class is not significant. Operator
overloading works in the same way for both structs and classes.
|
The following is the definition for Vector - containing the member fields, constructors,
and a ToString() override so you can
easily view the contents of a Vector,
and finally that operator overload:
This example has two constructors that require the
initial value of the vector to be specified, either by passing in
the values of each component or by supplying another Vector whose value can be copied. Constructors like
the second one that takes single Vector
argument are often termed copy
constructors, because they effectively allow you to initialize
a class or struct instance by copying another instance. Note that
to keep things simple, the fields are left as public. We could have made them private and written corresponding properties to
access them, but it wouldn’t have made any difference to the
example, other than to make the code longer.
Here is the interesting part of the Vector struct - the operator overload that provides
support for the addition operator:
The operator overload is declared in much the same
way as a method, except the operator
keyword tells the compiler it’s actually an operator overload
you’re defining. The operator keyword is
followed by the actual symbol for the relevant operator, in this
case the addition operator (+). The
return type is whatever type you get when you use this operator.
Adding two vectors results in a vector, therefore, the return type
is also a Vector. For this particular
override of the addition operator, the return type is the same as
the containing class, but that’s not necessarily the case as you
see later in this example. The two parameters are the things you’re
operating on. For binary operators (those that take two
parameters), like the addition and subtraction operators, the first
parameter is the value on the left of the operator, and the second
parameter is the value on the right.
C# requires that all operator overloads be declared
as public and static, which means that they are associated with
their class or struct, not with a particular instance. Because of
this, the body of the operator overload has no access to nonstatic
class members and has no access to the this identifier. This is fine because the parameters
provide all the input data the operator needs to know to perform
its task.
Now that you understand the syntax for the addition
operator declaration, you can look at what happens inside the
operator:
This part of the code is exactly the same as if you
were declaring a method, and you should easily be able to convince
yourself that this really will return a vector containing the sum
of lhs and rhs as defined. You simply add the members
x, y, and
z together individually.
Now all you need to do is write some simple code to
test the Vector struct. Here it is:
Saving this code as Vectors.cs and compiling and running it returns this
result:
Vectors
Adding More Overloads
In addition to adding vectors, you can
multiply and subtract them and compare their values. In this
section, you develop the Vector example further by adding a few
more operator overloads. You won’t develop the complete set that
you’d probably need for a fully functional Vector type, just enough to demonstrate some other
aspects of operator overloading. First, you’ll overload the
multiplication operator to support multiplying vectors by a scalar
and multiplying vectors by another vector.
Multiplying a vector by a scalar simply means
multiplying each component individually by the scalar: for example,
2 * (1.0, 2.5, 2.0) returns (2.0, 5.0, 4.0). The relevant operator overload
looks like this:
This by itself, however, is not sufficient. If
a and b are
declared as type Vector, it will allow
you to write code like this:
The compiler will implicitly convert the integer
2 to a double
in order to match the operator overload signature. However, code
like the following will not compile:
The thing is that the compiler treats operator
overloads exactly like method overloads. It examines all the
available overloads of a given operator to find the best match. The
preceding statement requires the first parameter to be a
Vector and the second parameter to be an
integer, or something that an integer can be implicitly converted
to. You have not provided such an overload. The compiler can’t
start swapping the order of parameters so the fact that you’ve
provided an overload that takes a double
followed by a Vector is not sufficient.
You need to explicitly define an overload that takes a Vector followed by a double as well. There are two possible ways of
implementing this. The first way involves breaking down the vector
multiplication operation in the same way that you’ve done for all
operators so far:
Given that you’ve already written code to implement
essentially the same operation, however, you might prefer to reuse
that code by writing:
This code works by effectively telling the compiler
that if it sees a multiplication of a Vector by a double, it
can simply reverse the parameters and call the other operator
overload. The sample code for this chapter uses the second version,
because it looks neater and illustrates the idea in action. This
version also makes for more maintainable code, because it saves
duplicating the code to perform the multiplication in two separate
overloads.
Next, you need to overload the multiplication
operator to support vector multiplication. Mathematics provides a
couple of ways of multiplying vectors together, but the one we are
interested in here is known as the dot
product or inner product, and it
actually gives a scalar as a result. That’s the reason for this
example, to demonstrate that arithmetic operators don’t have to
return the same type as the class in which they are defined.
In mathematical terms, if you have two vectors
(x, y, z) and (X, Y,
Z), then the inner product is defined to be the value of
x*X + y*Y + z*Z. That might look like a
strange way to multiply two things together, but it’s actually very
useful, because it can be used to calculate various other
quantities. Certainly, if you ever end up writing code that
displays complex 3D graphics, for example using Direct3D or
DirectDraw, you’ll almost certainly find your code needs to work
out inner products of vectors quite often as an intermediate step
in calculating where to place objects on the screen. What concerns
us here is that we want people using your Vector to be able to write doubleX=a*b to calculate the dot product of two
Vector objects (a and b). The relevant
overload looks like this:
Now that you understand the arithmetic operators,
you can check that they work using a simple test method:
Running this code (Vectors2.cs) produces the following result:
Vectors2
This shows that the operator overloads have given
the correct results, but if you look at the test code closely, you
might be surprised to notice that it actually used an operator that
wasn’t overloaded - the addition assignment operator, +=:
Although +=
normally counts as a single operator, it can be broken down into
two steps: the addition and the assignment. Unlike the C++
language, C# won’t actually allow you to overload the = operator, but if you overload +, the compiler will automatically use your overload
of + to work out how to carry out a
+= operation. The same principle works
for the all of the assignment operators such as -=, *=, /=, &=, and so
on.
Overloading the Comparison Operators
C# has six comparison operators, and they
come in three pairs:
-
== and
!=
-
> and
<
-
>= and
<=
The C# language requires that you overload these
operators in pairs. That is, if you overload ==, you must overload !=
too; otherwise, you get a compiler error. In addition, the
comparison operators must return a bool.
This is the fundamental difference between these operators and the
arithmetic operators. The result of adding or subtracting two
quantities, for example, can theoretically be any type depending on
the quantities. You’ve already seen that multiplying two
Vector objects can be implemented to
give a scalar. Another example involves the .NET base class
System.DateTime. It’s possible to
subtract two DateTime instances, but the
result is not a DateTime; instead it is
a System.TimeSpan instance. By contrast,
it doesn’t really make much sense for a comparison to return
anything other than a bool.
|
|
Tip |
If you overload ==
and !=, you must also override the
Equals() and GetHashCode() methods inherited from System.Object, otherwise you’ll get a compiler
warning. The reasoning is that the Equals() method should implement the same kind of
equality logic as the == operator.
|
Apart from these differences, overloading the
comparison operators follows the same principles as overloading the
arithmetic operators. However, comparing quantities isn’t always as
simple as you’d think. For example, if you simply compare two
object references, you will compare the memory address where the
objects are stored. This is rarely the desired behavior of a
comparison operator, and so you must code the operator to compare
the value of the objects and return the appropriate Boolean
response. The following example overrides the == and != operators for
the Vector struct. Here’s the
implementation of ==:
This approach simply compares two Vector objects for equality based on the values of
their components. For most structs, that is probably what you will
want to do, though in some cases you may need to think carefully
about what you mean by equality. For example, if there are embedded
classes, should you simply compare whether the references point to
the same object (shallow comparison) or
whether the values of the objects are the same (deep comparison)?
|
|
Tip |
Don’t be tempted to overload the comparison
operator by calling the instance version of the Equals() method inherited from System.Object. If you do and then an attempt is made
to evaluate (objA == objB), when objA happens
to be null, you
will get an exception as the .NET runtime tries to evaluate
null.Equals(objB). Working the other way
around (overriding Equals() to call the
comparison operator) should be safe.
|
You also need to override the != operator. The simple way to do it is like
this:
As usual, you should quickly check that your
override works with some test code. This time you’ll define three
Vector objects and compare them:
Compiling this code (the Vectors3.cs sample in the code download) generates
this compiler warning because you haven’t overridden Equals() for your Vector.
For our purposes here, that doesn’t matter, so we will ignore
it.
csc Vectors3.cs
Running the example produces these results at the
command line:
Vectors3
Which
Operators Can You Overload?
It is not possible to overload all of the
available operators. The operators that you can overload are listed
in the following table.
|