# 4.3 其他數據類型
雖然 Web 應用最終都是處理字符串,但也需要其他的數據類型來生成字符串。本節介紹一些對開發 Rails 應用很重要的其他 Ruby 數據類型。
## 4.3.1 數組和值域
數組是一組具有特定順序的元素。前面還沒用過數組,不過理解數組對理解哈希有很大幫助([4.3.3 節](#hashes-and-symbols)),也有助于理解 Rails 中的數據模型(例如 [2.3.3 節](chapter2.html#a-user-has-many-microposts)用到的 `has_many` 關聯,[11.1.3 節](chapter11.html#user-micropost-associations)會做詳細介紹)。
目前,我們已經花了很多時間理解字符串,從字符串過渡到數組可以從 `split` 方法開始:
```
>> "foo bar baz".split # 把字符串拆分成有三個元素的數組
=> ["foo", "bar", "baz"]
```
上述操作得到的結果是一個有三個字符串的數組。默認情況下,`split` 在空格處把字符串拆分成數組,不過也可以在幾乎任何地方拆分:
```
>> "fooxbarxbazx".split('x')
=> ["foo", "bar", "baz"]
```
和大多數編程語言的習慣一樣,Ruby 數組的索引也從零開始,因此數組中第一個元素的索引是 0,第二個元素的索引是 1,依此類推:
```
>> a = [42, 8, 17]
=> [42, 8, 17]
>> a[0] # Ruby 使用方括號獲取數組元素
=> 42
>> a[1]
=> 8
>> a[2]
=> 17
>> a[-1] # 索引還可以是負數
=> 17
```
我們看到,Ruby 使用方括號獲取數組中的元素。除了方括號之外,Ruby 還為一些經常需要獲取的元素提供了別名:[[8](#fn-8)]
```
>> a # 只是為了看一下 a 的值是什么
=> [42, 8, 17]
>> a.first
=> 42
>> a.second
=> 8
>> a.last
=> 17
>> a.last == a[-1] # 用 == 符號對比
=> true
```
最后一行用到了相等比較操作符 `==`,Ruby 和其他語言一樣還提供了 `!=`(不等)等其他操作符:
```
>> x = a.length # 和字符串一樣,數組也可以響應 length 方法
=> 3
>> x == 3
=> true
>> x == 1
=> false
>> x != 1
=> true
>> x >= 1
=> true
>> x < 1
=> false
```
除了 `length`(上述代碼的第一行)之外,數組還可以響應一系列其他方法:
```
>> a
=> [42, 8, 17]
>> a.empty?
=> false
>> a.include?(42)
=> true
>> a.sort
=> [8, 17, 42]
>> a.reverse
=> [17, 8, 42]
>> a.shuffle
=> [17, 42, 8]
>> a
=> [42, 8, 17]
```
注意,上面的方法都沒有修改 `a` 的值。如果想修改數組的值,要使用相應的“炸彈”(bang)方法(之所以這么叫是因為,這里的感嘆號經常都讀作“bang”):
```
>> a
=> [42, 8, 17]
>> a.sort!
=> [8, 17, 42]
>> a
=> [8, 17, 42]
```
還可以使用 `push` 方法向數組中添加元素,或者使用等價的 `<<` 操作符:
```
>> a.push(6) # 把 6 加到數組結尾
=> [42, 8, 17, 6]
>> a << 7 # 把 7 加到數組結尾
=> [42, 8, 17, 6, 7]
>> a << "foo" << "bar" # 串聯操作
=> [42, 8, 17, 6, 7, "foo", "bar"]
```
最后一個命令說明,可以把添加操作串在一起使用;也說明,Ruby 不像很多其他語言,數組中可以包含不同類型的數據(本例中包含整數和字符串)。
前面用 `split` 把字符串拆分成數組,我們還可以使用 `join` 方法進行相反的操作:
```
>> a
=> [42, 8, 17, 7, "foo", "bar"]
>> a.join # 沒有連接符
=> "428177foobar"
>> a.join(', ') # 連接符是一個逗號和空格
=> "42, 8, 17, 7, foo, bar"
```
和數組有點類似的是值域(range),使用 `to_a` 方法把它轉換成數組或許更好理解:
```
>> 0..9
=> 0..9
>> 0..9.to_a # 錯了,to_a 在 9 上調用了
NoMethodError: undefined method `to_a' for 9:Fixnum
>> (0..9).to_a # 調用 to_a 要用括號包住值域
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```
雖然 `0..9` 是有效的值域,不過上面第二個表達式告訴我們,調用方法時要加上括號。
值域經常用來獲取數組中的一組元素:
```
>> a = %w[foo bar baz quux] # %w 創建一個元素為字符串的數組
=> ["foo", "bar", "baz", "quux"]
>> a[0..2]
=> ["foo", "bar", "baz"]
```
有個特別有用的技巧:值域的結束值使用 -1 時,不用知道數組的長度就能從起始值開始一直獲取到最后一個元素:
```
>> a = (0..9).to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..(a.length-1)] # 顯式使用數組的長度
=> [2, 3, 4, 5, 6, 7, 8, 9]
>> a[2..-1] # 小技巧,索引使用 -1
=> [2, 3, 4, 5, 6, 7, 8, 9]
```
值域也可以使用字母:
```
>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]
```
## 4.3.2 塊
數組和值域可以響應的方法中有很多都可以跟著一個塊(block),這是 Ruby 最強大也是最難理解的功能:
```
>> (1..5).each { |i| puts 2 * i }
2
4
6
8
10
=> 1..5
```
這段代碼在值域 `(1..5)` 上調用 `each` 方法,然后又把 `{ |i| puts 2 * i }` 這個塊傳給 `each` 方法。`|i|` 兩邊的豎線在 Ruby 中用來定義塊變量。只有這個方法才知道如何處理后面跟著的塊。本例中,值域的 `each` 方法會處理后面的塊,塊中有一個本地變量 `i`,`each` 會把值域中的各個值傳進塊中,然后執行其中的代碼。
花括號是表示塊的一種方式,除此之外還有另一種方式:
```
>> (1..5).each do |i|
?> puts 2 * i
>> end
2
4
6
8
10
=> 1..5
```
塊中的內容可以多于一行,而且經常多于一行。本書遵照一個常用的約定,當塊只有一行簡單的代碼時使用花括號形式;當塊是一行很長的代碼,或者有多行時使用 `do..end` 形式:
```
>> (1..5).each do |number|
?> puts 2 * number
>> puts '-'
>> end
2
-
4
-
6
-
8
-
10
-
=> 1..5
```
上面的代碼用 `number` 代替了 `i`,我想告訴你的是,變量名可以使用任何值。
除非你已經有了一些編程知識,否則對塊的理解是沒有捷徑的。你要做的是多看,看多了就會習慣這種用法。[[9](#fn-9)]幸好人類擅長從實例中歸納出一般性。下面是一些例子,其中幾個用到了 `map` 方法:
```
>> 3.times { puts "Betelgeuse!" } # 3.times 后跟的塊沒有變量
"Betelgeuse!"
"Betelgeuse!"
"Betelgeuse!"
=> 3
>> (1..5).map { |i| i**2 } # ** 表示冪運算
=> [1, 4, 9, 16, 25]
>> %w[a b c] # 再說一下,%w 用來創建元素為字符串的數組
=> ["a", "b", "c"]
>> %w[a b c].map { |char| char.upcase }
=> ["A", "B", "C"]
>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]
```
可以看出,`map` 方法返回的是在數組或值域中每個元素上執行塊中代碼后得到的結果。在最后兩個命令中,`map` 后面的塊在塊變量上調用一個方法,這種操作經常使用簡寫形式:
```
>> %w[A B C].map { |char| char.downcase }
=> ["a", "b", "c"]
>> %w[A B C].map(&:downcase)
=> ["a", "b", "c"]
```
(簡寫形式看起來有點兒奇怪,其中用到了符號,[4.3.3 節](#hashes-and-symbols)會介紹。)這種寫法比較有趣,一開始是由 Rails 擴展實現的,但人們太喜歡了,現在已經集成到 Ruby 核心代碼中。
最后再看一個使用塊的例子。我們看一下[代碼清單 4.4](#listing-home-base-title-spec) 中的一個測試用例:
```
test "should get home" do
get :home
assert_response :success
assert_select "title", "Ruby on Rails Tutorial Sample App"
end
```
現在不需要理解細節(其實我也不懂),從 `do` 關鍵字可以看出,測試的主體其實就是個塊。`test` 方法的參數是一個字符串(測試的描述)和一個塊,運行測試組件時會執行塊中的內容。
現在我們來分析一下我在 [1.5.4 節](chapter1.html#heroku-commands)生成隨機二級域名時使用的那行 Ruby 代碼:
```
('a'..'z').to_a.shuffle[0..7].join
```
我們一步步分解:
```
>> ('a'..'z').to_a # 由全部英文字母組成的數組
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> ('a'..'z').to_a.shuffle # 打亂數組
=> ["c", "g", "l", "k", "h", "z", "s", "i", "n", "d", "y", "u", "t", "j", "q",
"b", "r", "o", "f", "e", "w", "v", "m", "a", "x", "p"]
>> ('a'..'z').to_a.shuffle[0..7] # 取出前 8 個元素
=> ["f", "w", "i", "a", "h", "p", "c", "x"]
>> ('a'..'z').to_a.shuffle[0..7].join # 把取出的元素合并成字符串
=> "mznpybuj"
```
## 4.3.3 哈希和符號
哈希(Hash)本質上就是數組,只不過它的索引不局限于只能使用數字。(實際上在一些語言中,特別是 Perl,因為這個原因把哈希叫做“關聯數組”。)哈希的索引(或者叫“鍵”)幾乎可以使用任何對象。例如,可以使用字符串當鍵:
```
>> user = {} # {} 是一個空哈希
=> {}
>> user["first_name"] = "Michael" # 鍵為 "first_name",值為 "Michael"
=> "Michael"
>> user["last_name"] = "Hartl" # 鍵為 "last_name",值為 "Hartl"
=> "Hartl"
>> user["first_name"] # 獲取元素的方式和數組類似
=> "Michael"
>> user # 哈希的字面量形式
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}
```
哈希通過一對花括號中包含一些鍵值對的形式表示,如果只有一對花括號而沒有鍵值對(`{}`)就是一個空哈希。注意,哈希中的花括號和塊中的花括號不是一個概念。(是的,這可能會讓你困惑。)哈希雖然和數組類似,但二者卻有一個很重要的區別:哈希中的元素沒有特定的順序。[[10](#fn-10)]如果順序很重要的話就要使用數組。
通過方括號的形式每次定義一個元素的方式不太敏捷,使用 `?` 分隔的鍵值對這種字面量形式定義哈希要簡潔得多:
```
>> user = { "first_name" => "Michael", "last_name" => "Hartl" }
=> {"last_name"=>"Hartl", "first_name"=>"Michael"}
```
在上面的代碼中我用到了一個 Ruby 句法約定,在左花括號后面和右花括號前面加入了一個空格,不過控制臺會忽略這些空格。(不要問我為什么這些空格是約定俗成的,或許是某個 Ruby 編程大牛喜歡這種形式,然后約定就產生了。)
目前為止哈希的鍵都使用字符串,在 Rails 中用“符號”(Symbol)當鍵很常見。符號看起來有點兒像字符串,只不過沒有包含在一對引號中,而是在前面加一個冒號。例如,`:name` 就是一個符號。你可以把符號看成沒有約束的字符串:[[11](#fn-11)]
```
>> "name".split('')
=> ["n", "a", "m", "e"]
>> :name.split('')
NoMethodError: undefined method `split' for :name:Symbol
>> "foobar".reverse
=> "raboof"
>> :foobar.reverse
NoMethodError: undefined method `reverse' for :foobar:Symbol
```
符號是 Ruby 特有的數據類型,其他語言很少用到。初看起來感覺很奇怪,不過 Rails 經常用到,所以你很快就會習慣。符號和字符串不同,并不是所有字符都能在符號中使用:
```
>> :foo-bar
NameError: undefined local variable or method `bar' for main:Object
>> :2foo
SyntaxError
```
只要以字母開頭,其后都使用單詞中常用的字符就沒事。
用符號當鍵,我們可以按照如下的方式定義一個 `user` 哈希:
```
>> user = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> user[:name] # 獲取 :name 對應的值
=> "Michael Hartl"
>> user[:password] # 獲取未定義的鍵對應的值
=> nil
```
從上面的例子可以看出,哈希中沒有定義的鍵對應的值是 `nil`。
因為符號當鍵的情況太普遍了,Ruby 1.9 干脆就為這種用法定義了一種新句法:
```
>> h1 = { :name => "Michael Hartl", :email => "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h2 = { name: "Michael Hartl", email: "michael@example.com" }
=> {:name=>"Michael Hartl", :email=>"michael@example.com"}
>> h1 == h2
=> true
```
第二中句法把“符號 ?”變成了“鍵的名字:”形式:
```
{ name: "Michael Hartl", email: "michael@example.com" }
```
這種形式更好地沿襲了其他語言(例如 JavaScript)中哈希的表示方式,在 Rails 社區中也越來越受歡迎。這兩種方式現在都在使用,所以你要能識別它們。可是,新句法有點讓人困惑,因為 `:name` 本身是一種數據類型(符號),但 `name:` 卻沒有意義。不過在哈希字面量中,`:name ?` 和 `name:` 作用一樣。因此,`{ :name ? "Michael Hartl" }` 和 `{ name: "Michael Hartl" }` 是等效的。如果要表示符號,只能使用 `:name`(冒號在前面)。
哈希中元素的值可以是任何對象,甚至是另一個哈希,如[代碼清單 4.10](#listing-nested-hashes) 所示。
##### 代碼清單 4.10:嵌套哈希
```
>> params = {} # 定義一個名為 params(parameters 的簡稱)的哈希
=> {}
>> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
=> {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
>> params
=> {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
>> params[:user][:email]
=> "mhartl@example.com"
```
Rails 大量使用這種哈希中有哈希的形式(或稱為“嵌套哈希”),我們從 [7.3 節](chapter7.html#unsuccessful-signups)起會接觸到。
與數組和值域一樣,哈希也能響應 `each` 方法。例如,一個名為 `flash` 的哈希,它的鍵是兩個判斷條件,`:success` 和 `:danger`:
```
>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> flash.each do |key, value|
?> puts "Key #{key.inspect} has value #{value.inspect}"
>> end
Key :success has value "It worked!"
Key :danger has value "It failed."
```
注意,數組的 `each` 方法后面的塊只有一個變量,而哈希的 `each` 方法后面的塊接受兩個變量,分別表示鍵和對應的值。所以哈希的 `each` 方法每次遍歷都會以一個鍵值對為單位進行。
這段代碼用到了很有用的 `inspect` 方法,返回被調用對象的字符串字面量表現形式:
```
>> puts (1..5).to_a # 把值域轉換成數組
1
2
3
4
5
>> puts (1..5).to_a.inspect # 輸出數組的字面量形式
[1, 2, 3, 4, 5]
>> puts :name, :name.inspect
name
:name
>> puts "It worked!", "It worked!".inspect
It worked!
"It worked!"
```
順便說一下,因為使用 `inspect` 打印對象的方式經常使用,為此還有一個專門的快捷方式,`p` 方法:[[12](#fn-12)]
```
>> p :name # 等價于 'puts :name.inspect'
:name
```
## 4.3.4 重溫引入 CSS 的代碼
現在我們要重新認識一下[代碼清單 4.1](#listing-application-layout-redux) 中在布局中引入層疊樣式表的代碼:
```
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true %>
```
我們現在基本上可以理解這行代碼了。在 [4.1 節](#motivation)簡單提到過,Rails 定義了一個特殊的函數用來引入樣式表,下面的代碼
```
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
```
就是對這個函數的調用。不過還有幾個奇怪的地方。第一,括號哪去了?在 Ruby 中,括號是可以省略的,所以下面兩種寫法是等價的:
```
# 調用函數時可以省略括號
stylesheet_link_tag('application', media: 'all',
'data-turbolinks-track' => true)
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
```
第二,`media` 部分顯然是一個哈希,但是怎么沒用花括號?調用函數時,如果哈希是最后一個參數,可以省略花括號。所以下面兩種寫法是等價的:
```
# 如果最后一個參數是哈希,可以省略花括號
stylesheet_link_tag 'application', { media: 'all',
'data-turbolinks-track' => true }
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
```
還有,為什么 `data-turbolinks-track` 這個鍵值對使用舊句法?如果使用新句法,寫成
```
data-turbolinks-track: true
```
是無效的,因為其中有連字符。([4.3.3 節](#hashes-and-symbols)說過,符號中不能使用連字符。)所以只能使用舊句法,寫成
```
'data-turbolinks-track' => true
```
最后,為什么換了一行 Ruby 還能正確解析?
```
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
```
因為在這種情況下,Ruby 不關心有沒有換行。[[13](#fn-13)]我之所以把代碼寫成兩行,是要保證每行代碼不超過 80 個字符。[[14](#fn-14)]
所以,下面這段代碼
```
stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true
```
調用了 `stylesheet_link_tag` 函數,并且傳入兩個參數:一個是字符串,指明樣式表的路徑;另一個是哈希,包含兩個元素,第一個指明媒介類型,第二個啟用 Rails 4.0 中添加的 [Turbolink](https://github.com/rails/turbolinks) 功能。因為使用的是 `<%= %>`,函數的執行結果會通過 ERb 插入模板中。如果在瀏覽器中查看網頁的源碼,會看到引入樣式表所用的 HTML,如[代碼清單 4.11](#listing-scss-source) 所示。(你可能會在 CSS 的文件名后看到額外的字符,例如 `?body=1`。這是 Rails 加入的,確保修改 CSS 后瀏覽器會重新加載。)
##### 代碼清單 4.11:引入 CSS 的代碼生成的 HTML
```
<link data-turbolinks-track="true" href="/assets/application.css" media="all"
rel="stylesheet" />
```
如果在瀏覽器中打開 [http://localhost:3000/assets/application.css](http://localhost:3000/assets/application.css) 查看 CSS 的話,會發現是這個文件是空的(但有一些注釋)。[第 5 章](chapter5.html#filling-in-the-layout)會介紹如何添加樣式。
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習