Operators, Delegates, and Events |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
In this section we will see how we can use the operators of the C# language on instances of our own classes, or on values of our own structs.
|
21.1. Why operator overloading?
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this section we will describe how to program operations that can be called in expressions that make use of the conventional operators (such as +, &, >>, and !) of C#. Thus, in a client of a class C, we will provide for notation such as aC1 + aC2 * aC3 instead of aC1.Plus(aC2.Mult(aC3)) or (with use of class methods) C.Plus(aC1, C.Mult(aC2,aC3)).
Use of operators provides for substantial notational convenience in certain classes |
When operator notation is natural for objects or values of type C, clients of C can often be programmed with a more dense and readable notation. The example in Program 21.1 (only on web) provides additional motivation. In Program 21.1 (only on web) we use the class MyInt, which is shown in Program 21.2 (only on web). MyInt just wraps the operators +, -, *, /, and % in static methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System; public class OperatorsOrNot { public static int With (int a, int b, int c, int m){ return (a % m + b % m + c % m) / 3; } public static int Without (int a, int b, int c, int m){ return MyInt.Quotient( MyInt.Plus( MyInt.Plus( MyInt.Remainder(a,m), MyInt.Remainder(b,m)), MyInt.Remainder(c,m)), 3); } // In some languages, such as Lisp, // with more liberal identifiers rules: // (/ (+ (% a m) (% b m) (% c m)) 3) public static void Main(){ Console.WriteLine(With(18,19,25, 7)); // 4 Console.WriteLine(Without(18,19,25, 7)); // 4 } } | |||
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; public class MyInt{ public static int Plus(int a, int b){ return a + b; } public static int Minus(int a, int b){ return a - b; } public static int Multiply(int a, int b){ return a * b; } public static int Quotient(int a, int b){ return a / b; } public static int Remainder(int a, int b){ return a % b; } } | |||
|
21.2. Overloadable operators in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We have already once studied the operator table of C#, see Section 6.7. In Table 21.1 below we show a version of the operator table of C# (with operator priority and associativity) in which we have emphasized all the operators that can be overloaded in C#.
Level | Category | Operators | Associativity |
14 | Primary | x.y f(x) a[x] x++ x-- new typeof checked unchecked default delegate | left to right |
13 | Unary | + - ! ~ ++x --x (T)x true false sizeof | left to right |
12 | Multiplicative | * / % | left to right |
11 | Additive | + - | left to right |
10 | Shift | << >> | left to right |
9 | Relational and Type testing | < <= > >= is as | left to right |
8 | Equality | == != | left to right |
7 | Logical/bitwise And | & | left to right |
6 | Logical/bitwise Xor | ^ | left to right |
5 | Logical/bitwise Or | | | left to right |
4 | Conditional And | && | left to right |
3 | Conditional Or | || | left to right |
2 | Conditional | ?: | right to left |
1 | Assignment | = *= /= %= += -= <<= >>= &= ^= |= ?? => | right to left |
Table 21.1 The operator priority table of C#. The operators that can be overloaded directly are emphasized. |
All the gray operators in Table 21.1 cannot be overloaded directly. Many of them can, however, be overloaded indirectly, or defined by other means. We will now discuss how this can be done.
A notation similar to a[x] (array indexing) can be obtained by use of indexers, see Chapter 19.
The conditional (short circuited) operators && and || can be overloaded indirectly by overloading the operators & and |. In addition, the operators called true and false must also be provided. true(x) tells if x counts as boolean true. false(x) tells if x counts as boolean false. (Notice that x belongs to the type - class or struct - in which we have defined the operators). The operators && and || are defined by the following equivalences:
Thus, when x && y is encountered we first evaluate the expression false(x). If the value is true, x is returned. If it is false, y is also evaluated, and the value of x && y becomes x & y. A similar explanation applies for x || y.
You can define the unary true and false operators in your own classes, and hereby control if the object is considered to be true or false in some boolean contexts. If you define one of them, you will also have to define the other. Recall that an expression of the form a ? b : c uses the conditional operator ?: with the meaning if a then b else c.
All the assignment operators, apart from the basic assignment operator =, are implicitly overloadable. As an example, the assignment operator *= is implicitly overloaded when we explicitly overload the multiplication operator *.
The type cast operator (T)x can in reality also be overloaded. In a given class C you can define explicit and/or implicit conversion operators that converts to and from C. We will see an example of an explicit type conversion in Program 21.3.
21.3. An example of overloaded operators: Interval
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We will now study the type Interval. This type allows us to represent and operate on intervals of integers. The Interval type makes a good case for illustration of overloaded operators. We program all interval operations in a functional style. We want intervals to be non-mutable, and the type is therefore programmed as a struct.
An interval is characterized by two integer end points from and to. The interval [from - to] denotes the interval that starts in from and goes to to. The notation [from - to] is an informal notation which we use to explain the the idea of intervals. In a C# program, the interval [from - to] is denoted by the expression new Interval( from,to). Notice that from is not necessarily less than to. The following are concrete examples: [1 - 5] represents the sequence 1, 2, 3, 4, and 5. [5 - 1] represents the sequence 5, 4, 3, 2, and 1. These two sequences are different.
In Program 21.3 we see the struct Interval. The private instance variables from and to represent the interval in a simple and straightforward way, and the constructor is also simple. Just after the constructor there are two properties, From and To, that access the end points of the interval. In this version of the type it is not possible to construct an empty interval. We have already dealt with this weakness in Section 16.4 (see Program 16.7 versus Program 16.8) in the context of factory methods.
After the two properties we have highlighted a number of overloaded operators. These are our main interest in this section. Notice the syntax for definition of the operators. There are two definitions of the + operator. One of the form anInterval + i and one of the form i + anInterval. Both have the same meaning, namely addition of i to both end-points. Thus [1 - 5] + 3 and 3 + [1 - 5] are both equal to the interval [4 - 8].
In similar ways we define multiplication of intervals and integers. We also define subtraction of an integer from an interval (but not the other way around). The shift operators << and >> provide nice notations for moving one of the end-points of an interval. Thus, [1 - 5] >> 3 is equal to the interval [1 - 8].
Finally, the unary prefix operator ! reverses an interval (internally, by making an interval with swapped end-points). Thus, ![1 - 5] is equal to the interval [5 - 1].
The private class IntervalEnumerator (shown only in the web version) and the method GetEnumerator make it possible to traverse an interval in a convenient way with use of foreach. Interval traversal is what makes intervals useful. This is illustrated in Program 21.4. We will, in great details, discuss IntervalEnumerator later in this material, see Section 31.6 - in particular Program 31.9. As of now you can ignore the private class IntervalEnumerator and the method GetEnumerator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | using System; using System.Collections; public struct Interval{ private readonly int from, to; public Interval(int from, int to){ this.from = from; this.to = to; } public int From{ get {return from;} } public int To{ get {return to;} } public int Length{ get {return Math.Abs(to - from) + 1;} } public static Interval operator +(Interval i, int j){ return new Interval(i.From + j, i.To + j); } public static Interval operator +(int j, Interval i){ return new Interval(i.From + j, i.To + j); } public static Interval operator >>(Interval i, int j){ return new Interval(i.From, i.To + j); } public static Interval operator <<(Interval i, int j){ return new Interval(i.From + j, i.To); } public static Interval operator *(Interval i, int j){ return new Interval(i.From * j, i.To * j); } public static Interval operator *(int j, Interval i){ return new Interval(i.From * j, i.To * j); } public static Interval operator -(Interval i, int j){ return new Interval(i.From - j, i.To - j); } public static Interval operator !(Interval i){ return new Interval(i.To, i.From); } public static explicit operator int[] (Interval i){ int[] res = new int[i.Length]; for (int j = 0; j < i.Length; j++) res[j] = i[j]; return res; } private class IntervalEnumerator: IEnumerator{ private readonly Interval interval; private int idx; public IntervalEnumerator (Interval i){ this.interval = i; idx = -1; // position enumerator outside range } public Object Current{ get {return (interval.From < interval.To) ? interval.From + idx : interval.From - idx;} } public bool MoveNext (){ if ( idx < Math.Abs(interval.To - interval.From)) {idx++; return true;} else {return false;} } public void Reset(){ idx = -1; } } public IEnumerator GetEnumerator (){ return new IntervalEnumerator(this); } } | |||
|
Take a look at Program 21.4 in which we use intervals. Based on the constructed intervals iv1 and iv2 we write expressions that involve intervals. These are all highlighted in Program 21.4. Let me explain the expression !(3 + !iv2 * 2). When we evaluate this expression we adhere to normal precedence rules and normal association rules of the operators. We cannot change these rules. Therefore, we first evaluate !iv2, which is [5 - 2]. Next we evaluate !iv2 * 2, which is [10 - 4]. To this interval we add 3. This gives the interval [13 - 7]. Finally we reverse this interval. The final value is [7 - 13].
Also emphasized in Program 21.3 we show iv3[0] and iv3[iv3.Length-1]. These expressions use interval indexers. In Exercise 6.1 it is an exercise to program this indexer.
Emphasized with blue in Program 21.3 and Program 21.4 we show how to program and use an explicit type cast from Interval to int[].
You should follow the evaluations of all highlighted expressions in Program 21.4 and compare your results with the program output in Listing 21.5.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System; public class app { public static void Main(){ Interval iv1 = new Interval(17,14), iv2 = new Interval(2,5), iv3; foreach(int k in !(3 + iv1 - 2)){ Console.Write("{0,4}", k); } Console.WriteLine(); foreach(int k in !(3 + !iv2 * 2)){ Console.Write("{0,4}", k); } Console.WriteLine(); iv3 = !(3 + !iv2 * 3) >> 2 ; Console.WriteLine("First and last in iv3: {0}, {1}", iv3[0], iv3[iv3.Length-1]); int[] arr = (int[])iv3; foreach(int j in arr){ Console.Write("{0,4}", j); } } } | |||
|
1 2 3 4 | 15 16 17 18 7 8 9 10 11 12 13 First and last in iv3: 9, 20 9 10 11 12 13 14 15 16 17 18 19 20 | |||
|
In Program 21.6 we show yet another example of programming overloaded operators. We overload ==, !=, <, and >. This example brings us back to the playing card class which we have discussed already in Program 12.7 of Section 12.6 and Program 14.3 of Section 14.3.
Emphasized with colors in Program 21.6 we show operators that compare two cards. Notice, as above, that the operator definitions always are static. Also notice that if we define == we also have to define != . The == operator is defined via the Equals methods, which is redefined in class Card such that it provides value comparison of Card instances. If we redefine Equals we must also redefine GetHashCode. All together, a lot of work! Similarly, if we define <= we have also have to define >= .
Please notice that our redefinition of Equals in Program 21.6 is too simple for a real-life program. In Section 28.16 we will see the general pattern for redefinition of the Equals instance method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | using System; public enum CardSuite { Spades, Hearts, Clubs, Diamonds }; public enum CardValue { Ace = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6, Seven = 7, Eight = 8, Nine = 9, Ten = 10, Jack = 11, Queen = 12, King = 13}; public class Card{ private CardSuite suite; private CardValue value; public Card(CardSuite suite, CardValue value){ this.suite = suite; this.value = value; } public Card(CardSuite suite, int value){ this.suite = suite; this.value = (CardValue)value; } public Card(int suite, int value){ this.suite = (CardSuite)suite; this.value = (CardValue)value; } public CardSuite Suite{ get { return this.suite; } } public CardValue Value{ get { return this.value; } } public System.Drawing.Color Color{ get{ System.Drawing.Color result; if (suite == CardSuite.Spades || suite == CardSuite.Clubs) result = System.Drawing.Color.Black; else result = System.Drawing.Color.Red; return result; } } public override String ToString(){ return String.Format("Suite:{0}, Value:{1}, Color:{2}", suite, value, Color.ToString()); } public override bool Equals(Object other){ return (this.suite == ((Card)other).suite) && (this.value == ((Card)other).value); } public override int GetHashCode(){ return (int)suite ^ (int)value; } public static bool operator ==(Card c1, Card c2){ return c1.Equals(c2); } public static bool operator !=(Card c1, Card c2){ return !(c1.Equals(c2)); } public static bool operator <(Card c1, Card c2){ bool res; if (c1.suite < c2.suite) res = true; else if (c1.suite == c2.suite) res = (c1.value < c2.value); else res = false; return res; } public static bool operator >(Card c1, Card c2){ return !(c1 < c2) && !(c1 == c2); } } | |||
|
Exercise 6.1. Interval indexer The Interval type represents an oriented interval [from - to] of integers. We use the Interval example to illustrate the overloading of operators. If you have not already done so, read about the idea behind the struct Interval in the course teaching material. In the client of struct Interval we use an indexer to access elements of the interval. For some interval i, the expression i[0] should access the from-value of i, and i[i.Length-1] should access the to-value of i. Where, precisely, is the indexer used in the given client class? Add the indexer to the struct Interval (getter only) which accesses element number j (0 <= j <= i.Length) of an interval i. Hint: Be careful to take the orientation of the interval into account. Does it make sense to program a setter of this indexer? Solution |
Exercise 6.2. An interval overlap operation In this exercise we continue our work on struct Interval, which we have used to illustrate overloaded operators in C#. Add an Interal operation that finds the overlap between two intervals. Your starting point should be the struct Interval. In the version of struct Interval, provided as starting point for this exercise, intervals may be empty. Please analyze the possible overlappings between two intervals. There are several cases that need consideration. The fact that Interval is oriented may turn out to be a complicating factor in the solution. Feel free to ignore the orientation of intervals in your solution to this exercise. Which kind of operation will you chose for the overlapping operation in C# (method, property, indexer, operator)? Before you program the operation in C# you should design the signature of the operation. Program the operation in C#, and test your solution in an Interval client program. You may chose to revise the Interval client program from the teaching material. Solution |
21.4. Some details of operator overloading
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Below we summarize the syntax of operator definition, which overloads a predefined operators symbol.
| |||
|
There are many detailed rules that must be observed when we overload the predefined operator symbols. Some of them were mentioned in Section 21.3. Others are brought up below.
|
This concludes our coverage of operator overloading. Notice that we have not discussed all details of this subject. You should consult a C# reference manual for full coverage.