Exercise index of this lecture   Alphabetic index   Course home   

Exercises and solutions
Reference types, Value types, and Patterns


4.1   Equality of value types and reference types  

Take a close look at the following program which uses the == operator on integers.

using System;

class WorderingAboutEquality{

  public static void Main(){
    int i = 5,
        j = 5;

    object m = 5,
           n = 5;  

    Console.WriteLine(i == j);
    Console.WriteLine(m == n);
  }

}

Predict the result of the program. Compile and run the program, and explain the results. Were your predictions correct?

Solution

We study the following program:

using System;

class WorderingAboutEquality{

  public static void Main(){
    int i = 5,
        j = 5;

    object m = 5,
           n = 5;  

    Console.WriteLine(i == j);
    Console.WriteLine(m == n);
  }

}

i == j is value comparison, and as such the value of i == j is true.

First notice that object is an alias for System.Object.

The expression m == n compares references because the two 5-values are boxed when assigned to values of a reference type, see the section about boxing and unboxing. The == operator in m == n is determined by the static type of m and n, namely object. For m and n of type object, == will lead to comparison of references. m and n reference two different objects (which hold the same value). Therefore the value of the boolean expression m == n is false.


4.2   Are playing cards and dice immutable?  

Evaluate and discuss the classes Die and Card with respect to mutability.

Make sure that you understand what mutability means relative to the concrete code. Explain it to your fellow programmers!

More specific, can you argue for or against the claim that a Die instance/value should be mutable?

And similarly, can you argue for or against the claim that a Card instance/value should be mutable?

Why is it natural to use structs for immutable objects and classes for mutable objects? Please compare your findings with the remarks in 'the solution' when it is released.

Solution

I find it reasonable that a Die is mutable. Each time we toss it we change/mutate its state.

I do not think it is justifiable to have a mutable Card class. It would be strange - and against our intuition - to change the suite or value of an already existing card. The state of a Card is never changed.

In the code, mutability reveals itself by assignments to the instance variables in the bodies of methods.

Why is it natural to use structs for immutable objects and classes for mutable objects?

Classes are reference types. There are often several references to the same objects (this is called aliasing). If the state of the object is update/mutated it can be observed via all the references. Instances of classes are only copied on demand (for instance via Clone or via use of a copy constructor). In your client program you need to be aware of this. Due to aliasing via references, the copy of an object cannot always be used in place of the original object! The identity of class instances are strong.

In contrast, structs are value types. Instances of structs are copied a lot - by assignments, parameter passing, and by return. A copy of a struct instance is just as good as the original. In your client program, you should not notice if you deal with the original struct instance or a copy of it. Mutation is dealt with by creating a "mutated copy". The identity of struct instances are weak.

If you want to mutate an object you should be very conscious about making copies of the object. Such consciousness can be handled with classes, but not with structs.


4.3   Privacy Leaks  

The starting point of this exercise is the observations about privacy leaks on the accompanying slide.

Make sure that you understand the problem. Test-drive the program (together with its dependent Person class and Date class) on your own computer.

If you have not already done so, read the section about privacy leaks in the textbook!

Find a good solution to the problem, program it, and test your solution.

Discuss to which degree you will expect that this problem to occur in everyday programming situations.


4.4   Mutable and immutable Point objects with Move methods  

On the slides about mutable and immutable structs we have illustrated two versions of struct Point with two different implementations of the Move method.

In the first struct, Move mutates the x and y coordinates and it returns this- the current object.

The second struct is immutable and Move returns a new instance of Point which is moved relative to the receiver of the Move message.

For both versions of Point we use the same client class, which sends Move messages to different points. In the lecture - and in the textbook - the differences between the first struct and the second struct has been explained relative to the client class. Be sure that you understand the difference!

Now, you are asked to implement version 1 and version 2 of Point from above with classes instead of structs. Compile and run each of them relative to the given client class. In each of the two cases explain the behavior of the Move methods in the classes.

In each of the two cases, explain how many instances of class Point there is created.

Solution

The first class is the following:

using System;

public class Point {
  private double x, y;

  public Point(double x, double y){
   this.x = x; this.y = y;
  }

  public double Getx (){
    return x;
  }

  public double Gety (){
    return y;
  }

  public  Point  Move(double dx, double dy){
    x += dx; y += dy; 
    return this;
  }

  public override string ToString(){
    return "Point: " + "(" + x + "," + y + ")" + ".";
  }
}

In the client program

using System;

public class Application{

   public static void Main(){
    Point p1 = new Point(1.0, 2.0);
    
    p1.Move(3.0, 4.0).Move(5.0, 6.0);
    Console.WriteLine("{0}", p1);      // Where is p1 located?
  }
}

only a single Point object is involved. As expected, it is moved from (1.0, 2.0) via (4.0, 6.0) to (9.0, 12.0). In the similar version, where Point is implemented as a struct, the problem was that the expression p1.Move(3.0, 4.0) returns a copy of a point, which subsequently is moved. This problem does not occur when we work with classes. The client program only creates a single Point object.

In the next version of the Point class

using System;

public class Point {
  private readonly double x, y;

  public Point(double x, double y){
   this.x = x; this.y = y;
  }

  public double Getx (){
    return x;
  }

  public double Gety (){
    return y;
  }

  public Point Move(double dx, double dy){
    return new Point(x+dx, y+dy);
  }

  public override string ToString(){
    return "Point: " + "(" + x + "," + y + ")" + ".";
  }
}

each Move message creates a new Point object. Therefore, the client class

using System;

public class Application{

   public static void Main(){
     Point p1 = new Point(1.0, 2.0),
           p2;
    
     p2 = p1.Move(3.0, 4.0).Move(5.0, 6.0);
     Console.WriteLine("{0} {1}", p1, p2);
   }

}

creates three instances of class Point. First (1.0, 2.0) is referred from p1. The expression p1.Move(3.0, 4.0) returns a new Point object located at (4.0, 6.0). This point receives the message Move(5.0, 6.0). This allocates the third Point object located at (9.0, 12.0). A reference to this object is assigned to p2. Hereby p2 refers the point located at (9.0, 12.0). The two copies are wasteful and actually not needed.

With use of classes, we do not encounter the same problem as we did with the first version of the struct Point.

All taken together, I recommend the second version of struct Point and the first version of class Point. You should use program immutable types with structs and mutable types with classes.


4.5   Pyramid BankAccounts  

This exercise can be seen as a continuation of the bank account exercise in which we supplied a bank account with a backup account. The primary purpose of the exercises is to train recursive object-oriented programming.

We organize bank accounts in a tree structure (called a pyramid) in which each bank account may have two sons which are also bank accounts. The money in the account are somehow distributed on the accounts of the pyramid.

Program a version of class BankAccount with an owner, a balance, and possible references to two additional bank accounts: leftAccount and rightAccount. A skeleton of this class is provided together with a client program. You can assume that an account is either a leaf or the account has two sons.

  • Program the Balance method. It should add together the contributions from all accounts in the pyramid.
  • Program the method Deposit. The message ba.Deposit(amount) inserts one third of the amount in the receiver, one third in the left part of the pyramid (recursively), and one third in the right part of the pyramid (recursively). In case of a leaf account, ordinary simple depositing is used.
  • Program the method Withdraw. The message ba.Withdraw(amount) withdraws recursively half of the amount from the left part of the pyramid and recursively half of the amount from the right part of the pyramid. In case of a leaf account, ordinary simple withdrawing is used.
  • Program a method DistributeEven which distributes the total amount of money evenly on all accounts in the pyramid.

Solution

Here is my solution:

using System;

public class BankAccount {

   private string owner;
   private decimal balance;
   private BankAccount leftAccount, rightAccount;

   public BankAccount(string owner, decimal balance) {
      this.owner = owner; 
      this.balance = balance;
      this.leftAccount = null;
      this.rightAccount = null;
   }

   public BankAccount(string owner, decimal balance, BankAccount leftAccount, BankAccount rightAccount) {
      this.owner = owner; 
      this.balance = balance;
      this.leftAccount = leftAccount;
      this.rightAccount = rightAccount;
   }

   private bool LeafAccount(){
      return leftAccount == null && rightAccount == null;
  }

   public decimal Balance () {
      if (this.LeafAccount())
         return balance;
      else 
         return balance + leftAccount.Balance() + rightAccount.Balance();
   }

   public void Withdraw (decimal amount) {
      if (this.LeafAccount())
        balance -= amount;
      else{
        this.leftAccount.Withdraw(amount/2);
        this.rightAccount.Withdraw(amount/2);
      }
   }

   public void Deposit (decimal amount) {
      if (this.LeafAccount())
         balance += amount;         
      else {
         balance += amount/3;
         leftAccount.Deposit(amount/3);
         rightAccount.Deposit(amount/3);
      }
   }

   public void DistributeEven(){
     int number = this.CountAccounts();
     decimal totalAmount = this.Balance();
     this.ForceBalance(totalAmount/number);
   }

   private int CountAccounts(){
      if (this.LeafAccount())
        return 1;
      else return 1 + 
           leftAccount.CountAccounts() + rightAccount.CountAccounts();
   }

   private void ForceBalance(decimal amount){
      balance = amount;
      if (!(this.LeafAccount())){
         leftAccount.ForceBalance(amount);
         rightAccount.ForceBalance(amount);
      }
   }

   public override string ToString() {
      return String.Format("{0} {1,12:c2} (local amount: {2,12:c2})", owner, Balance(), balance);
  }
} 


Generated: Monday February 7, 2011, 12:14:35