# 4.4 Ruby 類
我們之前說過,Ruby 中的一切都是對象。本節我們要自己定義一些對象。Ruby 和其他面向對象的語言一樣,使用類來組織方法,然后實例化類,創建對象。如果你剛接觸“面向對象編程”(Object-Oriented Programming,簡稱 OOP),這些聽起來都似天書一般,那我們來看一些實例吧。
## 4.4.1 構造方法
我們看過很多使用類初始化對象的例子,不過還沒自己動手做過。例如,我們使用雙引號初始化一個字符串,雙引號是字符串的字面構造方法:
```
>> s = "foobar" # 使用雙引號字面構造方法
=> "foobar"
>> s.class
=> String
```
我們看到,字符串可以響應 `class` 方法,返回值是字符串所屬的類。
除了使用字面構造方法之外,我們還可以使用等價的“具名構造方法”(named constructor),即在類名上調用 `new` 方法:[[15](#fn-15)]
```
>> s = String.new("foobar") # 字符串的具名構造方法
=> "foobar"
>> s.class
=> String
>> s == "foobar"
=> true
```
這段代碼中使用的具名構造方法和字面構造方法是等價的,只是更能表現我們的意圖。
數組和字符串類似:
```
>> a = Array.new([1, 3, 2])
=> [1, 3, 2]
```
不過哈希就有點不同了。數組的構造方法 `Array.new` 可接受一個可選的參數指明數組的初始值,`Hash.new` 可接受一個參數指明元素的默認值,就是當鍵不存在時返回的值:
```
>> h = Hash.new
=> {}
>> h[:foo] # 試圖獲取不存在的鍵 :foo 對應的值
=> nil
>> h = Hash.new(0) # 讓不存在的鍵返回 0 而不是 nil
=> {}
>> h[:foo]
=> 0
```
在類上調用的方法,如本例的 `new`,叫“類方法”(class method)。在類上調用 `new` 方法,得到的結果是這個類的一個對象,也叫做這個類的“實例”(instance)。在實例上調用的方法,例如 `length`,叫“實例方法”(instance method)。
## 4.4.2 類的繼承
學習類時,理清類的繼承關系會很有用,我們可以使用 `superclass` 方法:
```
>> s = String.new("foobar")
=> "foobar"
>> s.class # 查找 s 所屬的類
=> String
>> s.class.superclass # 查找 String 的父類
=> Object
>> s.class.superclass.superclass # Ruby 1.9 使用 BasicObject 作為基類
=> BasicObject
>> s.class.superclass.superclass.superclass
=> nil
```
這個繼承關系如[圖 4.1](#fig-string-inheritance-ruby-1-9) 所示。可以看到,`String` 的父類是 `Object`,`Object` 的父類是 `BasicObject`,但是 `BasicObject` 就沒有父類了。這樣的關系對每個 Ruby 對象都適用:只要在類的繼承關系上往上多走幾層,就會發現 Ruby 中的每個類最終都繼承自 `BasicObject`,而它本身沒有父類。這就是“Ruby 中一切皆對象”技術層面上的意義。
圖 4.1:`String` 類的繼承關系
要想更深入地理解類,最好的方法是自己動手編寫一個類。我們來定義一個名為 `Word` 的類,其中有一個名為 `palindrome?` 方法,如果單詞順讀和反讀都一樣就返回 `true`:
```
>> class Word
>> def palindrome?(string)
>> string == string.reverse
>> end
>> end
=> :palindrome?
```
我們可以按照下面的方式使用這個類:
```
>> w = Word.new # 創建一個 Word 對象
=> #<Word:0x22d0b20>
>> w.palindrome?("foobar")
=> false
>> w.palindrome?("level")
=> true
```
如果你覺得這個例子有點大題小做,很好,我的目的達到了。定義一個新類,可是只創建一個接受一個字符串作為參數的方法,這么做很古怪。既然單詞是字符串,讓 `Word` 繼承 `String` 不就行了,如[代碼清單 4.12](#listing-word-class) 所示。(你要退出控制臺,然后再在控制臺中輸入這寫代碼,這樣才能把之前的 `Word` 定義清除掉。)
##### 代碼清單 4.12:在控制臺中定義 `Word` 類
```
>> class Word < String # Word 繼承自 String
>> # 如果字符串和反轉后相等就返回 true
>> def palindrome?
>> self == self.reverse # self 代表這個字符串本身
>> end
>> end
=> nil
```
其中,`Word < String` 在 Ruby 中表示繼承([3.2 節](chapter3.html#static-pages)簡介過),這樣除了定義 `palindrome?` 方法之外,`Word` 還擁有所有字符串擁有的方法:
```
>> s = Word.new("level") # 創建一個 Word 實例,初始值為 "level"
=> "level"
>> s.palindrome? # Word 實例可以響應 palindrome? 方法
=> true
>> s.length # Word 實例還繼承了普通字符串的所有方法
=> 5
```
`Word` 繼承自 `String`,我們可以在控制臺中查看類的繼承關系:
```
>> s.class
=> Word
>> s.class.superclass
=> String
>> s.class.superclass.superclass
=> Object
```
這個繼承關系如[圖 4.2](#fig-word-inheritance-ruby-1-9) 所示。
圖 4.2:[代碼清單 4.12](#listing-word-class) 中定義的 `Word` 類(非內置類)的繼承關系
注意,在[代碼清單 4.12](#listing-word-class) 中檢查單詞和單詞的反轉是否相同時,要在 `Word` 類中引用這個單詞。在 Ruby 中使用 `self` 關鍵字[[16](#fn-16)]引用:在 `Word` 類中,`self` 代表的就是對象本身。所以我們可以使用
```
self == self.reverse
```
來檢查單詞是否為“回文”。其實,在類中調用方法或訪問屬性時可以不用 `self.`(賦值時不行),所以也可以寫成 `self == reverse`。
## 4.4.3 修改內置的類
雖然繼承是個很強大的功能,不過在判斷回文這個例子中,如果能把 `palindrome?` 加入 `String` 類就更好了,這樣(除了其他方法外)我們可以在字符串字面量上調用 `palindrome?` 方法。現在我們還不能直接調用:
```
>> "level".palindrome?
NoMethodError: undefined method `palindrome?' for "level":String
```
有點令人驚訝的是,Ruby 允許你這么做,Ruby 中的類可以被打開進行修改,允許像我們這樣的普通人添加一些方法:
```
>> class String
>> # 如果字符串和反轉后相等就返回 true
>> def palindrome?
>> self == self.reverse
>> end
>> end
=> nil
>> "deified".palindrome?
=> true
```
(我不知道哪一個更牛:Ruby 允許向內置的類中添加方法,或 `"deified"` 是個回文。)
修改內置的類是個很強大的功能,不過功能強大意味著責任也大,如果沒有很好的理由,向內置的類中添加方法是不好的習慣。Rails 自然有很好的理由。例如,在 Web 應用中我們經常要避免變量的值是空白(blank)的,像用戶名之類的就不應該是空格或[空白](http://en.wikipedia.org/wiki/Whitespace_(computer_science)),所以 Rails 為 Ruby 添加了一個 `blank?` 方法。Rails 控制臺會自動加載 Rails 添加的功能,下面看幾個例子(在 `irb` 中不可以):
```
>> "".blank?
=> true
>> " ".empty?
=> false
>> " ".blank?
=> true
>> nil.blank?
=> true
```
可以看出,一個包含空格的字符串不是空的(empty),卻是空白的(blank)。還要注意,`nil` 也是空白的。因為 `nil` 不是字符串,所以上面的代碼說明了 Rails 其實是把 `blank?` 添加到 `String` 的基類 `Object` 中的。[8.4 節](chapter8.html#remember-me)會再介紹一些 Rails 擴展 Ruby 類的例子。)
## 4.4.4 控制器類
討論類和繼承時你可能覺得似曾相識,不錯,我們之前見過,在靜態頁面控制器中([代碼清單 3.18](chapter3.html#listing-adding-the-about-page)):
```
class StaticPagesController < ApplicationController
def home
end
def help
end
def about
end
end
```
你現在可以理解,至少有點能理解,這些代碼的意思了:`StaticPagesController` 是一個類,繼承自 `ApplicationController`,其中有三個方法,分別是 `home`、`help` 和 `about`。因為 Rails 控制臺會加載本地的 Rails 環境,所以我們可以在控制臺中創建一個控制器,查看一下它的繼承關系:[[17](#fn-17)]
```
>> controller = StaticPagesController.new
=> #<StaticPagesController:0x22855d0>
>> controller.class
=> StaticPagesController
>> controller.class.superclass
=> ApplicationController
>> controller.class.superclass.superclass
=> ActionController::Base
>> controller.class.superclass.superclass.superclass
=> ActionController::Metal
>> controller.class.superclass.superclass.superclass.superclass
=> AbstractController::Base
>> controller.class.superclass.superclass.superclass.superclass.superclass
=> Object
```
這個繼承關系如[圖 4.3](#fig-static-pages-controller-inheritance) 所示。
我們還可以在控制臺中調用控制器的動作,動作其實就是方法:
```
>> controller.home
=> nil
```
`home` 動作的返回值為 `nil`,因為它是空的。
注意,動作沒有返回值,或至少沒返回真正需要的值。如我們在[第 3 章](chapter3.html#mostly-static-pages)看到的,`home` 動作的目的是渲染網頁,而不是返回一個值。但是,我記得沒在任何地方調用過 `StaticPagesController.new`,到底怎么回事呢?
原因在于,Rails 是用 Ruby 編寫的,但 Rails 不是 Ruby。有些 Rails 類就像普通的 Ruby 類一樣,不過也有些則得益于 Rails 的強大功能。Rails 是單獨的一門學問,應該和 Ruby 分開學習和理解。
圖 4.3:靜態頁面控制器的類繼承關系
## 4.4.5 用戶類
我們要自己定義一個類,結束對 Ruby 的介紹。這個類名為 `User`,目的是實現 [第 6 章](chapter6.html#modeling-users)用到的用戶模型。
到目前為止,我們都在控制臺中定義類,這樣很快捷,但也有點不爽。現在我們要在應用的根目錄中創建一個名為 `example_user.rb` 的文件,然后寫入[代碼清單 4.13](#listing-example-user) 中的內容。
##### 代碼清單 4.13:定義 `User` 類
example_user.rb
```
class User
attr_accessor :name, :email
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
def formatted_email
"#{@name} <#{@email}>"
end
end
```
這段代碼有很多地方要說明,我們一步步來。先看下面這行:
```
attr_accessor :name, :email
```
這行代碼為用戶的名字和電子郵件地址創建“屬性訪問器”(attribute accessors),也就是定義了“獲取方法”(getter)和“設定方法”(setter),用來取回和賦值 `@name` 和 `@email` 實例變量([2.2.2 節](chapter2.html#mvc-in-action)和 [3.6 節](chapter3.html#mostly-static-pages-exercises)簡介過)。在 Rails 中,實例變量的意義在于,它們自動在視圖中可用。而通常實例變量的作用是在 Ruby 類中不同的方法之間傳遞值。(稍后會更詳細地介紹這一點。)實例變量總是以 `@` 符號開頭,如果未定義,值為 `nil`。
第一個方法,`initialize`,在 Ruby 中有特殊的意義:執行 `User.new` 時會調用這個方法。這個 `initialize` 方法接受一個參數,`attributes`:
```
def initialize(attributes = {})
@name = attributes[:name]
@email = attributes[:email]
end
```
`attributes` 參數的默認值是一個空哈希,所以我們可以定義一個沒有名字或沒有電子郵件地址的用戶。(回想一下 [4.3.3 節](#hashes-and-symbols)的內容,如果鍵不存在就返回 `nil`,所以如果沒定義 `:name` 鍵,`attributes[:name]` 會返回 `nil`,`attributes[:email]` 也是一樣。)
最后,類中定義了一個名為 `formatted_email` 的方法,使用被賦了值的 `@name` 和 `@email` 變量進行插值,組成一個格式良好的用戶電子郵件地址:
```
def formatted_email
"#{@name} <#{@email}>"
end
```
因為 `@name` 和 `@email` 都是實例變量(如 `@` 符號所示),所以在 `formatted_email` 方法中自動可用。
我們打開控制臺,加載(`require`)這個文件,實際使用一下這個類:
```
>> require './example_user' # 加載 example_user 文件中代碼的方式
=> true
>> example = User.new
=> #<User:0x224ceec @email=nil, @name=nil>
>> example.name # 返回 nil,因為 attributes[:name] 是 nil
=> nil
>> example.name = "Example User" # 賦值一個非 nil 的名字
=> "Example User"
>> example.email = "user@example.com" # 賦值一個非 nil 的電子郵件地址
=> "user@example.com"
>> example.formatted_email
=> "Example User <user@example.com>"
```
這段代碼中的點號 `.`,在 Unix 中指“當前目錄”,`'./example_user'` 告訴 Ruby 在當前目錄中尋找這個文件。接下來的代碼創建了一個空用戶,然后通過直接賦值給相應的屬性來提供他的名字和電子郵件地址(因為有 `attr_accessor` 所以才能賦值)。我們輸入 `example.name = "Example User"` 時,Ruby 會把 `@name` 變量的值設為 `"Example User"`(`email` 屬性類似),然后就可以在 `formatted_email` 中使用。
[4.3.4 節](#css-revisited)介紹過,如果最后一個參數是哈希,可以省略花括號。我們可以把一個預先定義好的哈希傳給 `initialize` 方法,再創建一個用戶:
```
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User:0x225167c @email="mhartl@example.com", @name="Michael Hartl">
>> user.formatted_email
=> "Michael Hartl <mhartl@example.com>"
```
從[第 7 章](chapter7.html#sign-up)開始,我們會使用哈希初始化對象,這種技術叫做“批量賦值”(mass assignment),在 Rails 中很常見。
- 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 練習