Chapter 6
Operators, Delegates, and Events

Kurt NÝrmark ©
Department of Computer Science, Aalborg University, Denmark


Abstract
Previous lecture Next lecture
Index References Contents
Delegates is a new kind of types that hold operations (methods). Events are variable of delegate types. Therefore we have found it natural to also to cover delegates and events in this lecture. At the end of the lecture we study the observer design pattern, because this pattern is natural to deal with in terms of delegates and events.


Overloaded Operators

Why operator overloading?
Slide Annotated slide Contents Index
References Textbook 
Operator overloading is about use of ordinary operatorsl like + and !=, for non-simple types. On this page will motivate you for operator overloading.

Use of operators provides for substantial notational convenience in certain classes

Program: Comparison of operator notation and function notation. Illustrates that use of operator notation is much briefer than use of method notation - at least when we use natural names of the operations.
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
  
 }

}

Program: The class MyInt. How the methods used above can be be implemented as static methods in the MyInt class.
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;
  }

}

Read more about motivation for operator overloading in the text book version of this material.

Overloadable operators in C#
Slide Annotated slide Contents Index
References Textbook 
Overview of overloadable operators.

A subset of the C# operators are overloadable

Table. The operator priority table of C#. The operators that can be overloaded directly are emphasized.
LevelCategoryOperatorsAssociativity
14Primaryx.y      f(x)      a[x]      x++      x--      new      typeof      checked      unchecked      default      delegateleft to right
13Unary+      -      !      ~      ++x      --x      (T)x      true      false      sizeofleft to right
12Multiplicative*      /      %left to right
11Additive+      -left to right
10Shift<<      >>left to right
9Relational and Type testing<      <=      >      >=      is      asleft to right
8Equality==      !=left to right
7Logical/bitwise And&left to right
6Logical/bitwise Xor^left to right
5Logical/bitwise Or|left to right
4Conditional And&&left to right
3Conditional Or||left to right
2Conditional?:right to left
1Assignment=      *=      /=      %=      +=      -=      <<=      >>=      &=      ^=      |=      ??      =>right to left
 

Read more about overloadable C# operators in the text book version of this material.

An example of overloaded operators: Interval
Slide Annotated slide Contents Index
References Textbook 
We introduce a new type, struct Interval, in which we implement a number of operations by overloads of some of the well-known C# operators.

An interval is a type that represents the ordered sequence of integers between one integer and another

We program the type Interval with value semantics

Program: The struct Interval. This type defines a number of overloaded operators in struct Interval.
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);   
  }

}

Program: A client program of struct Interval. This class uses the operators on given interval values.
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);
    }

  }

}

Program: Output from the interval application.
  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

Exercise 6.3. 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?

Exercise 6.3. 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.

Program: The class PlayingCard with relational operators. The operators ==, !=, <, and > in class Card.
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);
   }
}

Read more about the interval and playing card examples in the text book version of this material.

Some details of operator overloading
Slide Annotated slide Contents Index
References Textbook 
Additional details about operator overloading. Not the full story, however.

Syntax: The C# syntax for definition of an overloaded operator.

public static return-type operator symbol(formal-par-list){
  body-of-operator
}

  • Operators must be public and static

  • One or two formal parameters must occur, corresponding to unary and binary operators

  • At least one of the parameters must be of the type to which the operator belongs

  • Only value parameters apply

  • Some operators must be defined in pairs (either none or both):

    • ==   and   !=            <   and   >            <=   and   >=

  • The special unary boolean operators true and false define when an object is playing the role as true or false in relation to the conditional logical operators

  • Overloading the binary operator op causes automatic overloading of the assignment operator op=

The list of rules above is not exhaustive


Delegates

Delegates in C#
Slide Annotated slide Contents Index
References Textbook 
Delegates are types. The objects in such types are methods. With the introduction of delegates, methods become data.

A delegate is a type the values of which consist of methods

Delegates allow us to work with variables and parameters that contain methods

Program: A Delegate of simple numeric functions. An example which defines a simple delegate (a type of methods) called NumericFunction. The example illustrates both existing, named methods in NumericFunction and a new, anonymous method in in the type.
using System;

public class Application {

  public delegate double NumericFunction(double d);   

  public static void PrintTableOfFunction(NumericFunction f, 
                                          string fname, 
                                          double from, double to, 
                                          double step){
    double d;

    for(d = from; d <= to; d += step){
     Console.WriteLine("{0,10}({1,-4:F3}) = {2}", fname, d, f(d));
    }

    Console.WriteLine();
  }

  public static double Cubic(double d){  
    return d*d*d;
  }

  public static void Main(){
    PrintTableOfFunction(Math.Log, "log", 0.1, 5, 0.1);           
    PrintTableOfFunction(Math.Sin, "sin", 0.0, 2 * Math.PI, 0.1); 
    PrintTableOfFunction(Math.Abs, "abs", -1.0, 1.0, 0.1);

    PrintTableOfFunction(Cubic, "cubic", 1.0, 5.0, 0.5);

    // Equivalent to previous:
    PrintTableOfFunction(delegate (double d){return d*d*d;},  
                         "cubic", 1.0, 5.0, 0.5);
  }
}

Program: Output from the NumericFunction delegate program.
       log(0,100) = -2,30258509299405
       log(0,200) = -1,6094379124341
       log(0,300) = -1,20397280432594
       log(0,400) = -0,916290731874155
       log(0,500) = -0,693147180559945
       log(0,600) = -0,510825623765991
       log(0,700) = -0,356674943938732
       log(0,800) = -0,22314355131421
       log(0,900) = -0,105360515657826
       log(1,000) = -1,11022302462516E-16
       log(1,100) = 0,0953101798043247
       log(1,200) = 0,182321556793955
       log(1,300) = 0,262364264467491
       log(1,400) = 0,336472236621213
       log(1,500) = 0,405465108108165
       log(1,600) = 0,470003629245736
       log(1,700) = 0,530628251062171
       log(1,800) = 0,587786664902119
       log(1,900) = 0,641853886172395
       log(2,000) = 0,693147180559946
       log(2,100) = 0,741937344729378
       log(2,200) = 0,788457360364271
       log(2,300) = 0,832909122935104
       log(2,400) = 0,8754687373539
       log(2,500) = 0,916290731874155
       log(2,600) = 0,955511445027437
       log(2,700) = 0,993251773010284
       log(2,800) = 1,02961941718116
       log(2,900) = 1,06471073699243
       log(3,000) = 1,09861228866811
       log(3,100) = 1,1314021114911
       log(3,200) = 1,16315080980568
       log(3,300) = 1,19392246847243
       log(3,400) = 1,22377543162212
       log(3,500) = 1,25276296849537
       log(3,600) = 1,28093384546206
       log(3,700) = 1,30833281965018
       log(3,800) = 1,33500106673234
       log(3,900) = 1,3609765531356
       log(4,000) = 1,38629436111989
       log(4,100) = 1,41098697371026
       log(4,200) = 1,43508452528932
       log(4,300) = 1,45861502269952
       log(4,400) = 1,48160454092422
       log(4,500) = 1,50407739677627
       log(4,600) = 1,52605630349505
       log(4,700) = 1,54756250871601
       log(4,800) = 1,56861591791385
       log(4,900) = 1,58923520511658
       log(5,000) = 1,6094379124341

       sin(0,000) = 0
       sin(0,100) = 0,0998334166468282
       sin(0,200) = 0,198669330795061
       sin(0,300) = 0,29552020666134
       sin(0,400) = 0,389418342308651
       sin(0,500) = 0,479425538604203
       sin(0,600) = 0,564642473395035
       sin(0,700) = 0,644217687237691
       sin(0,800) = 0,717356090899523
       sin(0,900) = 0,783326909627483
       sin(1,000) = 0,841470984807896
       sin(1,100) = 0,891207360061435
       sin(1,200) = 0,932039085967226
       sin(1,300) = 0,963558185417193
       sin(1,400) = 0,98544972998846
       sin(1,500) = 0,997494986604054
       sin(1,600) = 0,999573603041505
       sin(1,700) = 0,991664810452469
       sin(1,800) = 0,973847630878195
       sin(1,900) = 0,946300087687414
       sin(2,000) = 0,909297426825681
       sin(2,100) = 0,863209366648873
       sin(2,200) = 0,80849640381959
       sin(2,300) = 0,74570521217672
       sin(2,400) = 0,67546318055115
       sin(2,500) = 0,598472144103956
       sin(2,600) = 0,515501371821463
       sin(2,700) = 0,427379880233829
       sin(2,800) = 0,334988150155904
       sin(2,900) = 0,239249329213981
       sin(3,000) = 0,141120008059866
       sin(3,100) = 0,0415806624332892
       sin(3,200) = -0,0583741434275814
       sin(3,300) = -0,15774569414325
       sin(3,400) = -0,255541102026833
       sin(3,500) = -0,350783227689622
       sin(3,600) = -0,442520443294854
       sin(3,700) = -0,529836140908495
       sin(3,800) = -0,611857890942721
       sin(3,900) = -0,687766159183975
       sin(4,000) = -0,756802495307929
       sin(4,100) = -0,818277111064411
       sin(4,200) = -0,871575772413589
       sin(4,300) = -0,916165936749455
       sin(4,400) = -0,951602073889516
       sin(4,500) = -0,977530117665097
       sin(4,600) = -0,993691003633464
       sin(4,700) = -0,999923257564101
       sin(4,800) = -0,996164608835841
       sin(4,900) = -0,982452612624333
       sin(5,000) = -0,958924274663139
       sin(5,100) = -0,925814682327733
       sin(5,200) = -0,883454655720154
       sin(5,300) = -0,832267442223903
       sin(5,400) = -0,772764487555989
       sin(5,500) = -0,705540325570394
       sin(5,600) = -0,631266637872324
       sin(5,700) = -0,550685542597641
       sin(5,800) = -0,464602179413761
       sin(5,900) = -0,373876664830241
       sin(6,000) = -0,279415498198931
       sin(6,100) = -0,182162504272101
       sin(6,200) = -0,0830894028175026

       abs(-1,000) = 1
       abs(-0,900) = 0,9
       abs(-0,800) = 0,8
       abs(-0,700) = 0,7
       abs(-0,600) = 0,6
       abs(-0,500) = 0,5
       abs(-0,400) = 0,4
       abs(-0,300) = 0,3
       abs(-0,200) = 0,2
       abs(-0,100) = 0,1
       abs(0,000) = 1,38777878078145E-16
       abs(0,100) = 0,0999999999999999
       abs(0,200) = 0,2
       abs(0,300) = 0,3
       abs(0,400) = 0,4
       abs(0,500) = 0,5
       abs(0,600) = 0,6
       abs(0,700) = 0,7
       abs(0,800) = 0,8
       abs(0,900) = 0,9
       abs(1,000) = 1

     cubic(1,000) = 1
     cubic(1,500) = 3,375
     cubic(2,000) = 8
     cubic(2,500) = 15,625
     cubic(3,000) = 27
     cubic(3,500) = 42,875
     cubic(4,000) = 64
     cubic(4,500) = 91,125
     cubic(5,000) = 125

     cubic(1,000) = 1
     cubic(1,500) = 3,375
     cubic(2,000) = 8
     cubic(2,500) = 15,625
     cubic(3,000) = 27
     cubic(3,500) = 42,875
     cubic(4,000) = 64
     cubic(4,500) = 91,125
     cubic(5,000) = 125

Program: The static method Compose in class Application. Illustrates a method that takes two NumericFunction objects as input, and which returns a NumericFunction.
using System;

public class Application {

  public delegate double NumericFunction(double d);

  public static NumericFunction Compose                           
                   (NumericFunction f, NumericFunction g){        
    return delegate(double d){return f(g(d));};                   
  }   

  public static void PrintTableOfFunction
                   (NumericFunction f, string fname, 
                    double from, double to, double step){
    double d;

    for(d = from; d <= to; d += step){
     Console.WriteLine("{0,35}({1,-4:F3}) = {2}", fname, d, f(d));
    }

    Console.WriteLine();
  }

  public static double Square(double d){
    return d*d;
  }

  public static double Cubic(double d){
    return d*d*d;
  }

  public static double Minus3(double d){
    return d-3;
  }

  public static void Main(){
    PrintTableOfFunction(Compose(Cubic, Minus3),                
                         "Cubic of Minus3", 0.0, 5.0, 1.0);     

    PrintTableOfFunction(                                       
      Compose(Square, delegate(double d){                       
                       return d > 2 ? -d : 0;}),
      "Square of if d>2 then -d else 0", 0.0, 5.0, 1.0);
  }
}

Program: Output from the Compose delegate program.
                    Cubic of Minus3(0,000) = -27
                    Cubic of Minus3(1,000) = -8
                    Cubic of Minus3(2,000) = -1
                    Cubic of Minus3(3,000) = 0
                    Cubic of Minus3(4,000) = 1
                    Cubic of Minus3(5,000) = 8

    Square of if d>2 then -d else 0(0,000) = 0
    Square of if d>2 then -d else 0(1,000) = 0
    Square of if d>2 then -d else 0(2,000) = 0
    Square of if d>2 then -d else 0(3,000) = 9
    Square of if d>2 then -d else 0(4,000) = 16
    Square of if d>2 then -d else 0(5,000) = 25

Delegates make it possible to approach the functional programming style

Methods can be passed as parameters to, and returned as results from other methods

Exercise 6.5. Finding and sorting elements in an array

In this exercise we will work with searching and sorting in arrays. To be concrete, we work on an array of type Point, where Point is the type we have been programming in earlier exercises.

Via this exercise you are supposed to learn how to pass a delegate to a method such as Find and Sort. The purpose of passing a delegate to Find is to specify which point we are looking for.

Make an array of Point objects. You can, for instance, use this version of class Point. You can also use a version that you wrote as solution to one of the previous exercises.

Use the static method System.Array.Find to locate the first point in the array that satisfies the condition:

The sum of the x and y coordinates is (very close to) zero

The solution involves the programming of an appropriate delegate in C#. The delegate must be a Point predicate: a method that takes a Point as parameter and returns a boolean value.

Next, in this exercise, sort the list of points by use of one of the static Sort methods in System.Array. Take a look at the Sort methods in System.Array. There is an overwhelming amount of these! We will use the one that takes a Comparison delegate, Comparison<T>, as the second parameter. Please find this method in your documentation browser. Why do we need to pass a Comparison predicate to the Sort method?

Comparison<Point> is a delegate that compares two points, say p1 and p2. Pass an actual delegate parameter to Sort in which

   p1 <= p2 if and only if p1.X + p1.Y <= p2.X + p2.Y

Please notice that a comparsion between p1 and p2 must return an integer. A negative integer means that p1 is less than p2. Zero means that p1 is equal to p2. A positive integer means that p1 is greater than p2.

Test run you program. Is your Point array sorted in the way you excepts?

Exercise 6.5. How local are local variables and formal parameters?

When we run the following program

using System;
public class Application { 

  public delegate double NumericFunction(double d);   
  static double factor = 4.0;

  public static NumericFunction MakeMultiplier(double factor){
     return delegate(double input){return input * factor;};
  }

  public static void Main(){
    NumericFunction f = MakeMultiplier(3.0); 
    double input = 5.0;

    Console.WriteLine("factor = {0}", factor);
    Console.WriteLine("input = {0}", input);
    Console.WriteLine("f is a generated function which multiplies its input with factor");
    Console.WriteLine("f(input) = input * factor = {0}", f(input));
  }
}

we get this output

factor = 4
input = 5
f is a generated function which multiplies its input with factor
f(input) = input * factor = 15

Explain!

Read more about delegates in the text book version of this material.

Delegates that contain instance methods
Slide Annotated slide Contents Index
References Textbook 
The methods on the previous page were all static. We will now explain how to deal with instance methods in relation to delegates.

In the examples until now all delegates contained static methods

In the case that delegates contain instance methods, the receiver is part the delegate

Program: A Messenger class and a Message delegate. Class Messenger encapsulates a Message delegate. Via the DoSend method it is possible to activate the method(s) in the encapsulated Message delegate.
using System;

public delegate void Message(string txt);  
                                           
public class Messenger{           
                                  
  private string sender;
  private Message message;                 

  public Messenger(string sender){
   this.sender = sender;
   message = null;
  }

  public Messenger(string sender, Message aMessage){  
   this.sender = sender;
   message = aMessage;
  }

  public void DoSend(){  
   message("Message from " + sender);
  }
}

Program: A very simple class A with an instance method MethodA. A class A with an instance method MethodA.
using System;

public class A{   
                  
  private int state;  

  public A(int i){  
    state = i;
  }

  public void MethodA(string s){                   
    Console.WriteLine("A: {0}, {1}", state, s);    
  }                                                
}

Program: An Application class which accesses an instance method in class A. Illustrates how to deal with an instance method of a particular object as a delegate.
using System;

public class Application{  
                           
  public static void Main(){
    A a1 = new A(1),  
      a2 = new A(2),  
      a3 = new A(3);

    Messenger m = new Messenger("CS at AAU", a2.MethodA);  
                                                           
    m.DoSend();  
 }

}

Program: Output from Main of class Application. Confirms that MethodA in a2 is called.
A: 2, Message from CS at AAU

Read more about delegates with instance methods in the text book version of this material.

Multivalued delegates
Slide Annotated slide Contents Index
References Textbook 
A delegate can contain more than one method.

A delegate can contain an arbitrary number of methods

Program: Install and UnInstall message methods in the Messenger class. Similar to class Messenger on the previous page. Allows adding and subtraction of methods to the delegate in a Messenger object via the methods InstallMessage and UninstallMessage.
using System;

public delegate void Message(string txt);  

public class Messenger{
  
  private string sender;
  private Message message;  

  public Messenger(string sender){  
   this.sender = sender;
   message = null;
  }

  public Messenger(string sender, Message aMessage){  
   this.sender = sender;
   message = aMessage;
  }

  public void InstallMessage(Message mes){  
   this.message += mes;                     
  }   

  public void UnInstallMessage(Message mes){  
   this.message -= mes;                       
  }   

  public void DoSend(){                       
   message("Message from " + sender);         
  }
}

Program: A simple class A. Identical to class A on the previous page. Just a simple class with an instance method.
using System;

public class A{
  
  private int state;  

  public A(int i){  
    state = i;
  }

  public void MethodA(string s){  
    Console.WriteLine("A: {0}, {1}", state, s);
  }   
}

Program: An Application class. Illustrates that it is possible to add (and delete) a number of different methods (of same signature) to a given instance of class Messenger.
using System;

public class Application{

  public static void Main(){
    A a1 = new A(1),  
      a2 = new A(2),
      a3 = new A(3);

    Messenger m = new Messenger("CS at AAU", a2.MethodA);  
    m.InstallMessage(a1.MethodA);  
    m.DoSend();  
    Console.WriteLine();

    m.InstallMessage(a3.MethodA);   
    m.InstallMessage(a3.MethodA);
    m.DoSend();
    Console.WriteLine();

    m.UnInstallMessage(a3.MethodA);  
    m.UnInstallMessage(a1.MethodA);  
    m.DoSend();
 }
}

Program: Output from Main of class Application. Confirms that a delegate calls all of its methods.
A: 2, Message from CS at AAU
A: 1, Message from CS at AAU

A: 2, Message from CS at AAU
A: 1, Message from CS at AAU
A: 3, Message from CS at AAU
A: 3, Message from CS at AAU

A: 2, Message from CS at AAU
A: 3, Message from CS at AAU

Summary of delegates

Delegates are types. The values of delegate types are methods

With delegates, methods become first class citizens

A variable of a delegate type can contain both static and instance methods

A variable of a delegate type can even contain two or more methods

Read more about multivalued delegates in the text book version of this material.

Lambda Expressions
Slide Annotated slide Contents Index
References Textbook 

A lambda expression in C#3.0 provides a smooth notation for anonymous functions

Anonymous method expressions in C#2.0 - delegate(...) {statements} - is very similar to lambda expressions

Program: Five equivalent functions - from anonymous method expressions to lambda expressions.
using System;
using System.Collections.Generic;

class Program{

  public delegate double NumericFunction(double d);   

  public static void Main(){

    NumericFunction[] equivalentFunctions = 
       new NumericFunction[]{
         delegate (double d){return d*d*d;},    
         (double d) => {return d*d*d;},         
         (double d) => d*d*d,                   
         (d) => d*d*d,                          
         d => d*d*d                             
      };

    foreach(NumericFunction nf in equivalentFunctions)
       Console.WriteLine("NumericFunction({0}) = {1}", 5, nf(5));
  }

}

Program: Program output.
NumericFunction(5) = 125
NumericFunction(5) = 125
NumericFunction(5) = 125
NumericFunction(5) = 125
NumericFunction(5) = 125

  • Lambda expression characteristics:

    • The body can be a statement block or an expression

    • Uses the operator => which has low priority and is right associative

    • May involve implicit inference of parameter types

    • Lambda expressions serve as syntactic sugar for a delegate expression


Events

Events
Slide Annotated slide Contents Index
References Textbook 

The concept event:

In a program, an event contains some actions that must be carried out when the event is triggered

  • Event

    • Belongs to a class

    • Contains one or more operations, which are called when the event is triggered.

    • The operations in the event are called implicitly

  • Operation

    • Belongs to a class

    • Is called explicitly - directly or indirectly - by other operations

Inversion of control

Don't call us - we call you

Events in C#
Slide Annotated slide Contents Index
References Textbook 

From inside some class, an event is a variable of a delegate type.

From outside a class, it is only possible to add to or remove from an event.

Events are intended to provide notifications, typically in relation to graphical user interfaces.

  • Restrictions on events

    • An event can only be activated from within the class to which the event belongs

    • From outside the class it is only possible to add (with +=) or subtract (with -=) operations to an event.

      • It is not possible to 'reset' the event with an ordinary assignment

There exists a generic delegate EventHandler<TEVentArgs> which is intended to be used as the type of standard events in .NET

Examples of events
Slide Annotated slide Contents Index
References Textbook 

A die tossing program that reports 'two sixes in a row'

A graphical user interface (GUI) program with two buttons

Program: The die class with history and dieNotifier.
using System;
using System.Collections.Generic;

public delegate void Notifier(string message);  

public class Die {   
                     
  private int numberOfEyes;
  private Random randomNumberSupplier; 
  private int maxNumberOfEyes;
  private List<int> history;   
  public event Notifier twoSixesInARow;  
                                         
  public int NumberOfEyes{
     get {return numberOfEyes;}
  }

  public Die (): this(6){}   

  public Die (int maxNumberOfEyes){   
    randomNumberSupplier = new Random(unchecked((int)DateTime.Now.Ticks));
    this.maxNumberOfEyes = maxNumberOfEyes;
    numberOfEyes = randomNumberSupplier.Next(1, maxNumberOfEyes + 1);
    history = new List<int>();
    history.Add(numberOfEyes);
  }
    
  public void Toss (){                                                     
    numberOfEyes = randomNumberSupplier.Next(1,maxNumberOfEyes + 1);
    history.Add(numberOfEyes);                                             
    if (DoWeHaveTwoSixesInARow(history))                                   
       twoSixesInARow("Two sixes in a row");                               
  }                                                                        

  private bool DoWeHaveTwoSixesInARow(List<int> history){                  
    int histLength = history.Count;                                        
    return histLength >= 2 &&                                              
           history[histLength-1] == 6 &&                                   
           history[histLength-2] == 6;
  }
       
  public override String ToString(){
    return String.Format("Die[{0}]: {1}", maxNumberOfEyes, NumberOfEyes);
  }
}

Program: A client of die that reports 'two sixes in a row' via an event.
using System;

class diceApp {   

  public static void Main(){

    Die d1 = new Die();   

    d1.twoSixesInARow +=               
     delegate (string mes){            
       Console.WriteLine(mes);
     };

    for(int i = 1; i < 100; i++){      
      d1.Toss();
      Console.WriteLine("{0}: {1}", i, d1.NumberOfEyes);  
    }

 }
}

Program: Possible program output of the die application (abbreviated).
1: 6
2: 4
...
32: 3
33: 6
Two sixes in a row
34: 6
Two sixes in a row
35: 6
...
66: 2
67: 6
Two sixes in a row
68: 6
69: 2
70: 4
...
97: 6
Two sixes in a row
98: 6
99: 3

Exercise 6.6. Additional Die events

In this exercise we add yet another method to the existing event i class Die, and we add another event to Die.

In the Die event example, we have a public event called twoSixesInARow which is triggered if a die shows two sixes in a row. In the sample client program we add an anonymous method to this event which reports the string parameter of the event on standard output.

Add yet another method to the twoSixesInARow event which counts the number of times 'two sixes in a row' appear. For this purpose we need - quite naturally - an integer variable for counting. Where should this variable be located relative to the 'counting method': Will you place the variable inside the new method, inside the Die class, or inside the client class of the Die?

Add a similar event called fullHouse, of the same type Notifier, which is triggered if the Die tosses a full house. A full house means (inspired from the rules of Yahtzee) two tosses of one kind and three tosses of another kind - in a row. For instance, the toss sequence 5 6 5 6 5 leads to a full house. Similarly, the 1 4 4 4 1 leads to a full house. The toss sequence 5 1 6 6 6 6 5 does not contain a full house sequence, and the toss sequence 6 6 6 6 6 is not a full house.

Be sure to test-drive the program and watch for triggering of both events.

Program: A window with two buttons and a textbox.
using System;
using System.Windows.Forms;
using System.Drawing;

// In System:
// public delegate void EventHandler (Object sender, EventArgs e)

public class Window: Form{   

  private Button b1, b2;     
  private TextBox tb;        

  // Constructor
  public Window (){               
    this.Size=new Size(150,200);  
                                  
    b1 = new Button();            
    b1.Text="Click Me";
    b1.Size=new Size(100,25);
    b1.Location = new Point(25,25);
    b1.BackColor = Color.Yellow;
    b1.Click += ClickHandler;                              
                              // Alternatively:            
                              // b1.Click+=new EventHandler(ClickHandler);
    b2 = new Button();  
    b2.Text="Erase";
    b2.Size=new Size(100,25);
    b2.Location = new Point(25,55);
    b2.BackColor=Color.Green; 
    b2.Click += EraseHandler; 
                              // Alternatively:
                              // b2.Click+=new EventHandler(EraseHandler);
    tb = new TextBox();                                    
    tb.Location = new Point(25,100);
    tb.Size=new Size(100,25);
    tb.BackColor=Color.White;
    tb.ReadOnly=true;
    tb.RightToLeft=RightToLeft.Yes;

    this.Controls.Add(b1);    
    this.Controls.Add(b2);    
    this.Controls.Add(tb);    
  }

  // Event handler:
  private void ClickHandler(object obj, EventArgs ea) {   
    tb.Text = "You clicked me";                           
  }                                                       

  // Event handler:                                       
  private void EraseHandler(object obj, EventArgs ea) {   
    tb.Text = "";                                         
  }

}

class ButtonTest{   

  public static void Main(){
    Window win = new Window();   
    Application.Run(win);        
  }

}


Patterns and Techniques

Motivation - Example
Slide Annotated slide Contents Index
References 

The subject (weather service object) to the left and its three observers (weather watcher objects) to the right. The Weather Service Object get its information various sensors.

The observer design pattern
Slide Annotated slide Contents Index
References Textbook 

The subject (weather service object) to the left and its three observers (weather watcher objects) to the right. The Weather Service Object get its information various sensors.

The Observer is often used to ensure a loose coupling between an application and its user interface

In general, Observer can be used whenever a set of observer objects need to be informed about state changes in a subject object

The loose couple is ensured by observers that subscribe to events from the subjects. Upon interesting and relevant events in the subject, the subject broadcasts to its observers. The observers may then ask the subject to supply additional information.

The main resposibility is shifted from the subject to the observers.

Reference

Program: Template of the Subject class. The subject class corresponds to the Weather Service.
using System.Collections;
namespace Templates.Observer {

 public class Subject {                             
   // Subject instance variables

   private ArrayList observers = new ArrayList();   

   public void Attach(Observer o){                  
     observers.Add(o);
   }

   public void Detach(Observer o){
     observers.Remove(o);
   }

   public void Notify(){                            
     foreach(Observer o in observers) o.Update();   
   }                                                

   public SubjectState GetState(){                  
     return new SubjectState();                     
   }                                                
 }

 public class SubjectState {                        
   // Selected state of the subject
 }
}

Program: A templates of the Observer class. The observer class corresponds to a Weather Watcher.
using System.Collections;
namespace Templates.Observer {

 public class Observer {                      

   private Subject mySubject;                 
                                              
   public Observer (Subject s){               
     mySubject = s;
   }   

   public void Update(){                      
      // ...                                  

      SubjectState state = mySubject.GetState();   
                                                   
      //   if (the state is interesting){
      //      react on state change
      //   }
   }
 }
}

Program: Application of the Subject and Observer classes. A client class that sets up the subject and the observers, handles subscriptions, and notifies the subject.
using Templates.Observer;
class Client {

  public static void Main(){
     Subject subj = new Subject();                                      
     Observer o1 = new Observer(subj),                                  
              o2 = new Observer(subj),
              o3 = new Observer(subj);

     subj.Attach(o1);  // o1 subscribes to updates from subj.            
     subj.Attach(o2);  // o2 subscribes to updates from subj.

     subj.Notify();    // Following some state changes in subj
                       // notify observers.
  }
}

An Observer Example
Slide Annotated slide Contents Index
References 

We show the concrete programs that implement observer in the weather center scenario.

Program: A WeatherCenter (subject) and TemperatureWatcher (observer).
using System;
using System.Collections;

namespace Templates.Observer {

 // An observer
 public class TemperatureWatcher {

   private float currentTemperature;
   private WeatherCenter mySubject;
   private string watcherName; 

   public TemperatureWatcher (WeatherCenter s, float initTemp, 
                              string name){
     mySubject = s;
     currentTemperature = initTemp;
     watcherName = name;
   }   

   public void Update(){
      SubjectState state = mySubject.GetState();
      currentTemperature = state.temperature;      
      Console.WriteLine
          ("Temperature watcher {1}: the temperature is now {0}.", 
            currentTemperature, watcherName);
   }

 }

 // A subject
 public class WeatherCenter {

   private float temperature,
                 rainAmount,
                 airPressure;

   public WeatherCenter(float temp, float rain, float pres){
      temperature = temp;
      rainAmount = rain;
      airPressure = pres;
   }

   public void WeatherUpdate(float temp, float rain, float pres){
      float oldTemperature = this.temperature,
            oldRainAmount = this.rainAmount,
            oldAirPressure = this.airPressure;

      this.temperature = temp;
      this.rainAmount += rain;
      this.airPressure = pres;

      if (Math.Abs(oldTemperature - this.temperature) > 2.0F ||
          rain > 0.5F ||
          Math.Abs(oldAirPressure - this.airPressure) > 3.0F)
       this.Notify();
   }
        
   private ArrayList observers = new ArrayList();

   public void Attach(TemperatureWatcher o){  
     observers.Add(o);
   }

   public void Detach(TemperatureWatcher o){
     observers.Remove(o);
   }

   public void Notify(){
     foreach(TemperatureWatcher o in observers)
        o.Update();
   }

   public SubjectState GetState(){
     return new SubjectState(temperature, rainAmount, airPressure);
   }

 }

 public class SubjectState {
   public float temperature,
                rainAmount,
                airPressure;

   public SubjectState(float temp, float rain, float pres){
     temperature = temp;
     rainAmount = rain;
     airPressure = pres;
   }
 }

}

Program: Application of the WeatherCenter and TemperatureWatcher.
using Templates.Observer;

class Client {

  public static void Main(){

     WeatherCenter subj = new WeatherCenter(25.0F, 0.0F, 1020.0F);
     TemperatureWatcher
              o1 = new TemperatureWatcher(subj, 25.0F, "w1"),
              o2 = new TemperatureWatcher(subj, 25.0F, "w2");

     subj.Attach(o1);
     subj.Attach(o2);

     subj.WeatherUpdate(23.0F, 0.0F, 1020.0F);
     subj.WeatherUpdate(23.0F, 0.0F, 1020.0F);
     subj.WeatherUpdate(23.0F, 0.0F, 1020.0F);
     subj.WeatherUpdate(24.0F, 0.0F,  920.0F);
     subj.WeatherUpdate(21.0F, 0.0F, 1050.0F);

  }

}

Program: Output of the WeatherCenter and TemperatureWatcher application.
Temperature watcher w1: the temperature is now 24.
Temperature watcher w2: the temperature is now 24.
Temperature watcher w1: the temperature is now 21.
Temperature watcher w2: the temperature is now 21.

It would be useful if a number of different classes could act as observers

Reference

Observer with Delegates and Events
Slide Annotated slide Contents Index
References Textbook 

It is possible to implement the observer design pattern with events and delegates

Program: Template of the Subject class.
using System.Collections;
namespace Templates.Observer {

 public delegate void Notification(SubjectState ss);

 public class Subject {
   // Subject instance variable

   private event Notification observerNotifier;

   public void AddNotifier(Notification n){
     observerNotifier += n;
   }

   public void RemoveNotifier(Notification n){
     observerNotifier -= n;
   }

   public void Notify(){
     observerNotifier(new SubjectState());
   }
 }

 public class SubjectState {
   // Selected state of the subject
 }
}

Program: Template of the Observer class.
using System.Collections;
namespace Templates.Observer {

 public class Observer {

   public Observer (){
     // ...
   }   

   public void Update(SubjectState ss){
      //   if (the state ss is interesting){
      //      react on state change
      //   }
   }   

 }
}

Program: Application of the Subject and Observer classes.
using Templates.Observer;
class Client {

  public static void Main(){
     Subject subj = new Subject();
     Observer o1 = new Observer(),
              o2 = new Observer(),
              o3 = new Observer();

     subj.AddNotifier(o1.Update);   
     subj.AddNotifier(o2.Update);   
                                    
     subj.Notify();    
  }
}

Observer Example with Delegates and Events
Slide Annotated slide Contents Index
References 

Program: Application of the two different Watchers.
using Templates.Observer;

class Client {

  public static void Main(){

     WeatherCenter subj = new WeatherCenter(25.0F, 0.0F, 1020.0F);
     TemperatureWatcher
              o1 = new TemperatureWatcher(subj, 25.0F, "w1"),
              o2 = new TemperatureWatcher(subj, 25.0F, "w2");
     RainWatcher
              o3 = new RainWatcher(subj, 0.0F, "w3");

     subj.AddNotifier(o1.TemperatureAlarm);  // Adding instance methods
     subj.AddNotifier(o2.TemperatureAlarm);  // to an event in
     subj.AddNotifier(o3.RainAlarm);         // the subject

     subj.WeatherUpdate(23.0F, 0.0F, 1020.0F);
     subj.WeatherUpdate(23.0F, 2.0F, 1020.0F);
     subj.WeatherUpdate(23.0F, 0.0F, 1020.0F);
     subj.WeatherUpdate(24.0F, 0.3F,  920.0F);
     subj.WeatherUpdate(21.0F, 3.7F, 1050.0F);

  }

}

Program: A WeatherCenter (subject) and Temperature/Rain Watchers (observer) with events.
using System;
using System.Collections;

namespace Templates.Observer {

 // Delegate type:
 public delegate void 
    WeatherNotification(float temp, float rain, float pres);

 // An observer
 public class TemperatureWatcher {

   private float currentTemperature;
   private WeatherCenter mySubject;
   private string watcherName; 

   public TemperatureWatcher (WeatherCenter s, float initTemp, string name){
     mySubject = s;
     currentTemperature = initTemp;
     watcherName = name;
   }   

   public void TemperatureAlarm(float temp, float rain, float pres){
      currentTemperature = temp;
      Console.WriteLine(
           "Temperature watcher {1}: the temperature is now {0}.", 
           currentTemperature, watcherName);
   }
 }    

 public class RainWatcher {

   private float currentRainAmount;
   private WeatherCenter mySubject;
   private string watcherName; 

   public RainWatcher (WeatherCenter s, float initAmount, string name){
     mySubject = s;
     currentRainAmount = initAmount;
     watcherName = name;
   }      

   public void RainAlarm(float temp, float rain, float pres){
      currentRainAmount = rain;
      Console.WriteLine("Rain watcher {1}: Accumulated rain fall: {0}.", 
                         currentRainAmount, watcherName);
   }   
 }    

 // A subject
 public class WeatherCenter {

   private float temperature,
                 rainAmount,
                 airPressure;

   public WeatherCenter(float temp, float rain, float pres){
      temperature = temp;
      rainAmount = rain;
      airPressure = pres;
   }

   public void WeatherUpdate(float temp, float rain, float pres){
      float oldTemperature = this.temperature,
            oldRainAmount = this.rainAmount,
            oldAirPressure = this.airPressure;

      this.temperature = temp;
      this.rainAmount += rain;
      this.airPressure = pres;

      if (Math.Abs(oldTemperature - this.temperature) > 2.0F ||
          rain > 0.5F ||
          Math.Abs(oldAirPressure - this.airPressure) > 3.0F)
       this.Notify();
   }
        
   private event WeatherNotification weatherNotifier;

   public void AddNotifier(WeatherNotification n){
     weatherNotifier += n;
   }

   public void RemoveNotifier(WeatherNotification n){
     weatherNotifier -= n;
   }

   public void Notify(){
     weatherNotifier(temperature, rainAmount, airPressure);
   }

 }    


}

Program: Output of the WeatherCenter and Watchers application.
Temperature watcher w1: the temperature is now 23.
Temperature watcher w2: the temperature is now 23.
Rain watcher w3: Accumulated rain fall: 2.
Temperature watcher w1: the temperature is now 24.
Temperature watcher w2: the temperature is now 24.
Rain watcher w3: Accumulated rain fall: 2,3.
Temperature watcher w1: the temperature is now 21.
Temperature watcher w2: the temperature is now 21.
Rain watcher w3: Accumulated rain fall: 6.


Collected references
Contents Index
Design patterns Earlier in these notes
Observer Later in these notes

 

Chapter 6: Operators, Delegates, and Events
Course home     Author home     About producing this web     Previous lecture (top)     Next lecture (top)     Previous lecture (bund)     Next lecture (bund)     
Generated: February 7, 2011, 12:15:45