1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Kỹ thuật lập trình >

2 Crafting an object: the behavior of a ticket

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (5.14 MB, 519 trang )


Crafting an object: the behavior of a ticket



39



def ticket.venue

"Town Hall"

end

def ticket.performer

"Mark Twain"

end

def ticket.event

"Author's reading"

end

def ticket.price

5.50

end



B



def ticket.seat

"Second Balcony, row J, seat 12"

end

def ticket.date

"01/02/03"

end



The majority of the methods defined here return string values. You can see this at a

glance: they hand back a value inside quotation marks. The price method B returns

a floating-point decimal number: 5.50.



The joy (or not) of floats

Floating-point numbers have more complexity and quirkiness than you may think.

Someday you’ll probably come across something similar to this frequently cited

example:

puts 0.5 - 0.4 - 0.1

-2.77555756156289e-17



Not zero!



The problem—or, more accurately, the inevitable consequence of the laws of mathematics and computers—is that decimal floating-point numbers of arbitrary length

can’t be stored and operated on in binary form with complete accuracy. Don’t be surprised if your “floats” don’t act as integer-like as you might wish or expect.



Now that the ticket object knows a little about itself, let’s ask it to share the information.



2.2.2



Querying the ticket object

Rather than produce a raw list of items, let’s generate a reader-friendly summary of

the details of the ticket. The use of print and puts can help get the information into

more or less narrative form:

print "This ticket is for: "

print ticket.event + ", at "

print ticket.venue + ", on "



Licensed to sam kaplan



40



CHAPTER 2



Objects, methods, and local variables



puts ticket.date + "."

print "The performer is "

puts ticket.performer + "."

print "The seat is "

print ticket.seat + ", "

print "and it costs $"

puts "%.2f." % ticket.price



Print floating-point number

to two decimal places



Save all the code, starting with ticket = Object.new, to a file called ticket.rb, and run

it. You’ll see the following:

This ticket is for: Author's reading, at Town Hall, on 01/02/03.

The performer is Mark Twain.

The seat is Second Balcony, row J, seat 12, and it costs $5.50.



The code for this example consists of a series of calls to the methods defined earlier:

ticket.event, ticket.venue, and so forth. The printing code embeds those calls—in

other words, embeds the return values of those methods (“Author’s reading”, “Town

Hall”, and so on)—in a succession of output commands, and adds connectors (“, at”,

“, on”, and so forth) to make the text read well and look nice.

The Twain ticket is a simple example, but it encompasses some vital Ruby procedures and principles. The most important lesson is that the knowledge necessary for

the program to do anything useful resides in the object. The ticket object has the knowledge; you tap into that knowledge by asking the ticket for it, via method calls. Nothing

is more central to Ruby programming than this. It’s all about asking objects to do

things and tell you things.

The ticket code works, and it embodies useful lessons; but it’s wordy. Ruby has a

reputation as a powerful, high-level language. You’re supposed to be able to get a lot

done with relatively little code. But the ticket example takes nine lines of print and

puts instructions to generate three lines of output.

Let’s improve that ratio a bit.



2.2.3



Shortening the ticket code via string interpolation

The goal of shortening the output of this little program gives us an excuse to dip into

one of the most useful programming techniques available in Ruby: string interpolation.

The string-interpolation operator gives you a way to drop variables, method-return values, or anything else into a string. This can save you a lot of back-and-forth between

print and puts.

Moreover, strings can be concatenated with the plus sign (+). Here’s how the printing code looks, using string interpolation to insert the values of expressions into the

string and using string “addition” to consolidate multiple puts calls into one:

puts "This ticket is for: #{ticket.event}, at #{ticket.venue}." +

"The performer is #{ticket.performer}." +

"The seat is #{ticket.seat}, " +

"and it costs $#{"%.2f." % ticket.price}"



Whatever’s inside the interpolation operator #{...} gets calculated separately, and

the results of the calculation are inserted into the string. When you run these lines,



Licensed to sam kaplan



Crafting an object: the behavior of a ticket



41



you won’t see the #{...} operator on your screen; instead, you’ll see the results of calculating or evaluating what was between the curly braces. Interpolation helped eliminate six of nine lines of code and also made the code look a lot more like the eventual

format of the output, rather than something that works but doesn’t convey much

visual information.

So far, we’ve been asking the ticket for information in the form of strings and numbers. Tickets also have some true/false—boolean—information about themselves.



2.2.4



Ticket availability: expressing boolean state in a method

By way of boolean information, consider the matter of whether a ticket has been sold

or is still available. One way to endow a ticket with knowledge of its own availability status is this:

def ticket.availability_status

"sold"

end



Another way is to ask the ticket whether it’s available and have it report back true or

false:

def ticket.available?

false

end



false is a special term in Ruby, as is the term true. true and false are objects.



Ruby uses them to represent results of, among other things, comparison operations

(like x > y), and you can use them to represent truth and falsehood in your own

methods. You may have noticed that the method name available? ends with a question mark. Ruby lets you do this so you can write methods that evaluate to true or

false and make the method calls look like questions:

if ticket.available?

puts "You're in luck!"

else

puts "Sorry--that seat has been sold."

end



But there’s more to truth and falsehood than the true and false objects. Every

expression in Ruby evaluates to an object; and every object in Ruby has a truth value.

The truth value of almost every object in Ruby is true. The only objects whose truth

value (or boolean value) is false are the objects false and the special non-entity

object nil. You’ll see booleans and nil in more detail in chapter 7. For the moment,

you can think of both false and nil as functionally equivalent indicators of a negative

test outcome.

Playing around with if expressions in irb is a good way to get a feel for how conditional logic plays out in Ruby. Try some examples like these:

>> if "abc"

>> puts "Strings are 'true' in Ruby!"

>> end



Licensed to sam kaplan



42



CHAPTER 2



Objects, methods, and local variables



Strings are 'true' in Ruby!

=> nil

>> if 123

>> puts "So are numbers!"

>> end

So are numbers!

=> nil

>> if 0

>> puts "Even 0 is true, which it isn't in some languages."

>> end

Even 0 is true, which it isn't in some languages.

=> nil

>> if 1 == 2

>> puts "One doesn't equal two, so this won't appear."

>> end

=> nil



(The indentation in this irb session isn’t mandatory, but indenting the bodies of if

statements, as you would in a program file, makes even an irb transcript easier to follow.)

Notice how irb not only obeys the puts method calls (when conditions are right)

but also, on its own initiative, outputs the value of the entire expression. In the cases

where the puts happens, the whole expression evaluates to nil—because the return

value of puts is always nil. In the last case, where the string isn’t printed (because the

condition fails), the value of the expression is also nil—because an if statement that

fails (and has no else branch to salvage it) also evaluates to nil.

Remembering that nil has a boolean value of false, you can, if you wish, get into

acrobatics with irb. A call to puts returns nil and is therefore false, even though the

string gets printed. If you put puts in an if clause, the clause will be false. But it will

still be evaluated. So…

>> if puts "You'll see this"; puts "but not this"; end

You'll see this

=> nil



The first puts is executed, but the value it returns, namely nil, isn’t true in the boolean sense—so the second puts isn’t executed.

This is, to use the popular phrase, a contrived example. But it’s a good idea to get

used to the fact that everything in Ruby has a boolean value, and sometimes it’s not

what you might expect. As is often the case, irb can be a great help in getting a handle

on this concept.

Now that the ticket object has some hand-crafted behaviors, let’s circle back

and consider the matter of what behaviors every object in Ruby is endowed with at

its creation.



2.3



The innate behaviors of an object

Even a newly created object isn’t a blank slate. As soon as an object comes into existence, it responds to a number of messages. Every object is “born” with certain innate

abilities.



Licensed to sam kaplan



The innate behaviors of an object



43



To see a list of innate methods, you can call the methods method (and throw in a

sort operation, to make it easier to browse visually):

p Object.new.methods.sort



The result is a list of all the messages (methods) this newly minted object comes bundled with. (Warning: the output looks cluttered. This is how Ruby displays arrays—

and the methods method gives you an array of method names. If you want a list of the

methods one per line, use puts instead of p in the command.)

[:!, :!=, :!~, :==, :===, :=~, :__id__, :__send__, :class, :clone,

:define_singleton_method, :display, :dup, :enum_for, :eql?, :equal?,

:extend, :freeze, :frozen?, :gem, :hash, :inspect, :instance_eval,

:instance_exec, :instance_of?, :instance_variable_defined?,

:instance_variable_get, :instance_variable_set, :instance_variables,

:is_a?, :kind_of?, :method, :methods, :nil?, :object_id,

:private_methods, :protected_methods, :public_method, :public_methods,

:public_send, :respond_to?, :send, :singleton_methods, :taint,

:tainted?, :tap, :to_enum, :to_s, :trust, :untaint, :untrust,

:untrusted?]



Don’t worry if most of these methods make no sense to you right now. You can try

them in irb, if you’re curious to see what they do (and if you’re not afraid of getting

some error messages).

But a few of these innate methods are common enough—and helpful enough,

even in the early phases of acquaintance with Ruby—that we’ll look at them in detail

here. The following methods fit this description:









object_id

respond_to?

send (synonym: __send__)



Adding these to your Ruby toolbox won’t be amiss, on account of what they do and

because they serve as examples of innate methods.



Generic objects vs. basic objects

Asking Ruby to create a new object for you, with the Object.new command, produces

what we’re calling here, informally, a generic object. Ruby also has basic objects—and

that’s a more formal name. If you call BasicObject.new, you get a kind of proto-object that can do very little. You can’t even ask a basic object to show you its methods,

because it has no methods method! In fact, it has only seven methods—enough for

the object to exist and be identifiable, and not much more. You’ll learn more about

these basic objects in chapters 3 and 13.



2.3.1



Identifying objects uniquely with the object_id method

Every object in Ruby has a unique id number associated with it. You can see an object’s

id by asking the object to show you its object_id, using this or similar code:



Licensed to sam kaplan



44



CHAPTER 2



Objects, methods, and local variables



obj = Object.new

puts "The id of obj is #{obj.object_id}."

str = "Strings are objects too, and this is a string!"

puts "The id of the string object str is #{str.object_id}."

puts "And the id of the integer 100 is #{100.object_id}."



Having a unique id number for every object can come in handy when you’re trying to

determine whether two objects are the same as each other. How can two objects be the

same? Well, the integer object 100 is the same as … the integer object 100. (Ask 100

for its object id twice; the result will be the same.) And here’s another case:

a = Object.new

b = a

puts "a's id is #{a.object_id} and b's id is #{b.object_id}."



Even though the variables a and b are different, the object they both refer to is the

same. (See section 2.6.1 for more on the concept of object references.) The opposite

scenario can happen too: sometimes two objects appear to be the same, but they’re

not. This happens a lot with strings. Consider the following example:

string_1 = "Hello"

string_2 = "Hello"

puts "string_1's id is #{string_1.object_id}."

puts "string_2's id is #{string_2.object_id}."



string_1 id: 287090

string_2 id: 279110



Even though these two strings contain the same text, they aren’t, technically, the same

object. If you printed them out, you’d see the same result both times (“Hello”). But

the string objects themselves are different. It’s like having two copies of the same

book: they contain the same text, but they aren’t the same thing as each other. You

could destroy one, and the other would be unaffected.

ID NUMBERS AND EQUALITY OF OBJECTS



As in the case of human institutions, one of the points of giving objects id numbers in

Ruby is to be able to make unique identifications—and, in particular, to be able to

determine when two objects are the same object.

Ruby provides a variety of ways to compare objects for different types of equality. If

you have two strings, you can test to see whether they contain the same characters. You

can also test to see whether they’re the same object (which, as you’ve just seen, isn’t

necessarily the case, even if they contain the same characters). The same holds true,

with slight variations, for other objects and other types of objects.

Comparing id numbers for equality is just one way of measuring object equality.

We’ll get into more detail about these comparisons a little later. Right now, we’ll turn

to the next innate method on our list: respond_to?.



2.3.2



Querying an object’s abilities with the respond_to? method

Ruby objects respond to messages. At different times during a program run, depending on the object and what sorts of methods have been defined for it, an object may

or may not respond to a given message. For example, the following code results in

an error:



Licensed to sam kaplan



The innate behaviors of an object



45



obj = Object.new

obj.talk



Ruby is only too glad to notify you of the problem:

NoMethodError: undefined method 'talk' for #



You can determine in advance (before you ask the object to do something) whether the

object knows how to handle the message you want to send it, by using the respond_to?

method. This method exists for all objects; you can ask any object whether it responds

to any message.

respond_to? usually appears in connection with conditional (if) logic:

obj = Object.new

if obj.respond_to?("talk")

obj.talk

else

puts "Sorry, the object doesn't understand the 'talk' message."

end



respond_to? is an example of introspection or reflection, two terms that refer to examin-



ing the state of a program while it’s running. Ruby offers a number of facilities for

introspection. Examining an object’s methods with the methods method, as we did

earlier, is another introspective or reflective technique. (You’ll see many more such

techniques in part 3 of the book.)

Up to now, we’ve been using the dot operator (.) to send messages to objects.

Nothing wrong with that. But what if you don’t know which message you want to send?



2.3.3



Sending messages to objects with the send method

Suppose you want to let a user get information from the ticket object by entering an

appropriate query term (venue, performer, and so on) at the keyboard. Here’s what

you’d add to the existing program:

print "Information desired: "

request = gets.chomp



The second line of code gets a line of keyboard input, “chomps” off the trailing newline character, and saves the resulting string in the variable request.

At this point, you could test the input for one value after another by using the double equal sign comparison operator (==), which compares strings based on their content, and calling the method whose value provides a match:

if request == "venue"

puts ticket.venue

elsif request == "performer"

puts ticket.performer

...



To be thorough, though, you’d have to continue through the whole list of ticket properties. That’s going to get lengthy.



Licensed to sam kaplan



46



CHAPTER 2



Objects, methods, and local variables



There’s an alternative: you can send the word directly to the ticket object. Instead

of the previous code, you’d do the following:



B



if ticket.respond_to?(request)

puts ticket.send(request)

else

puts "No such information available"

end



This version uses the send method as an all-purpose way of getting a message to the

ticket object. It relieves you of having to march through the whole list of possible

requests. Instead, having checked that the ticket object knows what to do B, you

hand the ticket the message and let it do its thing.



Using __send__ or public_send instead of send

Sending is a broad concept: email is sent, data gets sent to I/O sockets, and so

forth. It’s not uncommon for programs to define a method called send that conflicts

with Ruby’s built-in send method. Therefore, Ruby gives you an alternative way to call

send: __send__. By convention, no one ever writes a method with that name, so the

built-in Ruby version is always available and never comes into conflict with newly written methods. It looks strange, but it’s safer than the plain send version from the

point of view of method-name clashes.

In addition, there’s a safe—but in a different way—version of send (or __send__)

called public_send. The difference between plain send and public_send is that

plain send can call an object’s private methods, and public_send can’t. We don’t

cover private methods until later in the book, but in case you’re curious what

public_send was doing in the method list, that’s the gist.



Most of the time, you’ll use the dot operator to send messages to objects. But the send

alternative can be useful and powerful. It’s powerful enough, and error-prone

enough, that you normally wouldn’t use it to send messages to objects with no safety

checks or error handling, as in this example; but the example demonstrates the basic

mechanism of the explicit send operation.

Next, we’ll put method argument syntax and semantics under the microscope.



2.4



A close look at method arguments

Methods you write in Ruby can take more than one argument, or none at all. They

can also allow a variable number of arguments. We’ll look at several permutations

here, and you can see them summarized in table 2.2 at the end of this section.

We’ll start examining argument semantics by looking at the difference between

required and optional arguments. We’ll also look at how to assign default values to

arguments, and the rules governing the order in which you have to arrange the

parameters in the method signature so that Ruby can make sense of argument lists in

method calls and bind the parameters correctly.



Licensed to sam kaplan



A close look at method arguments



2.4.1



47



Required and optional arguments

When you call a Ruby method, you have to supply the correct number of arguments.

If you don’t, Ruby tells you there’s a problem. For example, calling a one-argument

method with three arguments

def obj.one_arg(x)

puts "I require one and only one argument!"

end

obj.one_arg(1,2,3)



results in

ArgumentError: wrong number of arguments (3 for 1)



It’s possible to write a method that allows any number of arguments. To do this, you

put a star (an asterisk: *) in front of a single argument name:

def obj.multi_args(*x)

puts "I can take zero or more arguments!"

end



The *x notation means that when you call the method, you can supply any number of

arguments (or none). In this case, the variable x is assigned an array of values corresponding to whatever arguments were sent. You can then examine the values one at a

time by traversing the array. (We’ll look more closely at arrays in chapter 9.)

You can fine-tune the number of arguments by mixing required and optional arguments:

def two_or_more(a,b,*c)

puts "I require two or more arguments!"

puts "And sure enough, I got: "

p a, b, c

end



In this example, a and b are required arguments. The final *c will sponge up any

other arguments that you may send and put them into an array in the variable c. If you

call two_or_more(1,2,3,4,5), you’ll get the following report of what got assigned to

a, b, and c:

I require two or more arguments!

And sure enough, I got:

1

2

[3, 4, 5]



(Using p rather than print or puts results in the array being printed out in array

notation. Otherwise, each array element would appear on a separate line, making it

harder to see that an array is involved at all.)

You can also make an argument optional by giving it a default value.



Licensed to sam kaplan



48



2.4.2



CHAPTER 2



Objects, methods, and local variables



Default values for arguments

When you supply a default value for an argument, the result is that if that argument

isn’t supplied, the variable corresponding to the argument receives the default value.

Default arguments are indicated with an equal sign and a value. Here’s an example:

def default_args(a,b,c=1)

puts "Values of variables: ",a,b,c

end



If you make a call this like this

default_args(3,2)



you’ll see this result:

Values of variables:

3

2

1



No value was supplied in the method call for c, so c was set to the default value provided for it in the parameter list: 1. If you do supply a third argument, that value overrides the default assignment of 1. The following call

default_args(4,5,6)



produces this result:

Values of variables:

4

5

6



The real fun starts when you mix and match the different elements of argument syntax and have to figure out what order to put everything in.



2.4.3



Order of parameters and arguments

What output would you expect from this code snippet?

def mixed_args(a,b,*c,d)

puts "Arguments:"

puts a,b,c,d

end

mixed_args(1,2,3,4,5)



You’ve seen that a starred parameter, like *c, sponges up the remaining arguments—

at least, it did so in the method two_or_more, where *c occurred last in the parameter

list. What happens when another argument follows it?

Basically, Ruby tries to assign values to as many variables as possible. And the

sponge parameters get the lowest priority: if the method runs out of arguments after

it’s performed the assignments of required arguments, then a catch-all parameter like

*c ends up as an empty array. The required arguments both before *c and after *c get

taken care of before *c does.



Licensed to sam kaplan



A close look at method arguments



49



The output of the previous snippet is this:

Arguments:

1

2

[3, 4]

5



The parameters a and b get the first two arguments, 1 and 2. Because the parameter at

the end of the list, d, represents a required argument, it grabs the first available value

from the right-hand end of the argument list—namely, 5. Whatever’s left in the middle (3, 4) gets sponged up by c.

If you only give enough arguments to match the required arguments of the

method, then the sponge array will be empty. This method call

mixed_args(1,2,3)



results in this output:

1

2

[]

3



In this example, c is out of luck; there’s nothing left.

You can get reasonably fancy with parameter syntax. Here’s a method that takes a

required argument; an optional argument that defaults to 1; two more required arguments taken from the right; and, somewhere in the middle, everything else:

def args_unleashed(a,b=1,*c,d,e)

puts "Arguments:"

p a,b,c,d,e

end



And here’s an irb session that puts this method through its paces. Note that the return

value of the method call, in every case, is an array consisting of all the values. That’s

the return value of the call to p. It’s an array representation of the same values that

you see printed out as individual values on separate lines:

>> args_unleashed(1,2,3,4,5)

1

2

[3]

4

5

=> [1, 2, [3], 4, 5]

>> args_unleashed(1,2,3,4)

1

2

[]

3

4

=> [1, 2, [], 3, 4]

>> args_unleashed(1,2,3)



Licensed to sam kaplan



Xem Thêm
Tải bản đầy đủ (.pdf) (519 trang)

×