Classes and objects III |
When making objects with new, we can provide class arguments, for instance:
scala> var d4 = new Date(2012, 4, 9) d4: Date = Monday, April 9, 2012
The arguments 2012, 4, 9 correspond to the parameters year, month, and day that appear in parentheses after the class name in the class declaration. Class parameters can be val fields, var fields, or simply parameters for the construction of the class.
Here is an immutable Point class. Both class parameters are val fields:
class Point(val x: Int, val y: Int) { override def toString: String = "(%d, %d)".format(x, y) }Here is a mutable Rect class to store an axis-parallel rectangle. The class parameters are var fields:
class Rect(var corner: Point, var width: Int, var height: Int) { require(width > 0 && height > 0) override def toString: String = "[%d ~ %d, %d ~ %d]".format(corner.x, corner.x + width, corner.y, corner.y + height) }
The first class parameter is a Point object, so I should create rectangle objects like this:
scala> var r = new Rect(new Point(10, 20), 50, 20) r: Rect = [10 ~ 60, 20 ~ 40]
Let's say I don't like this, I want to be able to create rectangles by giving four numbers, like this:
scala> var r = new Rect(10, 20, 50, 20) r: Rect = [10 ~ 60, 20 ~ 40]
I still want to be able to store the top-left corner as a Point object, but because the class parameters now look different, I cannot define the corner field in the class parameter list. Instead I need to do it like this:
class Rect(x: Int, y: Int, var width: Int, var height: Int) { var corner = new Point(x, y) require(width > 0 && height > 0) override def toString: String = "[%d ~ %d, %d ~ %d]".format(corner.x, corner.x + width, corner.y, corner.y + height) }
Here the the corner field is defined in the body of the class. The class parameters x and y are simply parameters of the method that constructs objects of type Rect, they are not fields of the class:
scala> r.x <console>:11: error: value x is not a member of Rect scala> r.y <console>:11: error: value y is not a member of Rect scala> r.corner res6: Point = (10, 20)
So every occurrence of val or var in the body of the class defines a field of the class. Every field has to be initialized with a starting value. This value is used when the object is constructed.
Let us define an Accumulator, a counter that starts with zero, and to which we can add a value:
class Accumulator { var sum = 0 def add(n: Int) { sum += n } }The class itself has no class parameters this time, so no arguments are given when we create objects with new:
scala> var acc1 = new Accumulator acc1: Accumulator = Accumulator@1dd70055 scala> acc1.add(7) scala> acc1.add(13) scala> println(acc1.sum) 20
What is not so nice here is that we could accidentally modify the sum-field of the Accumulator object:
scala> var acc2 = new Accumulator acc2: Accumulator = Accumulator@d91a224 scala> acc2.add(7) scala> acc2.add(23) scala> acc2.sum = 0 acc2.sum: Int = 0 scala> acc2.add(19) scala> acc2.sum res17: Int = 19The programmer using the Accumulator class made a mistake here and set acc2.sum back to zero—so now the final result is wrong.
This is an example that shows the importance of privacy. A client—that is, code using our class— should consider an object as a black box. The client should not need or want to know how the object is implemented, and must only use the methods provided by the object to communicate with it. Our Accumulator object should have two operations: adding a number to the current sum, and reading out the current sum. It should not be possible to modify the current sum.
We can achieve this by forbidding client code to access the field that stores the current sum. To do so, we declare the field to be private. However, that means that we cannot access it at all, so we have to add a new method to be able to read the current value of the summation:
class Accumulator { private var current = 0 def add(n: Int) { current += n } def sum: Int = current }Here is how we use it correctly:
scala> var acc1 = new Accumulator acc1: Accumulator = Accumulator@2b37d296 scala> acc1.add(7) scala> acc1.add(13) scala> acc1.sum res23: Int = 20
But now see what happens when we make a mistake and try to change the sum:
scala> acc1.sum = 0 <console>:9: error: value sum_= is not a member of Accumulator
Since sum is not a field but a method that returns a value, we cannot assign to it.
And even if we tried to change the field current directly, the compiler would catch this programming error:
scala> acc1.current = 0 <console>:12: error: variable current in class Accumulator cannot be accessed in Accumulator
In fact, even looking at the value of current is forbidden (that's why we needed the sum method):
scala> acc1.current <console>:10: error: variable current in class Accumulator cannot be accessed in Accumulator
The private keyword means that the member can be accessed only from methods inside the class. You can use it both for fields and for methods. So a private method is a method that can be called only from other methods in the same class.
When you create a new object, it is often not enough to just set the fields to some initial value. If the computations are simple, then you can write them directly inside the class body. This code is executed every time an object is constructed.
For instance, in our Date class we used some require statements to ensure the consistency of the object—they are executed every time a new Date object is created.
In our Rectangle object, we had the line
var corner = new Point(x, y)inside the class body. Since it starts with var, this line defines a mutable field corner of the class. This field is initialized with some simple computation, namely the creation of a new Point object.
To see a more interesting example, let us program a Blackjack game. We first need a class that represents the playing cards. Remember that there are four suits called clubs, spades, hearts, and diamonds, and 13 faces, namely 2 to 10, Jack, Queen, King, and Ace.
val Suits = Array("Clubs", "Spades", "Hearts", "Diamonds") val Faces = Array("2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King", "Ace") class Card(val face: String, val suit: String) { require(Suits contains suit) require(Faces contains face) override def toString: String = { val a = if (face == "Ace" || face == "8") "an " else "a " a + face + " of " + suit } def value: Int = face match { case "Ace" => 11 case "Jack" => 10 case "Queen" => 10 case "King" => 10 case _ => face.toInt } }
Note the value method, the returns the number of points that the card is worth. It has no parameter, so calling this method looks just like accessing a field:
scala> val c1 = new Card("Ace", "Diamonds") c1: Card = an Ace of Diamonds scala> val c2 = new Card("Jack", "Spades") c2: Card = a Jack of Spades scala> val c3 = new Card("8", "Hearts") c3: Card = an 8 of Hearts scala> val hand = Array(c1, c2, c3) hand: Array[Card] = Array(an Ace of Diamonds, a Jack of Spades, an 8 of Hearts) scala> for (c <- hand) | printf("Card %s has value %d\n", c, c.value) Card an Ace of Diamonds has value 11 Card a Jack of Spades has value 10 Card an 8 of Hearts has value 8
We next need a class Deck that stores an entire deck of cards, and allows us to draw cards from the deck (blackjack2.scala):
class Deck { private val cards = new Array[Card](52) private var count = 52 generateDeck() shuffleDeck() private def generateDeck() { var i = 0 for (suit <- Suits) { for (face <- Faces) { cards(i) = new Card(face, suit) i += 1 } } } private def shuffleDeck() { for (i <- 1 to 52) { // 0..i-2 already shuffled val j = (math.random * i).toInt val cj = cards(j) cards(j) = cards(i-1) cards(i-1) = cj } } def draw(): Card = { assert(count > 0) count -= 1 cards(count) } }
Note that Deck has no class parameter, so it is created simply by saying new Deck without any arguments. Let's look at the first four lines of the class more carefully:
private val cards = new Array[Card](52) private var count = 52 generateDeck() shuffleDeck()The first line defines a val-field named cards. This field is initialized with an array of length 52. The second line defines a var-field called count and sets it to 52, the number of cards initially in the deck.
Remember what new Array[Card](52) does:
scala> new Array[Card](52) res28: Array[Card] = Array(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
It creates an array with 52 empty slots! So before the Deck object can be used, the array has to be filled with the actual cards. This is done by the two method calls generateDeck() and shuffleDeck(). Both are private methods of Deck: That is, they are methods of Deck, but they cannot be called directly by client code. They are purely for internal use of the class (in our case for use in the constructor, to set up a properly shuffled deck of the 52 Blackjack cards). The first method generateDeck fills the array with the 52 cards, the second method shuffleDeck rearranges them into a random order.
Finally, the draw method is not private: It is meant for clients of the Deck to draw one card from the deck.
Here is an example of using the Deck class:
scala> val deck = new Deck deck: Deck = Deck@109636e6 scala> for (i <- 1 to 10) | println(deck.draw()) an 8 of Spades a King of Diamonds a 10 of Hearts a 4 of Spades an Ace of Diamonds a 9 of Diamonds an 8 of Clubs a 7 of Clubs a Queen of Diamonds a 6 of Diamonds
Now that we have a working deck, we can write the client code, that is, we can implement the complete Blackjack game. Here is the main function that implements one game (you can download the entire program as blackjack-game.scala). Note how easy it is to read this code, because we have hidden all the complexity of storing cards and shuffling and managing the deck inside the Card and Deck classes.
// Play one round of Blackjack // Returns 1 if player wins, -1 if dealer wins, and 0 for a tie. def blackjack(): Int = { val deck = new Deck() // initial cards var player = Array(deck.draw()) println("You are dealt " + player.head) var dealer = Array(deck.draw()) println("Dealer is dealt a hidden card") player = player :+ deck.draw() println("You are dealt " + player.last) dealer = dealer :+ deck.draw() println("Dealer is dealt " + dealer.last) printf("Your total is %d\n", handValue(player)) // player's turn to draw cards var want = true while (want && handValue(player) < 21) { want = askYesNo("Would you like another card? (y/n) ") if (want) { player = player :+ deck.draw() println("You are dealt " + player.last) printf("Your total is %d\n", handValue(player)) // if the player's score is over 21, the player loses immediately. if (handValue(player) > 21) { println("You went over 21! You lost!") return -1 } } } println("The dealer's hidden card was " + dealer.head) while (handValue(dealer) < 17) { dealer = dealer :+ deck.draw() println("Dealer is dealt " + dealer.last) } printf("The dealer's total is %d\n", handValue(dealer)) // summary val player_total = handValue(player) val dealer_total = handValue(dealer) printf("\nYour total is %d\n", player_total) printf("The dealer's total is %d\n", dealer_total) if (dealer_total > 21) { println("The dealer went over 21! You win!") 1 } else if (player_total > dealer_total) { println("You win!") 1 } else if (player_total < dealer_total) { println("You lost!") -1 } else { println("You have a tie!") 0 } }
Sometimes it is useful to have more than one constructor for objects. For instance, our Point class above requires us to provide the coordinates of a point. But sometimes we have no useful coordinates, we just want to create some point that can be filled in later. We would just like to say new Point to get a new Point object, with some default coordinates (both zero, for instance). With the definition above, this doesn't work:
scala> class Point(var x: Int, var y: Int) defined class Point scala> val p = new Point error: not enough arguments for constructor Point: (x: Int,y: Int)Point. Unspecified value parameters x, y.
The solution is to add an auxiliary constructor to the Point class. An auxiliary constructor is defined like a method with the special name this:
class Point(var x: Int, var y: Int) { def this() = { this(0, 0) } override def toString: String = "Point(" + x + "," + y + ")" }Note that the auxiliary constructor has no return type (since it is clear that it must create a Point object.
Inside an auxiliary constructor, we must call another constructor. Again, this looks like a method call with the special name this. Ultimately, any constructor will make use of the original constructor of the class, which is called its primary constructor.
We can now create points without arguments:
scala> var p = new Point p: Point = Point(0,0) scala> p.x = 7 scala> p res0: Point = Point(7,0)
Except to refer to the constructor, the keyword this has another use in Scala: Inside a method, this is always a name for the current object itself.
Classes and objects III |