C# allows you to define the meaning of an operator relative to a class that you create. This process is called operator overloading. By overloading an operator, you expand its usage to your class. The effects of the operator are completely under your control and may differ from class to class. For example, a class that defines a linked list might use the + operator to add an object to the list. A class that implements a stack might use the + to push an object onto the stack. Another class might use the + operator in an entirely different way.
When an operator is overloaded, none of its original meaning is lost. It is simply that a new operation, relative to a specific class, is added. Therefore, overloading the + to handle a linked list, for example, does not cause its meaning relative to integers (that is, addition) to be changed.
A principal advantage of operator overloading is that it allows you to seamlessly integrate a new class type into your programming environment. This type extensibility is an important part of the power of an object-oriented language such as C#. Once operators are defined for a class, you can operate on objects of that class using the normal C# expression syntax. You can even use an object in expressions involving other types of data. Operator overloading is one of C#’s most powerful features.
Operator overloading is closely related to method overloading. To overload an operator, use the operator keyword to define an operator method, which defines the action of the operator relative to its class.
There are two forms of operator methods: one for unary operators and one for binary operators. The general form for each is shown here:
// General form for overloading a unary operator
public static ret-type operator op(param-type operand)
{
// operations
}
// General form for overloading a binary operator
public static ret-type operator op(param-type1 operand1, param-type1 operand2)
{
// operations
}
Here, the operator that you are overloading, such as + or /, is substituted for op. The ret-type specifies the type of value returned by the specified operation. Although it can be any type you choose, the return value is often of the same type as the class for which the operator is being overloaded. This correlation facilitates the use of the overloaded operator in expressions. For unary operators, the operand is passed in operand. For binary operators, the operands are passed in operand1 and operand2. Notice that operator methods must be both public and static.
For unary operators, the operand must be of the same type as the class for which the operator is being defined. For binary operators, at least one of the operands must be of the same type as its class. Thus, you cannot overload any C# operators for objects that you have not created. For example, you can’t redefine + for int or string.
One other point: Operator parameters must not use the ref or out modifier.
To see how operator overloading works, let’s start with an example that overloads two binary operators, the + and the –. The following program creates a class called ThreeD, which maintains the coordinates of an object in three-dimensional space. The overloaded + adds the individual coordinates of one ThreeD object to another. The overloaded – subtracts the coordinates of one object from the other.
// An example of operator overloading.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload binary +.
public static ThreeD operator +(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
/* This adds together the coordinates of the two points
and returns the result. */
result.x = op1.x + op2.x; // These are integer additions
result.y = op1.y + op2.y; // and the + retains its original
result.z = op1.z + op2.z; // meaning relative to them.
return result;
}
// Overload binary -.
public static ThreeD operator -(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Notice the order of the operands. op1 is the left
operand and op2 is the right. */
result.x = op1.x - op2.x; // these are integer subtractions
result.y = op1.y - op2.y;
result.z = op1.z - op2.z;
return result;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c;
Console.Write("Here is a: ");
a.Show();
Console.WriteLine();
Console.Write("Here is b: ");
b.Show();
Console.WriteLine();
c = a + b; // add a and b together
Console.Write("Result of a + b: ");
c.Show();
Console.WriteLine();
c = a + b + c; // add a, b, and c together
Console.Write("Result of a + b + c: ");
c.Show();
Console.WriteLine();
c = c - a; // subtract a
Console.Write("Result of c - a: ");
c.Show();
Console.WriteLine();
c = c - b; // subtract b
Console.Write("Result of c - b: ");
c.Show();
Console.WriteLine();
}
}
This program produces the following output:
Here is a: 1, 2, 3
Here is b: 10, 10, 10
Result of a + b: 11, 12, 13
Result of a + b + c: 22, 24, 26
Result of c - a: 21, 22, 23
Result of c - b: 11, 12, 13
Let’s examine the preceding program carefully, beginning with the overloaded operator +. When two objects of type ThreeD are operated on by the + operator, the magnitudes of their respective coordinates are added together, as shown in operator+( ). Notice, however, that this method does not modify the value of either operand. Instead, a new object of type ThreeD, which contains the result of the operation, is returned by the method. To understand why the + operation does not change the contents of either object, think about the standard arithmetic + operation as applied like this: 10 + 12. The outcome of this operation is 22, but neither 10 nor 12 is changed by it. Although no rule prevents an overloaded operator from altering the value of one of its operands, it is best for the actions of an overloaded operator to be consistent with its usual meaning.
Notice that operator+( ) returns an object of type ThreeD. Although the method could have returned any valid C# type, the fact that it returns a ThreeD object allows the + operator to be used in compound expressions, such as a+b+c. Here, a+b generates a result that is of type ThreeD. This value can then be added to c. Had any other type of value been generated by a+b, such an expression would not work.
Here is another important point: When the coordinates are added together inside operator+( ), the addition of the individual coordinates results in an integer addition. This is because the individual coordinates, x, y, and z, are integer quantities. The fact that the + operator is overloaded for objects of type ThreeD has no effect on the + as it is applied to integer values.
Now, look at operator–( ). The – operator works just like the + operator except that the order of the parameters is important. Recall that addition is commutative, but subtraction is not. (That is, A – B is not the same as B – A!) For all binary operators, the first parameter to an operator method will contain the left operand. The second parameter will contain the one on the right. When implementing overloaded versions of the noncommutative operators, you must remember which operand is on the left and which is on the right.
The unary operators are overloaded just like the binary operators. The main difference, of course, is that there is only one operand. For example, here is a method that overloads the unary minus for the ThreeD class:
// Overload unary -.
public static ThreeD operator -(ThreeD op)
{
ThreeD result = new ThreeD();
result.x = -op.x;
result.y = -op.y;
result.z = -op.z;
return result;
}
Here, a new object is created that contains the negated fields of the operand. This object is then returned. Notice that the operand is unchanged. Again, this is in keeping with the usual meaning of the unary minus. For example, in an expression such as this,
a = -b
a receives the negation of b, but b is not changed.
In C#, overloading ++ and – – is quite easy; simply return the incremented or decremented value, but don’t change the invoking object. C# will automatically handle that for you, taking into account the difference between the prefix and postfix forms. For example, here is an operator++( ) method for the ThreeD class:
// Overload unary ++.
public static ThreeD operator ++(ThreeD op)
{
ThreeD result = new ThreeD();
// Return the incremented result.
result.x = op.x + 1;
result.y = op.y + 1;
result.z = op.z + 1;
return result;
}
Here is an expanded version of the previous example program that demonstrates the unary – and the ++ operator:
// More operator overloading.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload binary +.
public static ThreeD operator +(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
/* This adds together the coordinates of the two points
and returns the result. */
result.x = op1.x + op2.x;
result.y = op1.y + op2.y;
result.z = op1.z + op2.z;
return result;
}
// Overload binary -.
public static ThreeD operator -(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
/* Notice the order of the operands. op1 is the left
operand and op2 is the right. */
result.x = op1.x - op2.x;
result.y = op1.y - op2.y;
result.z = op1.z - op2.z;
return result;
}
// Overload unary -.
public static ThreeD operator -(ThreeD op)
{
ThreeD result = new ThreeD();
result.x = -op.x;
result.y = -op.y;
result.z = -op.z;
return result;
}
// Overload unary ++.
public static ThreeD operator ++(ThreeD op)
{
ThreeD result = new ThreeD();
// Return the incremented result.
result.x = op.x + 1;
result.y = op.y + 1;
result.z = op.z + 1;
return result;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD();
Console.Write("Here is a: ");
a.Show();
Console.WriteLine();
Console.Write("Here is b: ");
b.Show();
Console.WriteLine();
c = a + b; // add a and b together
Console.Write("Result of a + b: ");
c.Show();
Console.WriteLine();
c = a + b + c; // add a, b, and c together
Console.Write("Result of a + b + c: ");
c.Show();
Console.WriteLine();
c = c - a; // subtract a
Console.Write("Result of c - a: ");
c.Show();
Console.WriteLine();
c = c - b; // subtract b
Console.Write("Result of c - b: ");
c.Show();
Console.WriteLine();
c = -a; // assign -a to c
Console.Write("Result of -a: ");
c.Show();
Console.WriteLine();
c = a++; // post-increment a
Console.WriteLine("Given c = a++");
Console.Write("c is ");
c.Show();
Console.Write("a is ");
a.Show();
// Reset a to 1, 2, 3
a = new ThreeD(1, 2, 3);
Console.Write("\nResetting a to ");
a.Show();
c = ++a; // pre-increment a
Console.WriteLine("\nGiven c = ++a");
Console.Write("c is ");
c.Show();
Console.Write("a is ");
a.Show();
}
}
The output from the program is shown here:
Here is a: 1, 2, 3
Here is b: 10, 10, 10
Result of a + b: 11, 12, 13
Result of a + b + c: 22, 24, 26
Result of c - a: 21, 22, 23
Result of c - b: 11, 12, 13
Result of -a: -1, -2, -3
Given c = a++
c is 1, 2, 3
a is 2, 3, 4
Resetting a to 1, 2, 3
Given c = ++a
c is 2, 3, 4
a is 2, 3, 4
For any given class and operator, an operator method can, itself, be overloaded. One of the most common reasons for this is to allow operations between a class type and other types of data, such as a built-in type. For example, once again consider the ThreeD class. To this point, you have seen how to overload the + so that it adds the coordinates of one ThreeD object to another. However, this is not the only way in which you might want to define addition for ThreeD. For example, it might be useful to add an integer value to each coordinate of a ThreeD object. Such an operation could be used to translate axes. To perform such an operation, you will need to overload + a second time, as shown here:
// Overload binary + for ThreeD + int.
public static ThreeD operator +(ThreeD op1, int op2)
{
ThreeD result = new ThreeD();
result.x = op1.x + op2;
result.y = op1.y + op2;
result.z = op1.z + op2;
return result;
}
Notice that the second parameter is of type int. Thus, the preceding method allows an integer value to be added to each field of a ThreeD object. This is permissible because, as explained earlier, when overloading a binary operator, one of the operands must be of the same type as the class for which the operator is being overloaded. However, the other operand can be of any other type.
Here is a version of ThreeD that has two overloaded + methods:
// Overload addition for ThreeD + ThreeD, and for ThreeD + int.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload binary + for ThreeD + ThreeD.
public static ThreeD operator +(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
/* This adds together the coordinates of the two points
and returns the result. */
result.x = op1.x + op2.x;
result.y = op1.y + op2.y;
result.z = op1.z + op2.z;
return result;
}
// Overload binary + for object + int.
public static ThreeD operator +(ThreeD op1, int op2)
{
ThreeD result = new ThreeD();
result.x = op1.x + op2;
result.y = op1.y + op2;
result.z = op1.z + op2;
return result;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD();
Console.Write("Here is a: ");
a.Show();
Console.WriteLine();
Console.Write("Here is b: ");
b.Show();
Console.WriteLine();
c = a + b; // ThreeD + ThreeD
Console.Write("Result of a + b: ");
c.Show();
Console.WriteLine();
c = b + 10; // ThreeD + int
Console.Write("Result of b + 10: ");
c.Show();
}
}
The output from this program is shown here:
Here is a: 1, 2, 3
Here is b: 10, 10, 10
Result of a + b: 11, 12, 13
Result of b + 10: 20, 20, 20
As the output confirms, when the + is applied to two ThreeD objects, their coordinates are added together. When the + is applied to a ThreeD object and an integer, the coordinates are increased by the integer value.
While the overloading of + just shown certainly adds a useful capability to the ThreeD class, it does not quite finish the job. Here is why. The operator+(ThreeD, int) method allows statements like this:
ob1 = ob2 + 10;
It does not, unfortunately, allow ones like this:
ob1 = 10 + ob2;
The reason is that the integer argument is the second argument, which is the right-hand operand, but the preceding statement puts the integer argument on the left. To allow both forms of statements, you will need to overload the + yet another time. This version must have its first parameter as type int and its second parameter as type ThreeD. One version of the operator+( ) method handles ThreeD + integer, and the other handles integer + ThreeD. Overloading the + (or any other binary operator) this way allows a built-in type to occur on the left or right side of the operator. Here is a version ThreeD that overloads the + operator as just described:
// Overload the + for ThreeD + ThreeD, ThreeD + int, and int + ThreeD.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload binary + for ThreeD + ThreeD.
public static ThreeD operator +(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
/* This adds together the coordinates of the two points
and returns the result. */
result.x = op1.x + op2.x;
result.y = op1.y + op2.y;
result.z = op1.z + op2.z;
return result;
}
// Overload binary + for ThreeD + int.
public static ThreeD operator +(ThreeD op1, int op2)
{
ThreeD result = new ThreeD();
result.x = op1.x + op2;
result.y = op1.y + op2;
result.z = op1.z + op2;
return result;
}
// Overload binary + for int + ThreeD.
public static ThreeD operator +(int op1, ThreeD op2)
{
ThreeD result = new ThreeD();
result.x = op2.x + op1;
result.y = op2.y + op1;
result.z = op2.z + op1;
return result;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD();
Console.Write("Here is a: ");
a.Show();
Console.WriteLine();
Console.Write("Here is b: ");
b.Show();
Console.WriteLine();
c = a + b; // ThreeD + ThreeD
Console.Write("Result of a + b: ");
c.Show();
Console.WriteLine();
c = b + 10; // ThreeD + int
Console.Write("Result of b + 10: ");
c.Show();
Console.WriteLine();
c = 15 + b; // int + ThreeD
Console.Write("Result of 15 + b: ");
c.Show();
}
}
The output from this program is shown here:
Here is a: 1, 2, 3
Here is b: 10, 10, 10
Result of a + b: 11, 12, 13
Result of b + 10: 20, 20, 20
Result of 15 + b: 25, 25, 25
The relational operators, such as = = or <, can also be overloaded and the process is straightforward. Usually, an overloaded relational operator returns a true or false value. This is in keeping with the normal usage of these operators and allows the overloaded relational operators to be used in conditional expressions. If you return a different type result, then you are greatly restricting the operator’s utility.
Here is a version of the ThreeD class that overloads the < and > operators. In this example, these operators compare ThreeD objects based on their distance from the origin. One object is greater than another if its distance from the origin is greater. One object is less than another if its distance from the origin is less than the other. Given two points, such an implementation could be used to determine which point lies on the larger sphere. If neither operator returns true, then the two points lie on the same sphere. Of course, other ordering schemes are possible.
// Overload < and >.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload <.
public static bool operator >(ThreeD op1, ThreeD op2)
{
if(Math.Sqrt(op1.x * op1.x + op1.y * op1.y + op1.z * op1.z) <
Math.Sqrt(op2.x * op2.x + op2.y * op2.y + op2.z * op2.z))
return true;
else
return false;
}
// Overload >.
public static bool operator <(ThreeD op1, ThreeD op2)
{
if(Math.Sqrt(op1.x * op1.x + op1.y * op1.y + op1.z * op1.z) >
Math.Sqrt(op2.x * op2.x + op2.y * op2.y + op2.z * op2.z))
return true;
else
return false;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD(1, 2, 3);
ThreeD d = new ThreeD(6, 7, 5);
Console.Write("Here is a: ");
a.Show();
Console.Write("Here is b: ");
b.Show();
Console.Write("Here is c: ");
c.Show();
Console.Write("Here is d: ");
d.Show();
Console.WriteLine();
if(a > c) Console.WriteLine("a > c is true");
if(a < c) Console.WriteLine("a < c is true");
if(a > b) Console.WriteLine("a > b is true");
if(a < b) Console.WriteLine("a < b is true");
if(a > d) Console.WriteLine("a > d is true");
else if(a < d) Console.WriteLine("a < d is true");
else Console.WriteLine("a and d are same distance from origin");
}
}
The output from this program is shown here:
Here is a: 5, 6, 7
Here is b: 10, 10, 10
Here is c: 1, 2, 3
Here is d: 6, 7, 5
a > c is true
a < b is true
a and d are same distance from origin
An important restriction applies to overloading the relational operators: You must overload them in pairs. For example, if you overload <, you must also overload >, and vice versa. The operator pairs are
One other point: If you overload the = = and != operators, then you will usually need to override Object.Equals( ) and Object.GetHashCode( ). These methods and the technique of overriding are discussed in Chapter 11.
The keywords true and false can also be used as unary operators for the purposes of overloading. Overloaded versions of these operators provide custom determinations of true and false relative to classes that you create. Once true and false are overloaded for a class, you can use objects of that class to control the if, while, for, and do-while statements, or in a ? expression.
The true and false operators must be overloaded as a pair. You cannot overload just one. Both are unary operators and they have this general form:
public static bool operator true(param-type operand)
{
// return true or false
}
public static bool operator false(param-type operand)
{
// return true or false
}
Notice that each returns a bool result.
The following example shows how true and false can be implemented for the ThreeD class. Each assumes that a ThreeD object is true if at least one coordinate is non-zero. If all three coordinates are zero, then the object is false. The decrement operator is also implemented for the purpose of illustration.
// Overload true and false for ThreeD.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload true.
public static bool operator true(ThreeD op) {
if((op.x != 0) || (op.y != 0) || (op.z != 0))
return true; // at least one coordinate is non-zero
else
return false;
}
// Overload false.
public static bool operator false(ThreeD op) {
if((op.x == 0) && (op.y == 0) && (op.z == 0))
return true; // all coordinates are zero
else
return false;
}
// Overload unary --.
public static ThreeD operator --(ThreeD op)
{
ThreeD result = new ThreeD();
// Return the decremented result.
result.x = op.x - 1;
result.y = op.y - 1;
result.z = op.z - 1;
return result;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class TrueFalseDemo {
static void Main() {
ThreeD a = new ThreeD(5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD(0, 0, 0);
Console.Write("Here is a: ");
a.Show();
Console.Write("Here is b: ");
b.Show();
Console.Write("Here is c: ");
c.Show();
Console.WriteLine();
if(a) Console.WriteLine("a is true.");
else Console.WriteLine("a is false.");
if(b) Console.WriteLine("b is true.");
else Console.WriteLine("b is false.");
if(c) Console.WriteLine("c is true.");
else Console.WriteLine("c is false.");
Console.WriteLine();
Console.WriteLine("Control a loop using a ThreeD object.");
do {
b.Show();
b--;
} while(b);
}
}
The output is shown here:
Here is a: 5, 6, 7
Here is b: 10, 10, 10
Here is c: 0, 0, 0
a is true.
b is true.
c is false.
Control a loop using a ThreeD object.
10, 10, 10
9, 9, 9
8, 8, 8
7, 7, 7
6, 6, 6
5, 5, 5
4, 4, 4
3, 3, 3
2, 2, 2
1, 1, 1
Notice how the ThreeD objects are used to control if statements and a do-while loop. In the case of the if statements, the ThreeD object is evaluated using true. If the result of this operation is true, then the if statement succeeds. In the case of the do-while loop, each iteration of the loop decrements b. The loop repeats as long as b evaluates as true (that is, it contains at least one non-zero coordinate). When b contains all zero coordinates, it evaluates as false when the true operator is applied and the loop stops.
As you know, C# defines the following logical operators: &, |, !, &&, and ||. Of these, only the &, |, and ! can be overloaded. By following certain rules, however, the benefits of the short-circuit && and || can still be obtained. Each situation is examined here.
Let’s begin with the simplest situation. If you will not be making use of the short-circuit logical operators, then you can overload & and | as you would intuitively think, with each returning a bool result. An overloaded ! will also usually return a bool result.
Here is an example that overloads the !, &, and | logical operators for objects of type ThreeD. As before, each assumes that a ThreeD object is true if at least one coordinate is non-zero. If all three coordinates are zero, then the object is false.
// A simple way to overload !, |, and & for ThreeD.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload |.
public static bool operator |(ThreeD op1, ThreeD op2)
{
if( ((op1.x != 0) || (op1.y != 0) || (op1.z != 0)) |
((op2.x != 0) || (op2.y != 0) || (op2.z != 0)) )
return true;
else
return false;
}
// Overload &.
public static bool operator &(ThreeD op1, ThreeD op2)
{
if( ((op1.x != 0) && (op1.y != 0) && (op1.z != 0)) &
((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
return true;
else
return false;
}
// Overload !.
public static bool operator !(ThreeD op)
{
if((op.x != 0) || (op.y != 0) || (op.z != 0))
return false;
else return true;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class LogicalOpDemo {
static void Main() {
ThreeD a = new ThreeD(5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD(0, 0, 0);
Console.Write("Here is a: ");
a.Show();
Console.Write("Here is b: ");
b.Show();
Console.Write("Here is c: ");
c.Show();
Console.WriteLine();
if(!a) Console.WriteLine("a is false.");
if(!b) Console.WriteLine("b is false.");
if(!c) Console.WriteLine("c is false.");
Console.WriteLine();
if(a & b) Console.WriteLine("a & b is true.");
else Console.WriteLine("a & b is false.");
if(a & c) Console.WriteLine("a & c is true.");
else Console.WriteLine("a & c is false.");
if(a | b) Console.WriteLine("a | b is true.");
else Console.WriteLine("a | b is false.");
if(a | c) Console.WriteLine("a | c is true.");
else Console.WriteLine("a | c is false.");
}
}
The output from the program is shown here:
Here is a: 5, 6, 7
Here is b: 10, 10, 10
Here is c: 0, 0, 0
c is false.
a & b is true.
a & c is false.
a | b is true.
a | c is true.
In this approach, the &, |, and ! operator methods each return a bool result. This is necessary if the operators are to be used in their normal manner (that is, in places that expect a bool result). Recall that for all built-in types, the outcome of a logical operation is a value of type bool. Thus, having the overloaded versions of these operators return type bool is a rational approach. Unfortunately, this approach works only if you will not be needing the short-circuit operators.
To enable the use of the && and || short-circuit operators, you must follow four rules. First, the class must overload & and |. Second, the return type of the overloaded & and | methods must be the same as the class for which the operators are being overloaded. Third, each parameter must be a reference to an object of the class for which the operator is being overloaded. Fourth, the true and false operators must be overloaded for the class. When these conditions have been met, the short-circuit operators automatically become available for use.
The following program shows how to properly implement the & and | for the ThreeD class so that the short-circuit operators && and || are available.
/* A better way to overload !, |, and & for ThreeD.
This version automatically enables the && and || operators. */
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload | for short-circuit evaluation.
public static ThreeD operator |(ThreeD op1, ThreeD op2)
{
if( ((op1.x != 0) || (op1.y != 0) || (op1.z != 0)) |
((op2.x != 0) || (op2.y != 0) || (op2.z != 0)) )
return new ThreeD(1, 1, 1);
else
return new ThreeD(0, 0, 0);
}
// Overload & for short-circuit evaluation.
public static ThreeD operator &(ThreeD op1, ThreeD op2)
{
if( ((op1.x != 0) && (op1.y != 0) && (op1.z != 0)) &
((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
return new ThreeD(1, 1, 1);
else
return new ThreeD(0, 0, 0);
}
// Overload !.
public static bool operator !(ThreeD op)
{
if(op) return false;
else return true;
}
// Overload true.
public static bool operator true(ThreeD op) {
if((op.x != 0) || (op.y != 0) || (op.z != 0))
return true; // at least one coordinate is non-zero
else
return false;
}
// Overload false.
public static bool operator false(ThreeD op) {
if((op.x == 0) && (op.y == 0) && (op.z == 0))
return true; // all coordinates are zero
else
return false;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class LogicalOpDemo {
static void Main() {
ThreeD a = new ThreeD(5, 6, 7);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD(0, 0, 0);
Console.Write("Here is a: ");
a.Show();
Console.Write("Here is b: ");
b.Show();
Console.Write("Here is c: ");
c.Show();
Console.WriteLine();
if(a) Console.WriteLine("a is true.");
if(b) Console.WriteLine("b is true.");
if(c) Console.WriteLine("c is true.");
if(!a) Console.WriteLine("a is false.");
if(!b) Console.WriteLine("b is false.");
if(!c) Console.WriteLine("c is false.");
Console.WriteLine();
Console.WriteLine("Use & and |");
if(a & b) Console.WriteLine("a & b is true.");
else Console.WriteLine("a & b is false.");
if(a & c) Console.WriteLine("a & c is true.");
else Console.WriteLine("a & c is false.");
if(a | b) Console.WriteLine("a | b is true.");
else Console.WriteLine("a | b is false.");
if(a | c) Console.WriteLine("a | c is true.");
else Console.WriteLine("a | c is false.");
Console.WriteLine();
// Now use short-circuit ops.
Console.WriteLine("Use short-circuit && and ||");
if(a && b) Console.WriteLine("a && b is true.");
else Console.WriteLine("a && b is false.");
if(a && c) Console.WriteLine("a && c is true.");
else Console.WriteLine("a && c is false.");
if(a || b) Console.WriteLine("a || b is true.");
else Console.WriteLine("a || b is false.");
if(a || c) Console.WriteLine("a || c is true.");
else Console.WriteLine("a || c is false.");
}
}
The output from the program is shown here:
Here is a: 5, 6, 7
Here is b: 10, 10, 10
Here is c: 0, 0, 0
a is true.
b is true.
c is false.
Use & and |
a & b is true.
a & c is false.
a | b is true.
a | c is true.
Use short-circuit && and ||
a && b is true.
a && c is false.
a || b is true.
a || c is true.
Let’s look closely at how the & and | are implemented. They are shown here:
// Overload | for short-circuit evaluation.
public static ThreeD operator |(ThreeD op1, ThreeD op2)
{
if( ((op1.x != 0) || (op1.y != 0) || (op1.z != 0)) |
((op2.x != 0) || (op2.y != 0) || (op2.z != 0)) )
return new ThreeD(1, 1, 1);
else
return new ThreeD(0, 0, 0);
}
// Overload & for short-circuit evaluation.
public static ThreeD operator &(ThreeD op1, ThreeD op2)
{
if( ((op1.x != 0) && (op1.y != 0) && (op1.z != 0)) &
((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
return new ThreeD(1, 1, 1);
else
return new ThreeD(0, 0, 0);
}
Notice first that both now return an object of type ThreeD. Pay attention to how this object is generated. If the outcome of the operation is true, then a true ThreeD object (one in which at least one coordinate is non-zero) is created and returned. If the outcome is false, then a false object is created and returned. Thus, in a statement like this
if(a & b) Console.WriteLine("a & b is true.");
else Console.WriteLine("a & b is false.");
the outcome of a & b is a ThreeD object, which in this case is a true object. Since the operators true and false are defined, this resulting object is subjected to the true operator, and a bool result is returned. In this case, the result is true and the if succeeds.
Because the necessary rules have been followed, the short-circuit operators are now available for use on ThreeD objects. They work like this. The first operand is tested by using operator true (for ||) or operator false (for &&). If it can determine the outcome of the operation, then the corresponding & or | is not evaluated. Otherwise, the corresponding overloaded & or | is used to determine the result. Thus, using a && or || causes the corresponding & or | to be invoked only when the first operand cannot determine the outcome of the expression. For example, consider this statement from the program:
if(a || c) Console.WriteLine("a || c is true.");
The true operator is first applied to a. Since a is true in this situation, there is no need to use the | operator method. However, if the statement were rewritten like this:
if(c || a) Console.WriteLine("c || a is true.");
then the true operator would first be applied to c, which in this case is false. Thus, the | operator method would be invoked to determine if a was true (which it is in this case).
Although you might at first think that the technique used to enable the short-circuit operators is a bit convoluted, it makes sense if you think about it a bit. By overloading true and false for a class, you enable the compiler to utilize the short-circuit operators without having to explicitly overload either. Furthermore, you gain the ability to use objects in conditional expressions. In general, unless you need a very narrow implementation of & and |, you are better off creating a full implementation.
In some situations, you will want to use an object of a class in an expression involving other types of data. Sometimes, overloading one or more operators can provide the means of doing this. However, in other cases, what you want is a simple type conversion from the class type to the target type. To handle these cases, C# allows you to create a special type of operator method called a conversion operator. A conversion operator converts an object of your class into another type. Conversion operators help fully integrate class types into the C# programming environment by allowing objects of a class to be freely mixed with other data types as long as a conversion to those other types is defined.
There are two forms of conversion operators, implicit and explicit. The general form for each is shown here:
public static operator implicit target-type(source-type v) { return value; }
public static operator explicit target-type(source-type v) { return value; }
Here, target-type is the target type that you are converting to; source-type is the type you are converting from; and value is the value of the class after conversion. The conversion operators return data of type target-type, and no other return type specifier is allowed.
If the conversion operator specifies implicit, then the conversion is invoked automatically, such as when an object is used in an expression with the target type. When the conversion operator specifies explicit, the conversion is invoked when a cast is used. You cannot define both an implicit and explicit conversion operator for the same target and source types.
To illustrate a conversion operator, we will create one for the ThreeD class. Suppose you want to convert an object of type ThreeD into an integer so it can be used in an integer expression. Further, the conversion will take place by using the product of the three dimensions. To accomplish this, you will use an implicit conversion operator that looks like this:
public static implicit operator int(ThreeD op1)
{
return op1.x * op1.y * op1.z;
}
Here is a program that illustrates this conversion operator:
// An example that uses an implicit conversion operator.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload binary +.
public static ThreeD operator +(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
result.x = op1.x + op2.x;
result.y = op1.y + op2.y;
result.z = op1.z + op2.z;
return result;
}
// An implicit conversion from ThreeD to int.
public static implicit operator int(ThreeD op1)
{
return op1.x * op1.y * op1.z;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD();
int i;
Console.Write("Here is a: ");
a.Show();
Console.WriteLine();
Console.Write("Here is b: ");
b.Show();
Console.WriteLine();
c = a + b; // add a and b together
Console.Write("Result of a + b: ");
c.Show();
Console.WriteLine();
i = a; // convert to int
Console.WriteLine("Result of i = a: " + i);
Console.WriteLine();
i = a * 2 - b; // convert to int
Console.WriteLine("result of a * 2 - b: " + i);
}
}
This program displays the output:
Here is a: 1, 2, 3
Here is b: 10, 10, 10
Result of a + b: 11, 12, 13
Result of i = a: 6
result of a * 2 - b: -988
As the program illustrates, when a ThreeD object is used in an integer expression, such as i = a, the conversion is applied to the object. In this specific case, the conversion returns the value 6, which is the product of coordinates stored in a. However, when an expression does not require a conversion to int, the conversion operator is not called. This is why c = a + b does not invoke operator int( ).
Remember that you can create different conversion operators to meet different needs. You could define a second conversion operator that converts ThreeD to double, for example. Each conversion is applied automatically and independently.
An implicit conversion operator is applied automatically when a conversion is required in an expression, when passing an object to a method, in an assignment, and also when an explicit cast to the target type is used. Alternatively, you can create an explicit conversion operator, which is invoked only when an explicit cast is used. An explicit conversion operator is not invoked automatically. For example, here is the previous program reworked to use an explicit conversion to int:
// Use an explicit conversion.
using System;
// A three-dimensional coordinate class.
class ThreeD {
int x, y, z; // 3-D coordinates
public ThreeD() { x = y = z = 0; }
public ThreeD(int i, int j, int k) { x = i; y = j; z = k; }
// Overload binary +.
public static ThreeD operator +(ThreeD op1, ThreeD op2)
{
ThreeD result = new ThreeD();
result.x = op1.x + op2.x;
result.y = op1.y + op2.y;
result.z = op1.z + op2.z;
return result;
}
// This is now explicit.
public static explicit operator int(ThreeD op1)
{
return op1.x * op1.y * op1.z;
}
// Show X, Y, Z coordinates.
public void Show()
{
Console.WriteLine(x + ", " + y + ", " + z);
}
}
class ThreeDDemo {
static void Main() {
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
ThreeD c = new ThreeD();
int i;
Console.Write("Here is a: ");
a.Show();
Console.WriteLine();
Console.Write("Here is b: ");
b.Show();
Console.WriteLine();
c = a + b; // add a and b together
Console.Write("Result of a + b: ");
c.Show();
Console.WriteLine();
i = (int) a; // explicitly convert to int -- cast required
Console.WriteLine("Result of i = a: " + i);
Console.WriteLine();
i = (int)a * 2 - (int)b; // casts required
Console.WriteLine("result of a * 2 - b: " + i);
}
}
Because the conversion operator is now marked as explicit, conversion to int must be explicitly cast. For example, in this line:
i = (int) a; // explicitly convert to int -- cast required
if you remove the cast, the program will not compile.
There are a few restrictions to conversion operators:
• Either the target type or the source type of the conversion must be the class in which the conversion is declared. You cannot, for example, redefine the conversion from double to int.
• You cannot define a conversion to or from object.
• You cannot define both an implicit and an explicit conversion for the same source and target types.
• You cannot define a conversion from a base class to a derived class. (See Chapter 11 for a discussion of base and derived classes.)
• You cannot define a conversion from or to an interface. (See Chapter 12 for a discussion of interfaces.)
In addition to these rules, there are suggestions that you should normally follow when choosing between implicit and explicit conversion operators. Although convenient, implicit conversions should be used only in situations in which the conversion is inherently error-free. To ensure this, implicit conversions should be created only when these two conditions are met: First, that no loss of information, such as truncation, overflow, or loss of sign, occurs, or that such loss of information is acceptable based on the circumstances. Second, that the conversion does not cause an exception. If the conversion cannot meet these two requirements, then you should use an explicit conversion.
The action of an overloaded operator as applied to the class for which it is defined need not bear any relationship to that operator’s default usage, as applied to C#’s built-in types. However, for the purposes of the structure and readability of your code, an overloaded operator should reflect, when possible, the spirit of the operator’s original use. For example, the + relative to ThreeD is conceptually similar to the + relative to integer types. There would be little benefit in defining the + operator relative to some class in such a way that it acts more the way you would expect the / operator to perform, for instance. The central concept is that while you can give an overloaded operator any meaning you like, for clarity it is best when its new meaning is related to its original meaning.
There are some restrictions to overloading operators. You cannot alter the precedence of any operator. You cannot alter the number of operands required by the operator, although your operator method could choose to ignore an operand. There are several operators that you cannot overload. Perhaps most significantly, you cannot overload any assignment operator, including the compound assignments, such as +=. Here are the other operators that cannot be overloaded. (This list includes several operators that are discussed later in this book.)
Although you cannot overload the cast operator ( ) explicitly, you can create conversion operators, as shown earlier, that perform this function.
It may seem like a serious restriction that operators such as += can’t be overloaded, but it isn’t. In general, if you have defined an operator, then if that operator is used in a compound assignment, your overloaded operator method is invoked. Thus, += automatically uses your version of operator+( ). For example, assuming the ThreeD class, if you use a sequence like this:
ThreeD a = new ThreeD(1, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
b += a; // add a and b together
ThreeD’s operator+( ) is automatically invoked, and b will contain the coordinate 11, 12, 13.
One last point: Although you cannot overload the [ ] array indexing operator using an operator method, you can create indexers, which are described in the next chapter.
Throughout this chapter we have been using the ThreeD class to demonstrate operator overloading, and in this regard it has served us well. Before concluding this chapter, however, it is useful to work through another example. Although the general principles of operator overloading are the same no matter what class is used, the following example helps show the power of operator overloading—especially where type extensibility is concerned.
This example develops a four-bit integer type and defines several operations for it. As you might know, in the early days of computing, the four-bit quantity was common because it represented half a byte. It is also large enough to hold one hexadecimal digit. Since four bits are half a byte, a four-bit quantity is sometimes referred to as a nybble. In the days of front-panel machines in which programmers entered code one nybble at a time, thinking in terms of nybbles was an everyday affair! Although not as common now, a four-bit type still makes an interesting addition to the other C# integers. Traditionally, a nybble is an unsigned value.
The following example uses the Nybble class to implement a nybble data type. It uses an int for its underlying storage, but it restricts the values that can be held to 0 through 15. It defines the following operators:
• Addition of a Nybble to a Nybble
• Addition of an int to a Nybble
• Addition of a Nybble to an int
• Greater than and less than
• The increment operator
• Conversion to Nybble from int
• Conversion to int from Nybble
These operations are sufficient to show how a class type can be fully integrated into the C# type system. However, for complete Nybble implementation, you will need to define all of the other operators. You might want to try adding these on your own.
The complete Nybble class is shown here along with a NybbleDemo, which demonstrates its use:
// Create a 4-bit type called Nybble.
using System;
// A 4-bit type.
class Nybble {
int val; // underlying storage
public Nybble() { val = 0; }
public Nybble(int i) {
val = i;
val = val & 0xF; // retain lower 4 bits
}
// Overload binary + for Nybble + Nybble.
public static Nybble operator +(Nybble op1, Nybble op2)
{
Nybble result = new Nybble();
result.val = op1.val + op2.val;
result.val = result.val & 0xF; // retain lower 4 bits
return result;
}
// Overload binary + for Nybble + int.
public static Nybble operator +(Nybble op1, int op2)
{
Nybble result = new Nybble();
result.val = op1.val + op2;
result.val = result.val & 0xF; // retain lower 4 bits
return result;
}
// Overload binary + for int + Nybble.
public static Nybble operator +(int op1, Nybble op2)
{
Nybble result = new Nybble();
result.val = op1 + op2.val;
result.val = result.val & 0xF; // retain lower 4 bits
return result;
}
// Overload ++.
public static Nybble operator ++(Nybble op)
{
Nybble result = new Nybble();
result.val = op.val + 1;
result.val = result.val & 0xF; // retain lower 4 bits
return result;
}
// Overload >.
public static bool operator >(Nybble op1, Nybble op2)
{
if(op1.val > op2.val) return true;
else return false;
}
// Overload <.
public static bool operator <(Nybble op1, Nybble op2)
{
if(op1.val < op2.val) return true;
else return false;
}
// Convert a Nybble into an int.
public static implicit operator int (Nybble op)
{
return op.val;
}
// Convert an int into a Nybble.
public static implicit operator Nybble (int op)
{
return new Nybble(op);
}
}
class NybbleDemo {
static void Main() {
Nybble a = new Nybble(1);
Nybble b = new Nybble(10);
Nybble c = new Nybble();
int t;
Console.WriteLine("a: " + (int) a);
Console.WriteLine("b: " + (int) b);
// Use a Nybble in an if statement.
if(a < b) Console.WriteLine("a is less than b\n");
// Add two Nybbles together.
c = a + b;
Console.WriteLine("c after c = a + b: " + (int) c);
// Add an int to a Nybble.
a += 5;
Console.WriteLine("a after a += 5: " + (int) a);
Console.WriteLine();
// Use a Nybble in an int expression.
t = a * 2 + 3;
Console.WriteLine("Result of a * 2 + 3: " + t);
Console.WriteLine();
// Illustrate int assignment and overflow.
a = 19;
Console.WriteLine("Result of a = 19: " + (int) a);
Console.WriteLine();
// Use a Nybble to control a loop.
Console.WriteLine("Control a for loop with a Nybble.");
for(a = 0; a < 10; a++)
Console.Write((int) a + " ");
Console.WriteLine();
}
}
The output from the program is shown here:
a: 1
b: 10
a is less than b
c after c = a + b: 11
a after a += 5: 6
Result of a * 2 + 3: 15
Result of a = 19: 3
Control a for loop with a Nybble.
0 1 2 3 4 5 6 7 8 9
Although most of the operation of Nybble should be easy to understand, there is one important point to make: The conversion operators play a large role in the integration of Nybble into the C# type system. Because conversions are defined from Nybble to int and from int to Nybble, a Nybble object can be freely mixed in arithmetic expressions. For example, consider this expression from the program:
t = a * 2 + 3;
Here, t is an int, as are 2 and 3, but a is a Nybble. These two types are compatible in the expression because of the implicit conversion of Nybble to int. In this case, since the rest of the expression is of type int, a is converted to int by its conversion method.
The conversion from int to Nybble allows a Nybble object to be assigned an int value. For example, in the program, the statement
a = 19;
works like this. The conversion operator from int to Nybble is executed. This causes a new Nybble object to be created that contains the low-order 4 bits of the value 19, which is 3 because 19 overflows the range of a Nybble. (In this example, such overflow is acceptable.) This object is then assigned to a. Without the conversion operators, such expressions would not be allowed.
The conversion of Nybble to int is also used by the for loop. Without this conversion, it would not be possible to write the for loop in such a straightforward way.
NOTE As an exercise, you might want to try creating a version of Nybble that prevents overflow when an out-of-range value is assigned. The best way to do this is by thowing an exception. See Chapter 13 for a discussion of exceptions.