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 operator overloading example, The point of operator overloading is that you do not 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 we-rid of math, matrices can be added together and multiplied, just like numbers. Therefore, it is quite plausible that you would want to write code like this:
Matrix a, b, C;
// assume a, band 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 object, allowing you to write code like the preceding. If you were coding in a language that did not 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 have 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 it fan 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 will 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 is not concerned just with arithmetic operators. You also need to consider the comparison operators, =v, <. », ! =, >=, 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 ~ring 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 does not 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 does not 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:
int myInteger = 3;
uint myUnsignedInt = 2;
double myDouble = 4.0;
long myLong = myInteger + myUnsignedInt;
double myOtherDouble = myDouble + mylnteger;
What happens when the compiler encounters the following line?
long myLong = myInteger + myUnsignedlnt;
The compiler identifies that it needs to add two integers and assign the result to a long. However, the expression mylnteger + 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 nlVmal overloaded methods, the desired return type doee-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 myOtherDouble = myDouble + mylnteger;
In this instance, the parameters are a double and an int, but there is not an overload of the addition operator that takes this combination of parameters. Instead, the compiler identifies the best matching overload of tQeaddition operator as being the version that takes two doubles as its parameters, and it implicitly casts the int to a double. Adding two doubles requires a different process from adding two integers. Floating-point numbers are stored as a mantissa and an exponent. Adding them involves bitshifting the mantissa of one of the doubles so that the two exponents have the same value, adding the mantissas, thert shifting the mantissa of the result and adjusting its exponent to maintain the highest possible accuracy in the answer.
Now, you are in a position to see what happens if the compiler finds something like this:
Vector vectl. vect2. vect3;
II initialize vectl and vect2
vect3 = vectl + vect2;
vectl = vectl*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, vectl and vect2, together. It will look for an overload of the addition operator, which takes two Vector instances as its parameters.
If the compiler finds an appropriate overload, it will call up the implementation of that operator. If it cannot find one, it will 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 cannot find a suitable overload, it will raise a compilation error, just as it would if it could not 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. Do not worry if mathematics is not your strong point will keep the vector example very Simple. As far as you are concerned, a 3D-vector is just a set of three numbers (doubles) that tel! 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=l. 0 (which you would 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 is just a double. The significance of addition should be clear. If you move first by the vector (3. 0, 3. 0, l. 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. 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, a ToString ( ) override so.Jou 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 bf’ copied. Constructors like the second one that takes a 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 would not 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;
The operator overload is declared in much the same way as a method, except that the operator keyword tells the compiler it is actually an operator overload you are 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 is not necessarily the case as you will see later in this example. The two parameters are the things you are 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.
Note that it is convention to name your left-hand parameters lhs (for left-hand side) and your righthand parameters rhs (jar right-hand side).
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 non-static class members and has no access to the this identifier. This is fine because the p..arameters 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;
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:
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 will
not develop the complete set thatyou’d probably need for a fully functional Vector type, but 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
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 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 as 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. Youhave not provided such an overload. The compiler cannot 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. Youneed 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 have done for all operators so far:
Given that you have 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, which 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.
Inmathematicalterms, if you have two vectors (x, y, z ) and (X, Y, z),thentheinnerproductis defined to ~ the value oj x *X + y*Y + z*z. That might look like a strange way to multiply two things together, but it is 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 will 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 inner 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:
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, +=:
vect3. += vect2;
ConsolewriteLine(‘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 the C++ language, C# will not actually allow you to overload the = operator, but if you overload +, the compiler will automatically use your overload of + to work out how to’ perform a +=operation. The same principle works for 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 <=
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 have 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. Time Span instance. By contrast, it doesn’t really make much sense for a comparison to return anything other than a bool.
If you overload == and! =, you must also override the Equals () and GetHashCode () methods inherited from System.”Object; otherwise, you’ll get a compiler WGming. The reasoning isthat 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 might 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 is 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)?
A shallow comparison is where the objects point to the same point in memory, whereas deep comparisons are working with values and properties of the object to deem equality. You want to perform equality checks depending on the depth to help you decide what you will want to verify
Don’t be tempted to overload the comparison operator /Jy 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 this is:
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 (theVectors3 .cs sample in the code download) generates the following compiler warning because you haven’t overridden Equals () for your Vector. For our purposes here, thatdoes not matter, so we will ignore it.
Microsoft (R) Visual C’ 2008 Compiler version 3.05.20706.1 for Microsoft (R) .NET Framework vetaion 3.5 Copyright (C) Microsoft Corporation. All rights reserved.
Running the example produces these resultsat the command line:
vectl ••vect2 returna True
vectl ••vect3 returns False.
vect2==vect3 returns False
vectll=vect2 returns False
vectll=vect~ returns True
vect2lavect3 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.
Microsoft (R) Vi.ual C’ 2008 Compiler version 3.05.20706.1
for Microsoft (R) .NET Framework vetaion 3.5
Copyright (C) Microsoft Corporation. All rights reserved.
Vectors3.ca(S,ll): warning CS0660: ·Wrox.ProCSharp.OOCSharp.Vector’ definea
operator Ea or operator I~ but does not override Object.Equals(object 0)
Vectors3.ca(5,ll): warning CS0661: ‘Wrox.ProCSharp.OOCSharp.Vector’ defines
operator =s or operator 1= but does not override Object.GetHashCode()