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 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 tell the compiler what + and * do when used in conjunction with a Matrix, allowing you to write code like this. 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:

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

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 on screen.

  • 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 the compiler processes the following lines of code:

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

What happens when the compiler encounters the following line?

long l = a + b;

The compiler identifies that it needs to add two integers and assign the result to a long. However, the expression a+b is really just an intuitive and convenient syntax for calling a method that adds two numbers together .The method takes two parameters, a and b, 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:

 double x = d + a; 

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:

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

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 together 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.

Note

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:

 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 + " )"; } 

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:

 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; } } } 

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, so the return type is 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 are 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 non-static 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:

{    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 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:

 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 returns this result:

Vectors

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

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, but 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:

 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 you 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 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:

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

Given that you've already written code to implement essentially the same operation, however, you might prefer to reuse that code by 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 preference. 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 double X = a*b to calculate the dot product of two Vector objects (a and b). 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 that you understand the arithmetic operators, you can check that they work using a simple test method:

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 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 hadn't been overloaded — the addition assignment operator, +=:

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

Although += normally counts as a single operator, it 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 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 <=

C# 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, itdoesn't really make much sense for a comparison to return anything other than a bool.

Note

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 ==:

 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 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)?

Note

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:

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

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:

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 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

Microsoft (R) Visual C# 2005 Compiler version 8.00.50215.33 for Microsoft (R) Windows (R) 2005 Framework version 2.0.50215 Copyright (C) Microsoft Corporation 2001-2005. All rights reserved. Vectors3.cs(5,11): warning CS0660: 'Wrox.ProCSharp.OOCSharp.Vector'         operator == or operator != but does not override Object.Equals(object o) Vectors3.cs(5,11): warning CS0661: 'Wrox.ProCSharp.OOCSharp.Vector'         operator == or operator != but does not override Object.GetHashCode() 

Running the example produces these results at the command line:

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?

It is not possible to overload all of the available operators. The operators that you can overload are listed in the following table.

Category

Operators

Restrictions

Arithmetic binary

+, *, /, -, %

None.

Arithmetic unary

+, -, ++, --

None.

Bitwise binary

&, |, ^, <<, >>

None.

Bitwise unary

!, ~ true, false

The true and false operators must be overloaded as a pair.

Comparison

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

Comparison operators must be overloaded in pairs.

Assignment

+=, -=, *=, /=, >>=, <<=, %=, &=, |=, ^=

You cannot explicitly overload these operators; they are overridden implicitly when you override the indi- vidual operators such as +, -, %, and so on.

Index

[]

You cannot overload the index operator directly. The indexer member type, discussed in Chapter 2, "C# Basics," allows you to support the index operator on your classes and structs.

Cast

()

You cannot overload the cast operator directly. User-defined casts (discussed next) allow you to define custom cast behavior.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

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