Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 58 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
58
Dung lượng
281,17 KB
Nội dung
Using arrays 291 The code starts by grabbing a Work object and getting from it the full list of its editions #1. The editions collection reports its class as Array #2. However, the collection of editions refuses to accept a string as an element: When you try to push a string onto the collection, you get a fatal error #3. This is a good illustration of the fact that a Ruby object (in this case, a collec- tion of editions) isn’t constrained to behave exactly the way a default or vanilla instance of its class would behave. For Ruby objects, including objects that house other objects, being created is just the beginning. What matters is how the object gets shaped and used down the road. ActiveRecord collections consider them- selves instances of Array , but they have special knowledge and behaviors that dif- ferentiate them from arrays in general. This is a great example of the Ruby philosophy bearing fruit with practical results. Searching and filtering, ActiveRecord-style ActiveRecord’s approach to finding elements in collections is also instructive. At a general level, you can perform find operations on the entire existing set of records for any model you’ve defined. Here’s an example: Work.find(:all) Work.find_by_title("Sonata") You’re operating at the class (and class method) level: You’re looking for all existing objects (corresponding to database records, under the hood) of the given class. A couple of points are noteworthy here. First, ActiveRecord uses find(:all) rather than find_all . (Actually, either will work, but find_all is considered old- style usage and is likely to disappear from future versions of ActiveRecord.) Sec- ond, note the call to the method find_by_title . That method is created automatically, because instances of Work have title attributes. This is another example of the Rails framework giving you a good return on your investment: In return for creating a database field called title, you get a method that lets you search specifically on that field. find(:all) and its close relative find(:first) can both be supplied with condi- tions, which filter the results for you. These conditions are written as SQL frag- ments, using the kind of expression you use in an SQL query to narrow a SELECT operation. For example, to find all works whose titles start with the word The (The Rite of Spring, The Lark Ascending, and so on), you can do this: Work.find(:all, :conditions => "title like 'The %'") B C D 292 CHAPTER 11 Collections, containers, and enumerability To find only the first such work, use this: Work.find(:first, :conditions => "title like 'The %'") It’s always possible to accomplish this kind of find operation without SQL, through the use of pure Ruby array operations: Work.find(:all).select {|work| /^The /.match(work.title) } However, this approach is less efficient and almost certainly slower than the SQL- fragment approach, because it involves creating an array of all existing works and then filtering that array. Providing an explicit SQL fragment allows an optimiza- tion: The database engine can do the sifting and searching, presumably in a more efficient way. On the other hand, sometimes you need the ability to program a selection algorithm using Ruby’s resources—or you don’t mind a small slowdown in exchange for having the code be entirely in Ruby. You have to decide, based on each case, which approach is best for this kind of operation. What you see here is the creation of a parallel universe of collection searching and filtering—parallel but not identical to the facilities provided for Ruby arrays. The syntax is different from plain Ruby syntax, but it meshes with Rails style and with the specific searching needs of ActiveRecord models. Like arrays, hashes have popped up here and there in our discussions. Now, we’ll look at them in detail. 11.3 Hashes Like an array, a hash is a collection of objects. Unlike an array, a hash is an unordered collection: There is no such thing as the first or last or third-from-last item in a hash. Instead, a hash consists of key-value pairs. Hashes let you perform lookup operations based on keys. A typical use of a hash is to store complete strings along with their abbrevia- tions. Here’s a hash containing a selection of names and two-letter state abbrevia- tions, along with some code that exercises it. (The => operator connects a key on the left with the value corresponding to it on the right.) state_hash = { "Connecticut" => "CT", "Delaware" => "DE", "New Jersey" => "NJ", "Virginia" => "VA" } print "Enter the name of a state: " state = gets.chomp abbr = state_hash[state] puts "The abbreviation is #{abbr}." Hashes 293 When you run this snippet (assuming you enter one of the states defined in the hash), you see the abbreviation. This example involves creating a hash, using hash literal syntax, and assigning it to a variable. Let’s back-and-fill by looking in detail at how hashes are created. 11.3.1 Creating a new hash There are three ways to create a hash. One is by means of the literal hash con- structor, curly braces ( {} ); this is what we did in the last example. The literal hash constructor is convenient when you have values you wish to hash that aren’t going to change; you’re going to type them into the program file once and refer to them from the program. State abbreviations are a good example. You can also create an empty hash with the literal constructor: h = {} You’d presumably want to add items to the empty hash at some point; techniques for doing so will be forthcoming in section 11.3.2. The second way to create a hash is with the traditional new constructor: Hash.new This always creates an empty hash. However, if you provide an argument to Hash.new , it’s treated as the default value for nonexistent hash keys. (We’ll return to this point after looking at key/value insertion and retrieval.) The third way to create a hash involves another class method of the Hash class: the method [] (square brackets). You can put key-value pairs inside the square brackets, if you want to create your hash already populated with data: Hash["Connecticut" => "CT", "Delaware" => "DE" ] A word about => is in order. Separating keys from values in hashes When you physically type in a key/value pair for a hash (as opposed to setting key/value pairs through a method call, as you’ll learn to do shortly), you can sepa- rate the key from the value with either a comma or the special hash separator => (equal-greater than). The => separator makes for a more readable hash, especially when the hash includes a lot of entries, but either will work. After each complete key-value pair, you insert a comma. Look again at the state-name example, and you’ll see how this syntax works. Now, let’s turn to matter of manipulating a hash’s contents. 294 CHAPTER 11 Collections, containers, and enumerability 11.3.2 Inserting, retrieving, and removing hash pairs As you’ll see as we proceed, hashes have a lot in common with arrays, when it comes to the get- and set-style operations. However, there are differences, stem- ming from the underlying differences between arrays (ordered collections, indexed by number) and hashes (unordered collections, indexed by arbitrary key objects). As long as you keep this in mind, the behavior of hashes and the behav- ior of arrays mesh quite well. Adding a key/value pair to a hash To add a key/value pair to a hash, you use essentially the same technique as for adding an item to an array: the []= method, plus syntactic sugar. To add a state to state_hash , you do this state_hash["New York"] = "NY" which is the sugared version of this: state_hash.[]=("New York", "NY") You can also use the synonymous method store for this operation. store takes two arguments (a key and a value): state_hash.store("New York", "NY") When you’re adding to a hash, keep in mind the important principle that keys are unique. You can have only one entry with a given key. If you add a key-value pair to a hash that already has an entry for the key you’re adding, the old entry is over- written. Here’s an example: h = Hash.new h["a"] = 1 h["a"] = 2 puts h["a"] This code assigns two values to the a key of the hash h . The second assignment clobbers the first, as the puts statement shows by outputting 2. Note that hash values don’t have to be unique; you can have two keys that are paired with the same value. But you can’t have duplicate keys. Retrieving values from a hash You retrieve values from a hash with the [] method, plus the usual syntactic sugar involved with [] (no dot; the argument goes inside the brackets). For example, to get the Connecticut abbreviation from state_hash , you do this: conn_abbrev = state_hash["Connecticut"] Hashes 295 Now conn_abbrev has “CT” assigned to it. Using a hash key is much like indexing an array—but the index (the key) can be anything, whereas in an array it’s always an integer. Hashes also have a fetch method, which gives you an alternative way of retriev- ing values by key: conn_abbrev = state_hash.fetch("Connecticut") fetch differs from [] in the way it behaves when you ask it to look up a nonexist- ent key: fetch raises an exception, while [] gives you either nil or a default you’ve specified (as discussed below). You can also retrieve values for multiple keys in one operation, with values_at : two_states = state_hash.values_at("New Jersey","Delaware") This code returns an array consisting of ["NJ","DE"] and assigns it to the variable two_states . Now that you have a sense of the mechanics of getting information into and out of a hash, let’s circle back and look at the matter of supplying a default value (or default code block) when you create a hash. Specifying and getting a default value By default, when you ask a hash for the value corresponding to a nonexistent key, you get nil : >> h = Hash.new => {} >> h["no such key!"] => nil However, you can specify a different default value by supplying an argument to Hash.new : >> h = Hash.new(0) => {} >> h["no such key!"] => 0 Here, we get back the hash’s default value, 0, when we use a nonexistent key. (You can also set the default on an already existing hash, with the default method.) It’s important to remember that whatever you specify as the default value is what you get when you specify a nonexistent key. This does not mean the key is set to that value. The key is still nonexistent. If you want a key in a hash, you have to put it there. You can, however, do this as part of a default scenario for new (non- existent) keys—by supplying a default code block to Hash.new . The code block will 296 CHAPTER 11 Collections, containers, and enumerability be executed every time a nonexistent key is referenced. Furthermore, two objects will be yielded to the block: the hash and the (nonexistent) key. This technique gives you a foot in the door when it comes to setting keys auto- matically when they’re first used. It’s not the most elegant or streamlined tech- nique in all of Ruby, but it does work. You write a block that grabs the hash and the key, and you do a set operation. For example, if you want every nonexistent key to be added to the hash with a value of 0, you create your hash like this: h = Hash.new {|hash,key| hash[key] = 0 } When the hash h is asked to match a key it doesn’t have, that key is added after all, with the value 0. Given this assignment of a new hash to h , you can trigger the block like this: >> h["new key!"] => 0 >> h => {"new key!"=>0} When you try to look up the key new key #1, it’s not there; it’s added, with the value 0, and then that value is printed out by irb. Next, when you ask irb to show you the whole hash #2, it contains the automatically added pair. This technique has lots of uses. It lets you make assumptions about what’s in a hash, even if nothing is there to start with. It also shows you another facet of Ruby’s extensive repertoire of dynamic programming techniques, and the flexibil- ity of hashes. We’ll turn now to ways you can combine hashes with each other, as we did with strings and arrays. 11.3.3 Combining hashes with other hashes The process of combining two hashes into one comes in two flavors: the destruc- tive flavor, where the first hash has the key/value pairs from the second hash added to it directly; and the nondestructive flavor, where a new, third hash is cre- ated that combines the elements of the original two. The destructive operation is performed with the update method. Entries in the first hash are overwritten permanently if the second hash has a corresponding key: h1 = {"Smith" => "John", "Jones" => "Jane" } h2 = {"Smith" => "Jim" } h1.update(h2) puts h1["Smith"] B C Output: Jim B C Hashes 297 In this example, h1 ’s Smith entry has been changed (updated) to the value it has in h2 . You’re asking for a refresh of your hash, to reflect the contents of the sec- ond hash. That’s the destructive version of combining hashes. To perform nondestructive combining of two hashes, you use the merge method, which gives you a third hash and leaves the original unchanged: h1 = {"Smith" => "John", "Jones" => "Jane" } h2 = {"Smith" => "Jim" } h3 = h1.merge(h2) p h1["Smith"] Here, h1 ’s Smith/John pair isn’t overwritten by h2 ’s Smith/Jim pair. Instead, a new hash is created, with pairs from both of the other two. Note that h3 has a decision to make: Which of the two Smith entries should it contain? The answer is that when the two hashes being merged share a key, the second hash ( h2 , in this example) wins. h3 ’s value for the key Smith will be Jim. (Incidentally, merge! —the bang version of merge —is a synonym for update . You can use either name when you want to perform that operation.) In addition to being combined with other hashes, hashes can also be trans- formed in a number of ways, as you’ll see next. 11.3.4 Hash transformations You can perform several transformations on hashes. Transformation, in this case, means that the method is called on a hash, and the result of the operation (the method’s return value) is a hash. The term filtering, in the next subsection, refers to operations where the hash undergoes entry-by-entry processing and the results are stored in an array. (Remember that arrays are the most common, general- purpose collection objects in Ruby; they serve as containers for results of opera- tions that don’t even involve arrays.) Inverting a hash Hash#invert flips the keys and the values. Values become keys, and keys become values: >> h = { 1 => "one", 2 => "two" } => {1=>"one", 2=>"two"} >> h.invert => {"two"=>2, "one"=>1} Be careful when you invert hashes. Because hash keys are unique, but values aren’t, when you turn duplicate values into keys, one of the pairs will be discarded: Output: John 298 CHAPTER 11 Collections, containers, and enumerability >> h = { 1 => "one", 2 => "more than 1", 3 => "more than 1" } => {1=>"one", 2=>"more than 1", 3=>"more than 1"} >> h.invert => {"one"=>1, "more than 1"=>3} Only one of the two more than 1 values can survive as a key when the inversion is performed; the other is discarded. You should invert a hash only when you’re cer- tain the values as well as the keys are unique. Clearing a hash Hash#clear empties the hash: >> {1 => "one", 2 => "two" }.clear => {} This is an in-place operation: The empty hash is the same hash (the same object) as the one to which you send the clear message. Replacing the contents of a hash Hashes have a replace method: >> { 1 => "one", 2 => "two" }.replace({ 10 => "ten", 20 => "twenty"}) => {10 => "ten", 20 => "twenty"} This is also an in-place operation, as the name replace implies. 11.3.5 Hash iteration, filtering, and querying You can iterate over a hash several ways. Like arrays, hashes have a basic each method. On each iteration, an entire key/value pair is yielded to the block, in the form of a two-element array: {1 => "one", 2 => "two" }.each do |key,value| puts "The word for #{key} is #{value}." end The output of this snippet is The word for 1 is one. The word for 2 is two. Each time through the block, the variables key and value are assigned the key and value from the current pair. The return value of Hash#each is the hash—the receiver of the “each” message. Hashes 299 Iterating through all the keys or values You can also iterate through the keys or the values on their own—and you can do each of those things in one of two ways. You can grab all the keys or all the values of the hash, in the form of an array, and then do whatever you choose with that array: >> h = {1 => "one", 2 => "two" } => {1=>"one", 2=>"two"} >> h.keys => [1, 2] >> h.values => ["one", "two"] Or, you can iterate directly through either the keys or the values, as in this example: h = {"apple" => "red", "banana" => "yellow", "orange" => "orange" } h.each_key {|k| puts "The next key is #{key}." } h.each_value {|v| puts "The next value is #{value}." } The second approach (the each_key_or_value methods) saves memory by not accumulating all the keys or values in an array before iteration begins. Instead, it looks at one key or value at a time. The difference is unlikely to loom large unless you have a very big hash, but it’s worth knowing about. Let’s look now at filtering methods: methods you call on a hash, but whose return value is an array. Hash filtering operations Arrays don’t have key/value pairs; so when you filter a hash into an array, you end up with an array of two-element arrays: Each subarray corresponds to one key/ value pair. You can see this by calling find_all or select (the two method names are synonymous) on a hash. Like the analogous array operation, selecting from a hash involves supplying a code block containing a test. Any key/value pair that passes the test is added to the result; any that doesn’t, isn’t: >> { 1 => "one", 2 => "two", 3 => "three" }.select {|k,v| k > 1 } => [[2, "two"], [3, "three"]] Here, the select operation accepts only those key/value pairs whose keys are greater than 1. Each such pair (of which there are two in the hash) ends up as a two-element array inside the final returned array. Even with the simpler find method (which returns either one element or nil ), you get back a two-element array when the test succeeds: >> {1 => "un", 2 => "deux", 3 => "trois" }.find {|k,v| k == 3 } => [3, "trois"] 300 CHAPTER 11 Collections, containers, and enumerability The test succeeds when it hits the 3 key. That key is returned, with its value, in an array. You can also do a map operation on a hash. Like its array counterpart, Hash#map goes through the whole collection—one pair at a time, in this case—and yields each element (each pair) to the code block. The return value of the whole map operation is an array whose elements are all the results of all these yieldings. Here’s an example that launders each pair through a block that returns an uppercase version of the value: >> { 1 => "one", 2 => "two", 3 => "three" }.map {|k,v| v.upcase } => ["ONE", "TWO", "THREE"] The return array reflects an accumulation of the results of all three iterations through the block. We’ll turn next to hash query methods. Hash query methods Table 11.2 shows some common hash query methods. None of the methods in table 11.2 should offer any surprises at this point; they’re similar in spirit, and in some cases in letter, to those you’ve seen for arrays. With the exception of size , they all return either true or false . The only surprise may be how many of them are synonyms. Four methods test for the presence of a par- ticular key: has_key? , include? , key? , and member? . A case could be made that this is two or even three synonyms too many. has_key? seems to be the most popular of the four and is the most to-the-point with respect to what the method tests for. Table 11.2 Common hash query methods and their meanings Method name/sample call Meaning h.has_key?(1) True if h has the key 1 h.include?(1) Synonym for has_key? h.key?(1) Synonym for has_key? h.member?(1) Another (!) synonym for has_key? h.has_value?("three") True if any value in h is "three" h.value?("three") Synonym for has_value? h.empty? True if h has no key/value pairs h.size Number of key/value pairs in h [...]... expressions in your Rails applications Becoming a regex wizard isn’t a prerequisite for Rails programming However, regular expressions are often important in converting data from one format to another, and they often loom large in Rails- related activities like salvaging legacy data As the Rails framework gains in popularity, there are likely to be more and more cases where data in an old format (or a text-dump... of methods for searching, querying, and sorting Enumerable is the foundational Ruby tool for collection manipulation The chapter also looked at some special behaviors of ActiveRecord collections, specialized collection objects that use Ruby array behavior as a point of departure but don’t restrict themselves to array functionality These objects provide an enlightening example of the use of Ruby fundamentals... as their underlying premise may be, hashes are a powerful data structure Among other uses, you’ll see them a lot in method calls Ruby makes special allowances for hashes in argument lists, and Rails takes full advantage of them, as you’ll see next 11.3.6 Hashes in Ruby and Rails method calls In the previous chapter, you saw this example of the use of symbols as part of a method argument list: . hashes, hashes can also be trans- formed in a number of ways, as you’ll see next. 11.3.4 Hash transformations You can perform several transformations on hashes. Transformation, in this case, means. a lot in method calls. Ruby makes special allowances for hashes in argument lists, and Rails takes full advantage of them, as you’ll see next. 11.3.6 Hashes in Ruby and Rails method calls In. algorithm using Ruby s resources—or you don’t mind a small slowdown in exchange for having the code be entirely in Ruby. You have to decide, based on each case, which approach is best for this kind