Operator Overloading

 
Chapter 3 - Object-Oriented C#
bySimon Robinsonet al.
Wrox Press 2002
  

In this and the indexers. We start by discussing operator overloading in this section.

Operator overloading is something that will be familiar to C++ developers. However, since the concept will be new to both Java and VB developers, we'll explain the concept of it here. C++ developers will probably prefer to skip straight to the main example.

The point of operator overloading is that you don't always just want to call methods or properties on class instances. Often you need to do things like adding quantities together, multiplying them, or performing logical operations such as comparing objects. Suppose for example 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:

   Matrix a, b, c;     // assume a, b and c have been initialized     Matrix d = c * (a + b);   

By overloading the operators, you can effectively tell the compiler what + and * does to a Matrix , allowing you to write code like the above. If we were coding in a language that didn't have operator overloading, we'd have presumably have to define methods to perform those operations. The result would probably look a bit messy, like this:

   Matrix d = c.Multiply(a.Add(b));   

With what you've learned so far, operators like + and * have been strictly for the predefined data types, and for good reason: the compiler automatically knows what all the common operators mean for those data types. For example, it knows how to add two long s or how to divide one double by another double , and can generate the appropriate intermediate language code. When we define our own classes or structs, however, we have to tell the compiler everything: what methods are available to call, what fields to store with each instance, and so on. Similarly, if we want to use operators like + and * on our own classes, we'll have to tell the compiler what the relevant operators mean in the context of that class. The way we do that is by defining overloads for the operators.

We should also say that there are many classes you could write for which operator overloading would not be relevant. For example, multiplying together two MortimerPhones customers just doesn't make any sense conceptually. There are, however, a large number of cases where you might want to write overloads for operators, including:

  • From the world of mathematics, almost any mathematical object: 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 will also use mathematical or coordinate- related objects when calculating positions on screen.

  • A class that represents an amount of money (for example, in a financial program).

  • A word processing or text analysis program might have classes representing sentences, clauses and so on, and you might wish to use operators to combine sentences together (a more sophisticated version of concatenation for strings).

The other thing we should stress is that overloading isn't just concerned with arithmetic operators. We 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 wish 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 will produce a compilation error unless you explicitly overload == to tell the compiler how to perform the comparison.

There are a large number of situations in which being able to overload operators will help greatly to allow us to generate more readable, intuitive code.

Over the next few sections we're going to illustrate operator overloading by developing a new example, a struct called Vector that represents a 3D mathematical vector. In the world of mathematics, vectors can be added together or multiplied by other vectors or by numbers. Incidentally, in this context we'll use the term scalar, which is math-speak for a simple number - in C# terms that's just a double . However, before we can understand how to overload operators, we need a bit of theoretical understanding about how operators work, which we cover next.

The fact that our example will be developed as a struct rather than a class is not significant. Operator overloading works in just the same way for both structs and classes.

How Operators Work

In order to understand how to overload operators, it's quite useful to think about what happens when the compiler encounters an operator - and for this we'll take the addition operator, + , as an example. Suppose it meets the lines of code:

   int a = 3;     uint b = 2;     double d = 4.0;     long l = a + b;     double x = d + a;   

Consider the line:

 long l = a + b; 

Saying a+b is really just a very intuitive, convenient syntax for saying that we are calling a method to add the numbers together. The method takes two parameters, a and b , and returns their sum. For integers the compiler and the JIT compiler between them actually make sure this line is implemented internally as highly efficient inline code which does the addition using hardware, but the principle as far as the C# code is concerned is the same as if it was any ordinary method call.

The compiler will see it needs to add two integers and return a long , so it does the same thing as it does for any method call - it looks for the best matching overload of + given the parameter types. Adding two integers is fine - for modern processors there will be a specific machine code instruction to do this. The result will be an integer as well, so it will need to be cast to a long , which is allowed in C#, so there are no problems.

The next line demonstrates that there are actually quite a few overloads of " + " kicking around already, even before we start defining our own classes:

   double x = d + a;   

It appears that this overload takes a double and an int , adds them, and returns a double . Actually, on most machines, that's probably not directly possible - rather, we'll need to implicitly convert the int to a double and add the two double s together. In other words, we identify the best matching overload of " + " here as being a version of the operator that takes two double s as its parameters. Adding together two double s is a very different affair from adding two integers. Floating-point numbers are stored internally in modern processors as a mantissa and an exponent. Adding them involves bit-shifting the mantissa of one of the double s 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. There will be hardware support for this in a modern Pentium processor, but it's still a completely different operation from adding two integers. Finally, the compiler needs to make sure it can cast the result to the required return type if necessary. In this case there's no problem - adding two double s gives another double , which is what x is declared as.

Now, we're in a position to see what happens if the compiler finds something like this:

   Vector vect1, vect2, vect3;     // initialise vect1 and vect2     vect3 = vect1 + vect2;     vect1 = vect1*2;   

Here Vector is the struct that we shall define shortly. The compiler will see that it needs to add two Vector s, vect1 , and vect2 together. It'll look for an overload of the + operator which takes two Vector s as its parameters. This operator must also return either a Vector or something that can be implicitly converted to a Vector , so that we can set vect3 equal to the return value. The compiler therefore needs to find a definition of an operator that has a signature something like this:

   public static Vector operator + (Vector lhs, Vector rhs)   

If it finds one, 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 it can't find anything suitable, it'll raise a compilation error, just as it would do if it couldn't find an appropriate overload for any other method call. Of course, a similar approach is used for the * and any other operator too:

   vect1 = vect1*2;   

Addition Operator Overloading Example: The Vector Struct

Now that we've delved into the theory of how operators work, it's time to introduce our example. We're going to write the struct Vector , which represents a 3-dimensional vector.

If you're worried that mathematics is not your strong point, don't worry. We'll keep things very simple. As far as we 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 upwards (in height). Combine the three numbers together and you get the total movement. For example, if x=3.0 , y=3.0 , and z=1.0 , (which we'd normally write as (3.0, 3.0, 1.0) then you're moving 3 units East, 3 units North and rising upwards by 1 unit.

The significance of addition should be clear here. 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 will always write c=a+b , where a and b are the vectors and c is the result. We want to be able to use our Vector struct the same way.

The following is the definition for Vector - containing the member fields, constructors, and a ToString() override so we can easily view the contents of a Vector , and finally that operator overload:

   namespace Wrox.ProCSharp.OOCSharp     {     struct Vector     {     public double x, y, z;     public Vector(double x, double y, double z)     {     this.x = x;     this.y = y;     this.z = z;     }     public Vector(Vector rhs)     {     x = rhs.x;     y = rhs.y;     z = rhs.z;     }     public override string ToString()     {     return "( " + x + " , " + y + " , " + z + " )";     }   

Note that in order to keep things simple I've left the fields as public . I 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 a lot more complicated. Besides, for a simple struct like this, which to a large extent is just a grouping of the x , y, and z components , I think I can legitimately get away with keeping the fields public . Note that I have not supplied a default constructor, since it is not permitted for structs. Instead I've supplied 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 our second constructor, the one that takes just one argument, are often termed copy constructors , since they effectively allow you to initialize a class or struct instance by copying another instance.

Now let's have a closer look at the operator overload.

   public static Vector operator + (Vector lhs, Vector rhs)     {     Vector result = new Vector(lhs);     result.x += rhs.x;     result.y += rhs.y;     result.z += rhs.z;     return result;     }     }     }   

How does this work? The important syntax is in the declaration of the operator. It is declared in much the same way as a method, except the operator keyword tells the compiler it's actually an operator overload we're defining. operator is followed by the actual symbol for the relevant operator. The return type is whatever type you get when you use this operator. In our case, adding two Vector s gives us another Vector , so the return type is Vector . For this particular override of + , the return type is the same as the containing class, but that's not necessarily the case. Later on we'll define operators that return other types. The two parameters are the things you're operating on. For an operator that takes two parameters like " + ", the first parameter is the object or value that goes on the left of the " + " sign, and the second parameter is the object or value that goes to the right of it.

Finally, note that the operator has been declared as static , which means that it is associated with the struct or class, not with any object, and so does not have access to a this pointer; C# requires that operator overloads are declared in this way. That's fine because the lhs and rhs parameters between them cover all the data the operator needs to know to perform its task.

Now we've dealt with the syntax for the operator + declaration, we can look at what happens inside the operator:

 {          Vector result = new Vector(lhs);          result.x += rhs.x;          result.y += rhs.y;          result.z += rhs.z;          return result;       } 

This part of the code is exactly the same as if we 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 above. We simply add the individual doubles x , y , and z individually.

Now all we need to do is write some simple test harness code to test our Vector struct. Here it is:

   static void Main()     {     Vector vect1, vect2, vect3;     vect1 = new Vector(3.0, 3.0, 1.0);     vect2 = new Vector(2.0, -4.0, -4.0);     vect3 = vect1 + vect2;     Console.WriteLine("vect1 = " + vect1.ToString());     Console.WriteLine("vect2 = " + vect2.ToString());     Console.WriteLine("vect3 = " + vect3.ToString());     }   

Saving this code as Vectors.cs , and compiling and running it gives this result:

  Vectors  vect1 = ( 3 , 3 , 1 ) vect2 = ( 2 , -4 , -4 ) vect3 = ( 5 , -1 , -3 ) 

Adding More Overloads

Although the Vectors sample demonstrates in principle how you overload an operator, there's still not that much that we can do with the Vector struct. In real life, you can multiply vectors together, add and subtract them, and compare their values. In this section and the next we'll develop the sample by adding a few more operator overloads. Not the complete set that you'd probably need for a real and fully functional Vector type, but enough to demonstrate some other aspects of operator overloading. In the next section we'll see how to overload the process of comparing objects, but first, in this section we'll focus on more arithmetic operators. We'll overload multiplying vectors by a scalar and multiplying vectors together.

Multiplying a vector by a scalar simply means multiplying each component individually by the scalar: for example, 2 * (1.0, 2.5, 2.0) gives (2.0, 5.0, 4.0) . The relevant operator overload looks like this:

   public static Vector operator * (double lhs, Vector rhs)     {     return new Vector(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);     }   

This by itself, however, is not sufficient. If a and b are declared as type Vector , it will allow us to write code like this:

   b = 2 * a;   

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:

   b = a * 2;   

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 above 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. We have not provided such an overload. The compiler can't start swapping the order of parameters so the fact that we've provided an overload that takes a double followed by a Vector is not sufficient. We 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 explicitly breaking down the vector multiplication operation in the same way that we've done for all operators so far:

   public static Vector operator * (Vector lhs, double rhs)     {     return new Vector(rhs * lhs.x, rhs * lhs.y, rhs *l hs.z);     }   

Given that we've already written code to implement essentially the same operation, however, you might prefer to reuse that code by instead writing:

   public static Vector operator * (Vector lhs, double rhs)     {     return rhs * lhs;     }   

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. Which you prefer is to some extent a matter of taste. In the actual sample code with this chapter we've gone for the second version, which looks neater and because we want to illustrate the idea in action. This version also makes for more maintainable code, since it saves duplicating the code to perform the multiplication in two separate overloads.

The next operator to be overloaded is Vector multiplication. In mathematics there are 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 we're introducing that example, so that we can 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, since it can be used to calculate various other quantities. Certainly, if you ever end up writing any code that displays any 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. At any rate, what concerns us here is that we want people to be able to write double X = a*b where a and b are vectors and what they intend is for the dot product to be calculated. The relevant overload looks like this:

   public static double operator * (Vector lhs, Vector rhs)     {     return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;     }   

Now, we've defined the arithmetic operators, we can check that they work fine using a simple test harness routine:

 static void Main() {   // stuff to demonstrate arithmetic operations     Vector vect1, vect2, vect3;     vect1 = new Vector(1.0, 1.5, 2.0);     vect2 = new Vector(0.0, 0.0, -10.0);     vect3 = vect1 + vect2;     Console.WriteLine("vect1 = " + vect1);     Console.WriteLine("vect2 = " + vect2);     Console.WriteLine("vect3 = vect1 + vect2 = " + vect3);     Console.WriteLine("2*vect3 = " + 2*vect3);     vect3 += vect2;     Console.WriteLine("vect3+=vect2 gives " + vect3);     vect3 = vect1*2;     Console.WriteLine("Setting vect3=vect1*2 gives " + vect3);     double dot = vect1*vect3;     Console.WriteLine("vect1*vect3 = " + dot);   } 

Running this code ( Vectors2.cs ) produces this result:

  Vectors2  vect1 = ( 1 , 1.5 , 2 ) vect2 = ( 0 , 0 , -10 ) vect3 = vect1 + vect2 = ( 1 , 1.5 , -8 ) 2*vect3 = ( 2 , 3 , -16 ) vect3+=vect2 gives ( 1 , 1.5 , -18 ) Setting vect3=vect1*2 gives ( 2 , 3 , 4 ) vect1*vect3 = 14.5 

This shows that the operator overloads have given us the correct results, but if you look at the test harness code closely, you might be surprised to notice that we've actually sneakily used an operator that we hadn't overloaded - the addition assignment operator "+=":

   vect3 += vect2;   Console.WriteLine("vect3 += vect2 gives " + vect3); 

Amazingly, it gave us the correct result! So what's going on? Although += normally counts as a single operator, it really can be broken down into two steps - the addition and the assignment. Unlike C++, 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 ++ operator, and for -= , -- , *=, and /= (although in order for these operators to work, you'll need to have respectively overloaded the - , * , and / operators). For a struct, C# always interprets assignment as meaning just copy the contents of the memory where the struct is stored, while for a class C# always copies just the reference.

Overloading the Comparison Operators

There are six comparison operators in C#, and they come in three pairs:

  • == and !=

  • > and <

  • > = and < =

The significance of the pairing is twofold. First, within each pair, the second operator should always give exactly the opposite (Boolean) result to the first (whenever the first returns true , the second returns false , and vice versa), and second, C# always requires you to overload the operators in pairs. If you overload " == ", then you must overload " != " too, otherwise you get a compiler error.

In the case of overriding == and != , you should strictly also override the Equals() method which all classes and structs inherit from Chapter 5.

One other restriction is that the comparison operators must return a bool . This is the fundamental difference between these operators and the arithmetic ones. The result of adding or subtracting two quantities, for example, might theoretically be any type depending on the quantities. We've already seen that multiplying two Vector s can be understood to give a scalar. Another example involves the .NET base class, System.DateTime , which we've briefly encountered . It's possible to subtract two DateTime s, 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 .

Apart from these differences, overloading the comparison operators follows the same principles as overloading the arithmetic operators. Comparing quantities isn't always as simple as you'd think, however, as the example we use will illustrate. We're going to override the == and != operators for our Vector class. Let's start off with == . Here's our implementation of == :

   public static bool operator == (Vector lhs, Vector rhs)     {     if (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z)     return true;     else     return false;     }   

This approach simply compared vectors 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 )?

We also need to override the != operator. The simple way to do it is like this:

   public static bool operator != (Vector lhs, Vector rhs)     {     return ! (lhs == rhs);     }   

As usual, we'll quickly check that our override works with a test harness. This time we'll define three Vector s, two of which are close enough that they should count as equal, and compare all the vectors:

 static void Main()       {   Vector vect1, vect2, vect3;     vect1 = new Vector(3.0, 3.0, -10.0);     vect2 = new Vector(3.0, 3.0, -10.0);     vect3 = new Vector(2.0, 3.0, 6.0);     Console.WriteLine("vect1==vect2 returns  " + (vect1==vect2));     Console.WriteLine("vect1==vect3 returns  " + (vect1==vect3));     Console.WriteLine("vect2==vect3 returns  " + (vect2==vect3));     Console.WriteLine();     Console.WriteLine("vect1!=vect2 returns  " + (vect1!=vect2));     Console.WriteLine("vect1!=vect3 returns  " + (vect1!=vect3));     Console.WriteLine("vect2!=vect3 returns  " + (vect2!=vect3));   } 

Compiling and running this code, (the Vectors3.cs sample in the code download) produces these results at the command line, which demonstrates the correct results. Notice also the compiler warning because we haven't overridden Equals() for our Vector . For our purposes here, that doesn't matter.

  csc Vectors3.cs  Microsoft (R) Visual C# .NET Compiler version 7.00.9466 for Microsoft (R) .NET Framework version 1.0.3705 Copyright (C) Microsoft Corporation 2001. All rights reserved. Vectors3.cs(5,11): warning CS0660: 'Wrox.ProCSharp.OOCSharp.Vector' defines         operator == or operator != but does not override Object.Equals(object o) Vectors3.cs(5,11): warning CS0661: 'Wrox.ProCSharp.OOCSharp.Vector' defines         operator == or operator != but does not override Object.GetHashCode() Vectors3 vect1==vect2 returns  True vect1==vect3 returns  False vect2==vect3 returns  False vect1!=vect2 returns  False vect1!=vect3 returns  True vect2!=vect3 returns  True 

Which Operators Can You Overload?

There are quite a number of operators in C#, some of which you can overload, and some of which you can't. Operators that you are allowed to overload include:

Category

Operators

Restrictions

Arithmetic binary

+ , * , / , - , %

none

Arithmetic unary

+ , - , ++ , --

none

Bitwise binary

&, , ^ , <<, >>

none

Bitwise unary

! , ~ , true , false

none

Comparison

== , != , > = , <, < = , >

must be overloaded in pairs

This list leaves some gaps, but the gaps are there for logical reasons. A number of operators cannot be overloaded explicitly, but are evaluated in terms of other operators that can be overloaded. This includes the arithmetic and bitwise assignment operators += , -= , *= , /= , >> = , << = , %= , & = , =, and ^= , as well as the conditional logical operators && and (these last operators are evaluated using & and ). C++ developers will be surprised to learn that [] and () cannot be overloaded. The reason is that C# achieves the same functionality by other means: using indexers in place of [] overloading, and user -defined casts in place of () overloading; we cover user-defined casts in the next chapter.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net