# **第 8 章 類和模塊**
到目前為止,我們已經向大家介紹了基本數據類型(數值、字符串、數組、散列)、方法(操作數據的工具)、控制結構(描述程序如何運行)等程序運行所必需的要素。這些也都是大部分編程語言共通的元素,從某種意義上可以說是程序運行的基礎。
作為面向對象的腳本語言的 Ruby,還為我們提供了面向對象程序設計的支持。接下來,我們首先會了解面向對象程序設計的共通概念——類,以及 Ruby 獨有的功能——模塊,然后再討論面向對象程序設計的基本設計方法。

### **8.1 類是什么**
類(class)是面向對象中一個重要的術語。關于類,我們在第 4 章中已經做過簡單的說明,現在就進一步討論一下在面向對象語言中如何使用類。
### **8.1.1 類和實例**
類表示對象的種類。Ruby 中的對象都一定屬于某個類。例如,我們常說的“數組對象”“數組”,實際上都是 `Array` 類的對象(實例)。還有字符串對象,實際上是 `String` 類的對象(實例)。
相同類的對象所使用的方法也相同。類就像是對象的雛形或設計圖(圖 8.1),決定了對象的行為。

**圖 8.1 類和實例的關系**
> **1**一種傳統的日本食物。——譯者注
我們在生成新的對象時,一般會用到各個類的 `new` 方法。例如,使用 `Array.new` 方法可以生成新的數組對象。
~~~
ary = Array.new
p ary #=> []
~~~
> **備注** 像數組、字符串這樣的類,也可以使用字面量(像 `[1, 2, 3]`、`"abc"` 這樣的寫法)來生成對象。
當想知道某個對象屬于哪個類時,我們可以使用 `class` 方法。
~~~
ary = []
str = "Hello world."
p ary.class #=> Array
p str.class #=> String
~~~
當判斷某個對象是否屬于某個類時,我們可以使用 `instance_of?` 方法。
~~~
ary = []
str = "Hello world."
p ary.instance_of?(Array) #=> true
p str.instance_of?(String) #=> true
p ary.instance_of?(String) #=> false
p str.instance_of?(Array) #=> false
~~~
### **8.1.2 繼承**
我們把通過擴展已定義的類來創建新類稱為繼承。
假設我們需要編寫一個在屏幕中顯示時間的小程序。根據用戶的喜好,這個小程序能以模擬時鐘或者電子時鐘的方式顯示。
模擬時鐘與電子時鐘,兩者只是在時間的表現形式上不一樣,獲取當前時間的方法以及鬧鐘等基本功能都是相同的。因此,我們可以首先定義一個擁有基本功能的時鐘類,然后再通過繼承來分別創建模擬時鐘類和電子時鐘類(圖 8.2)。

**圖 8.2 繼承時鐘類的模擬時鐘類與電子時鐘類**
繼承后創建的新類稱為子類(subclass),被繼承的類被稱為父類(superclass)2。通過繼承我們可以實現以下事情:
2也稱超類。——譯者注
-
**在不影響原有功能的前提下追加新功能。**
-
**重定義原有功能,使名稱相同的方法產生不同的效果。**
-
**在已有功能的基礎上追加處理,擴展已有功能。**
此外,我們還可以利用繼承來輕松地創建多個具有相似功能的類。
`BasicObject` 類是 Ruby 中所有類的父類,它定義了作為 Ruby 對象的最基本功能。
> **備注** `BasicObject` 類是最最基礎的類,甚至連一般對象需要的功能都沒有定義。因此普通對象所需要的類一般都被定義為 `Object` 類。字符串、數組等都是 `Object` 類的子類。關于 `BasicObject` 和 `Object`,我們會在 8.3.2 節中再詳細說明。
圖 8.3 是本書中涉及的類繼承關系圖。另外,`Exception` 類下還有眾多子類,這里不再詳細羅列。

**圖 8.3 類的繼承關系**
子類與父類的關系稱為“is-a 關系”3。例如,`String` 類與它的父類 `Object` 就是 is-a 關系。
3這里的“a”指的是英語中的不定冠詞“a”。——譯者注
之前我們提到過查找對象所屬的類時使用 `instance_of?` 方法,而根據類的繼承關系反向追查對象是否屬于某個類時,則可以使用 `is_a?` 方法。
~~~
str = "This is a String."
p str.is_a?(String) #=> true
p str.is_a?(Object) #=> true
~~~
順便提一下,由于 `instance_of?` 方法與 `is_a?` 方法都已經在 `Object` 類中定義過了,因此普通的對象都可以使用這兩個方法。
在本章的最后,我們會介紹類與模塊的創建方法。在自己創建類之前,如果想對 Ruby 預先提供的類的使用方法一睹為快,請跳過第 2 部分余下的內容,直接進入到第 3 部分。
### **8.2 類的創建**
事不宜遲,讓我們來看看如何創建類。定義類時有很多約束,我們先從最基礎的開始。
代碼清單 8.1 就是一個創建類的例子:
**代碼清單 8.1 hello_class.rb**
~~~
class HelloWorld # class 關鍵字
def initialize(myname = "Ruby") # initialize 方法
@name = myname # 初始化實例變量
end
def hello # 實例方法
puts "Hello, world. I am #{@name}."
end
end
bob = HelloWorld.new("Bob")
alice = HelloWorld.new("Alice")
ruby = HelloWorld.new
bob.hello
~~~
### **8.2.1 class 關鍵字**
`class` 關鍵字在定義類時使用。以下是 `class` 關鍵字的一般用法:
**`class` 類名
類的定義
`end`**
類名的首字母必須大寫。
### **8.2.2 initialize 方法**
在 `class` 關鍵字中定義的方法為該類的實例方法。代碼清單 8.1 的 `hello` 方法就是實例方法。
其中,名為 `initialize` 的方法比較特別。使用 `new` 方法生成新的對象時,`initialize` 方法會被調用,同時 `new` 方法的參數也會被原封不動地傳給 `initialize` 方法。因此初始化對象時需要的處理一般都寫在這個方法中。
~~~
def initialize(myname = "Ruby") # initialize 方法
@name = myname # 初始化實例變量
end
~~~
在這個例子中,`initialize` 方法接受了參數 `myname`。因此,
~~~
bob = HelloWorld.new("Bob")
~~~
像這樣,就可以把 `"Bob"` 傳給 `initialize` 方法生成對象。由于 `initialize` 方法的參數指定了默認值 `"Ruby"`,因此,像下面這樣沒有指定參數時,
~~~
ruby = HelloWorld.new
~~~
會自動把 `"Ruby"` 傳給 `initialize` 方法。
### **8.2.3 實例變量與實例方法**
我們再回頭看看代碼清單 8.1 的 `initialize` 方法。
~~~
def initialize(myname = "Ruby") # initialize 方法
@name = myname # 初始化實例變量
end
~~~
通過 `@name = myname` 這行程序,作為參數傳進來的對象會被賦值給變量 `@name`。我們把以 `@` 開頭的變量稱為實例變量。在不同的方法中,程序會把局部變量看作是不同的變量來對待。而只要在同一個實例中,程序就可以超越方法定義,任意引用、修改實例變量的值。另外,引用未初始化的實例變量時的返回值為 `nil`。
不同實例的實例變量值可以不同。只要實例存在,實例變量的值就不會消失,并且可以被任意使用。而局部變量則是在調用方法時被創建,而且只能在該方法內使用。
我們來看看下面的例子:
~~~
alice = HelloWorld.new("Alice")
bob = HelloWorld.new("Bob")
ruby = helloWorld.new
~~~
`alice`、`bob`、`ruby` 各自擁有不同的 `@name`(圖 8.4)。

**圖 8.4 HelloWorld 類與實例**
可以在實例方法中引用實例變量,下面是 `HelloWorld` 類定義的 `hello` 方法引用 `@name` 的例子:
~~~
class HelloWorld
┊
def hello # 實例方法
puts "Hello, world. I am #{@name}."
end
end
~~~
通過以下方式調用 `HelloWolrd` 類定義的 `hello` 方法:
~~~
bob.hello
~~~
輸出結果如下所示:
~~~
Hello, world. I am Bob.
~~~
### **8.2.4 存取器**
在 Ruby 中,從對象外部不能直接訪問實例變量或對實例變量賦值,需要通過方法來訪問對象的內部。
為了訪問代碼清單 8.1 中 `HelloWorld` 類的 `@name` 實例變量,我們需要定義以下方法:
**代碼清單 8.2 hello_class.rb(部分)**
~~~
class HelloWorld
┊
def name # 獲取@name
@name
end
def name=(value) # 修改@name
@name = value
end
┊
end
~~~
第一個方法 `name` 只是簡單地返回 `@name` 的值,我們可以像訪問屬性一樣使用該方法。
~~~
p bob.name #=> "Bob"
~~~
第二個方法的方法名為 `name=`,使用方法如下:
~~~
bob.name = "Robert"
~~~
乍一看,該語法很像是在給對象的屬性賦值,但實際上卻是在調用 `name=("Robert")` 這個方法。利用這樣的方法,我們就可以突破 Ruby 原有的限制,從外部來自由地訪問對象內部的實例變量了。
當對象的實例變量有多個時,如果逐個定義存取器,就會使程序變得難懂,而且也容易寫錯。為此,Ruby 為了我們提供了更簡便的定義方法 `attr_reader`、`attr_writer`、`attr_accessor`(表 8.1)。只要指定實例變量名的符號(symbol),Ruby 就會自動幫我們定義相應的存取器。
**表 8.1 存取器的定義**
<table border="1" data-line-num="216 217 218 219 220 221" width="90%"><thead><tr><th> <p class="表頭單元格">定義</p> </th> <th> <p class="表頭單元格">意義</p> </th> </tr></thead><tbody><tr><td> <p class="表格單元格"><code>attr_reader :name</code></p> </td> <td> <p class="表格單元格">只讀(定義 <code>name</code> 方法)</p> </td> </tr><tr><td> <p class="表格單元格"><code>attr_writer :name</code></p> </td> <td> <p class="表格單元格">只寫(定義 <code>name=</code> 方法)</p> </td> </tr><tr><td> <p class="表格單元格"><code>attr_accessor :name</code></p> </td> <td> <p class="表格單元格">讀寫(定義以上兩個方法)</p> </td> </tr></tbody></table>
也可以像下面這樣只寫一行代碼,其效果與剛才的 `name` 方法以及 `name=` 方法的效果是一樣的。
~~~
class HelloWorld
attr_accessor :name
end
~~~
> **備注** Ruby 中一般把設定實例變量的方法稱為 writer,讀取實例變量的方法稱為 reader,這兩個方法合稱為 accessor。另外,有時也把 reader 稱為 getter,writer 稱為 setter,合稱為 accessor method4。
4一般把 accessor(method)翻譯為存取器或者訪問器,本書統一翻譯為存取器。——譯者注
### **8.2.5 特殊變量 self**
在實例方法中,可以用 `self` 這個特殊的變量來引用方法的接收者。接下來就讓我們來看看其他的實例方法如何調用 `name` 方法。
**代碼清單 8.3 hello_class.rb(部分)**
~~~
class HelloWorld
attr_accessor :name
┊
def greet
puts "Hi, I am #{self.name}."
end
end
┊
~~~
`greet` 方法里的 `self.name` 引用了調用 `greet` 方法時的接收者。
調用方法時,如果省略了接收者,Ruby 就會默認把 `self` 作為該方法的接收者。因此,即使省略了 `self`,也還是可以調用 `name` 方法,如下所示:
~~~
def greet
print "Hi, I am #{name}"
end
~~~
另外,在調用像 `name=` 方法這樣的以 `=` 結束的方法時,有一點需要特別注意。即使實例方法中已經有了 `name = "Ruby"` 這樣的定義,但如果僅在方法內部定義名為 `name` 的局部變量,也不能以缺省接收者的方式調用 `name=` 方法。這種情況下,我們需要用 `self.name = "Ruby"` 的形式來顯式調用 `name` 方法。
~~~
def test_name
name = "Ruby" # 為局部變量賦值
self.name = "Ruby" # 調用name= 方法
end
~~~
> **備注** 雖然 `self` 本身與局部變量形式相同,但由于它是引用對象本身時的保留字,因此我們即使對它進行賦值,也不會對其本身的值有任何影響。像這樣,已經被系統使用且不能被我們自定義的變量名還有 `nil`、`true`、`false`、`__FILE__`、`__LINE__`、`__ENCODING__` 等。
### **8.2.6 類方法**
方法的接收者就是類本身(類對象)的方法稱為類方法。正如我們在 7.2.2 節中提到的那樣,類方法的操作對象不是實例,而是類本身。
下面,讓我們在 `class << 類名 ~ end` 這個特殊的類定義中,以定義實例方法的形式來定義類方法。
~~~
class << HelloWorld
def hello(name)
puts "#{name} said hello."
end
end
HelloWorld.hello("John") #=> John said hello.
~~~
在 `class` 上下文中使用 `self` 時,引用的對象是該類本身,因此,我們可以使用 `class << self ~ end` 這樣的形式,在 `class` 上下文中定義類方法。
~~~
class HelloWorld
class << self
def hello(name)
puts "#{name} said hello."
end
end
end
~~~
除此以外,我們還可以使用 `def 類名 . 方法名 ~ end` 這樣的形式來定義類方法。
~~~
def HelloWorld.hello(name)
puts "#{name} said hello."
end
HelloWorld.hello("John") #=> John said hello.
~~~
同樣,只要是在 `class` 上下文中,這種形式下也可以像下面的例子那樣使用 `self`。
~~~
class HelloWorld
def self.hello(name)
puts "#{name} said hello."
end
end
~~~
> **備注** `class << 類名 ~ end` 這種寫法的類定義稱為單例類定義,單例類定義中定義的方法稱為單例方法。
### **8.2.7 常量**
在 class 上下文中可以定義常量。
~~~
class HelloWorld
Version = "1.0"
┊
end
~~~
對于在類中定義的常量,我們可以像下面那樣使用 `::`,通過類名來實現外部訪問。
~~~
p HelloWorld::Version #=> "1.0"
~~~
### **8.2.8 類變量**
以 `@@` 開頭的變量稱為類變量。類變量是該類所有實例的共享變量,這一點與常量類似,不同的是我們可以多次修改類變量的值。另外,與實例變量一樣,從類的外部訪問類變量時也需要存取器。不過,由于 `attr_accessor` 等存取器都不能使用,因此需要直接定義。代碼清單 8.4 的程序在代碼清單 8.1 的 `HelloWorld` 類的基礎上,添加了統計 `hello` 方法被調用次數的功能。
**代碼清單 8.4 hello_count.rb**
~~~
class HelloCount
@@count = 0 # 調用hello 方法的次數
def HelloCount.count # 讀取調用次數的類方法
@@count
end
def initialize(myname="Ruby")
@name = myname
end
def hello
@@count += 1 # 累加調用次數
puts "Hello, world. I am #{@name}.\n"
end
end
bob = HelloCount.new("Bob")
alice = HelloCount.new("Alice")
ruby = HelloCount.new
p HelloCount.count #=> 0
bob.hello
alice.hello
ruby.hello
p HelloCount.count #=> 3
~~~
### **8.2.9 限制方法的調用**
到目前為止,我們定義的方法,都能作為實例方法被任意調用,但是有時候我們可能并不希望這樣。例如,只是為了匯總多個方法的共同處理而定義的方法,一般不會公開給外部使用。
Ruby 提供了 3 種方法的訪問級別,我們可以按照需要來靈活調整。
-
**`public`……以實例方法的形式向外部公開該方法**
-
**`private`……在指定接收者的情況下不能調用該方法(只能使用缺省接收者的方式調用該方法,因此無法從實例的外部訪問)**
-
**`protected`……在同一個類中時可將該方法作為實例方法調用**
在修改方法的訪問級別時,我們會為這 3 個關鍵字指定表示方法名的符號。
首先來看看使用 `public` 和 `private` 的例子(代碼清單 8.5)。
**代碼清單 8.5 acc_test.rb**
~~~
class AccTest
def pub
puts "pub is a public method."
end
public :pub # 把pub 方法設定為public(可省略)
def priv
puts "priv is a private method."
end
private :priv # 把priv 方法設定為private
end
acc = AccTest.new
acc.pub
acc.priv
~~~
`AccTest` 類的兩個方法中,`pub` 方法可以正常調用,但是在調用 `priv` 方法時程序會發生異常,并出現以下錯誤信息 :
> **執行示例**
~~~
> ruby acc_test.rb
pub is a public method.
acc_test.rb:17:in `<main>': private method `priv' called for
#<AccTest:0x007fb4089293e8> (NoMethodError)
~~~
希望統一定義多個方法的訪問級別時,可以使用下面的語法 :
~~~
class AccTest
public # 不指定參數時,
# 以下的方法都被定義為public
def pub
puts "pub is a public method."
end
private # 以下的方法都被定義為private
def priv
puts "priv is a private method."
end
end
~~~
> **備注** 沒有指定訪問級別的方法默認為 `public`,但 `initialize` 方法是個例外,它通常會被定義為 `private`。
定義為 `protected` 的方法,在同一個類(及其子類)中可作為實例方法使用,而在除此以外的地方則無法使用。
代碼清單 8.6 定義了擁有 X、Y 坐標的 `Point` 類。在這個類中,實例中的坐標可以被外部讀取,但不能被修改。為此,我們可以利用 `protected` 來實現交換兩個坐標值的方法 `swap`。
**代碼清單 8.6 point.rb**
~~~
class Point
attr_accessor :x, :y # 定義存取器
protected :x=, :y= # 把x= 與y= 設定為protected
def initialize(x=0.0, y=0.0)
@x, @y = x, y
end
def swap(other) # 交換x、y 值的方法
tmp_x, tmp_y = @x, @y
@x, @y = other.x, other.y
other.x, other.y = tmp_x, tmp_y # 在同一個類中
# 可以被調用
return self
end
end
p0 = Point.new
p1 = Point.new(1.0, 2.0)
p [ p0.x, p0.y ] #=> [0.0, 0.0]
p [ p1.x, p1.y ] #=> [1.0, 2.0]
p0.swap(p1)
p [ p0.x, p0.y ] #=> [1.0, 2.0]
p [ p1.x, p1.y ] #=> [0.0, 0.0]
p0.x = 10.0 #=> 錯誤(NoMethodError)
~~~
### **8.3 擴展類**
### **8.3.1 在原有類的基礎上添加方法**
Ruby 允許我們在已經定義好的類中添加方法。下面,我們來試試給 `String` 類添加一個計算字符串單詞數的實例方法 `count_word`(代碼清單 8.7)。
**代碼清單 8.7 ext_string.rb**
~~~
class String
def count_word
ary = self.split(/\s+/) # 用空格分割接收者
return ary.size # 返回分割后的數組的元素總數
end
end
str = "Just Another Ruby Newbie"
p str.count_word #=> 4
~~~
### **8.3.2 繼承**
正如在 8.1.2 節中介紹的那樣,利用繼承,我們可以在不對已有的類進行修改的前提下,通過增加新功能或重定義已有功能等手段來創建新的類。
定義繼承時,在使用 `class` 關鍵字指定類名的同時指定父類名。
**`class` 類名`<` 父類名
類定義
`end`**
在程序清單 8.8 中,創建一個繼承了 `Array` 類的 `RingArray` 類。`RingArray` 類只是重定義了讀取數組內容時使用的 `[]` 運算符。該程序通過 `super` 關鍵字調用父類中同名的方法(在本例中也就是 `Array#[]`)。
**代碼清單 8.8 ring_array.rb**
~~~
class RingArray < Array # 指定父類
def [](i) # 重定義運算符[]
idx = i % size # 計算新索引值
super(idx) # 調用父類中同名的方法
end
end
wday = RingArray["日", "月", "火", "水", "木", "金", "土"]
p wday[6] #=> "土"
p wday[11] #=> "木"
p wday[15] #=> "月"
p wday[-1] #=> "土"
~~~
對 `RingArray` 類指定了超過數組長度的索引時,結果就會從溢出部分的開頭開始重新計算索引(圖 8.5)。

**圖 8.5 RingArray 類**
利用繼承,我們可以把共同的功能定義在父類,把各自獨有的功能定義在子類。
定義類時沒有指定父類的情況下,Ruby 會默認該類為 `Object` 類的子類。
`Object` 類提供了許多便于實際編程的方法。但在某些情況下,我們也有可能會希望使用更輕量級的類,而這時就可以使用 `BasicObject` 類。
`BasicObject` 類只提供了組成 Ruby 對象所需的最低限度的方法。類對象調用 `instance_methods` 方法后,就會以符號的形式返回該類的實例方法列表。下面我們就用這個方法來對比一下 `Object` 類和 `BasicObject` 類的實例方法。
> **執行示例**
~~~
> irb --simple-prompt
>> Object.instance_methods
=> [:nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone,
:dup, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze,
:frozen?, :to_s, ...... 等眾多方法名......]
>> BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
~~~
雖然大部分方法我們都還沒有接觸到,但據此也可以看出,相對于 `Object` 類持有多種方法,`BacsicObject` 類所擁有的功能都是最基本的。 定義 `BasicObject` 的子類時,與 `Object` 類不同,需要明確指定 `BasicObject` 類為父類,如下所示 :
~~~
class MySimpleClass < BasicObject
┊
end
~~~
### **8.4 alias 與 undef**
### **8.4.1 alias**
有時我們會希望給已經存在的方法設置別名。這種情況下就需要使用 `alias` 方法。`alias` 方法的參數為方法名或者符號名。
**`alias` 別名 原名 `#` 直接使用方法名
`alias``:` 別名 `:` 原名 `#` 使用符號名**
像 `Array#size` 與 `Array#length` 這樣,為同一種功能設置多個名稱時,我們會使用到 `alias`。
另外,除了為方法設置別名外,在重定義已經存在的方法時,為了能用別名調用原來的方法,我們也需要用到 `alias`。
下面的例子中定義了類 `C1` 及其子類 `C2`。在類 `C2` 中,對 `hello` 方法設置別名 `old_hello` 后,重定義了 `hello` 方法。
~~~
class C1 # 定義C1
def hello # 定義hello
"Hello"
end
end
class C2 < C1 # 定義繼承了C1 的子類C2
alias old_hello hello # 設定別名old_hello
def hello # 重定義hello
"#{old_hello}, again"
end
end
obj = C2.new
p obj.old_hello #=> "Hello"
p obj.hello #=> "Hello, again
~~~
### **8.4.2 undef**
`undef` 用于刪除已有方法的定義。與 `alias` 一樣,參數可以指定方法名或者符號名。
**`undef` 方法名 `#` 直接使用方法名
`undef :` 方法名 `#` 使用符號名**
例如,在子類中希望刪除父類定義的方法時可以使用 `undef`。
> **專欄**
> **單例類**
> 在 8.2.6 節中介紹定義類方法的方法時,我們提到了單例類定義,而通過利用單例類定義,就可以給對象添加方法(單例方法)。單例類定義被用于定義對象的專屬實例方法。在下面的例子中,我們分別將 `"Ruby"` 賦值給 `str1` 對象和 `str2` 對象,然后只對 `str1` 對象添加 `hello` 方法。這樣一來,兩個對象分別調用 `hello` 方法時,`str1` 對象可以正常調用,但 `str2` 對象調用時程序就會發生錯誤。
~~~
str1 = "Ruby"
str2 = "Ruby"
class << str1
def hello
"Hello, #{self}!"
end
end
p str1.hello #=> "Hello, Ruby!"
p str2.hello #=> 錯誤(NoMethodError)
~~~
> Ruby 中所有的類都是 `Class` 類的實例,對 `Class` 類添加實例方法,就等于給所有的類都添加了該類方法。因此,只希望對某個實例添加方法時,就需要利用單例方法。
> 單例類的英語為 singleton class 或者 eigenclass。
### **8.5 模塊是什么**
模塊是 Ruby 的特色功能之一。如果說類表現的是事物的實體(數據)及其行為(處理),那么模塊表現的就只是事物的行為部分。模塊與類有以下兩點不同:
-
**模塊不能擁有實例**
-
**模塊不能被繼承**
### **8.6 模塊的使用方法**
接下來,我們就來介紹一下模塊的典型用法。
### **8.6.1 提供命名空間**
所謂命名空間(namespace),就是對方法、常量、類等名稱進行區分及管理的單位。由于模塊提供各自獨立的命名空間,因此 `A` 模塊中的 `foo` 方法與 `B` 模塊中的 `foo` 方法,就會被程序認為是兩個不同的方法。同樣,`A` 模塊中的 `FOO` 常量與 `B` 模塊的 `FOO` 常量,也是兩個不同的常量。
無論是方法名還是類名,當然都是越簡潔越好,但是像 `size`、`start` 等這種普通的名稱,可能在很多地方都會使用到。因此,通過在模塊內定義名稱,就可以解決命名沖突的問題。
例如,在 `FileTest` 模塊中存在與獲取文件信息相關的方法。我們使用“模塊名 `.` 方法名”的形式來調用在模塊中定義的方法,這樣的方法稱為模塊函數。
~~~
# 檢查文件是否存在
p FileTest.exist?("/usr/bin/ruby") #=> true
# 文件大小
p FileTest.size("/usr/bin/ruby") #=> 1374684
~~~
如果沒有定義與模塊內的方法、常量等同名的名稱,那么引用時就可以省略模塊名。通過 `include` 可以把模塊內的方法名、常量名合并到當前的命名空間。下面是與數學運算有關的 `Math` 模塊的例子。
~~~
# 圓周率(常量)
p Math::PI #=> 3.141592653589793
# 2 的平方根
p Math.sqrt(2) #=> 1.4142135623730951
include Math # 包含Math 模塊
p PI #=> 3.141592653589793
p sqrt(2) #=> 1.4142135623730951
~~~
像這樣,通過把一系列相關的功能匯總在一個模塊中,就可以集中管理相關的命名。
### **8.6.2 利用 Mix-in 擴展功能**
Mix-in 就是將模塊混合到類中。在定義類時使用 `include`,模塊里的方法、常量就都能被類使用。
像代碼清單 8.9 那樣,我們可以把 `MyClass1` 和 `MyClass2` 中兩者共通的功能定義在 `MyModule` 中。雖然有點類似于類的繼承,但 Mix-in 可以更加靈活地解決下面的問題。
-
**雖然兩個類擁有相似的功能,但是不希望把它們作為相同的種類(`Class`)來考慮的時候**
-
**`Ruby` 不支持父類的多重繼承,因此無法對已經繼承的類添加共通的功能的時候**
關于繼承和 Mix-in 之間的關系,我們會在 8.8 節中進行說明。
**代碼清單 8.9 mixin_sample.rb**
~~~
module MyModule
# 共通的方法等
end
class MyClass1
include MyModule
# MyClass1 中獨有的方法
end
class MyClass2
include MyModule
# MyClass2 中獨有的方法
end
~~~
### **8.7 創建模塊**
我們使用 `module` 關鍵字來創建模塊。
語法與創建類時幾乎相同。模塊名的首字母必須大寫。
**`module` 模塊名
模塊定義
`end`**
下面,讓我們參考著代碼清單 8.1 的 `HelloWold` 類,來看看如何創建模塊(代碼清單 8.10)。
**代碼清單 8.10 hello_module.rb**
~~~
module HelloModule # module 關鍵字
Version = "1.0" # 定義常量
def hello(name) # 定義方法
puts "Hello, #{name}."
end
module_function :hello # 指定hello 方法為模塊函數
end
p HelloModule::Version #=> "1.0"
HelloModule.hello("Alice") #=> Hello, Alice.
include HelloModule # 包含模塊
p Version #=> "1.0"
hello("Alice") #=> Hello, Alice.
~~~
### **8.7.1 常量**
和類一樣,在模塊中定義的常量可以通過模塊名訪問。
~~~
p HelloModule::Version #=> "1.0"
~~~
### **8.7.2 方法的定義**
和類一樣,我們也可以在 `module` 上下文中定義方法。
然而,如果僅僅定義了方法,雖然在模塊內部與包含此模塊的上文中都可以直接調用,但卻不能以“模塊名 . 方法名”的形式調用。如果希望把方法作為模塊函數公開給外部使用,就需要用到 `module_function` 方法。`module_function` 的參數是表示方法名的符號。
~~~
def hello(name)
puts "Hello, #{name}."
end
module_function :hello
~~~
以“模塊名 `.` 方法名”的形式調用時,如果在方法中調用 `self`(接收者),就會獲得該模塊的對象。
~~~
module FooMoudle
def foo
p self
end
module_function :foo
end
FooMoudle.foo #=> FooMoudle
~~~
此外,如果類 Mix-in 了模塊,就相當于為該類添加了實例方法。在這種情況下,`self` 代表的就是被 Mix-in 的類的對象。
即使是相同的方法,在不同的上下文調用時,其含義也會不一樣,因此對于 Mix-in 的模塊,我們要注意根據實際情況判斷是否使用模塊函數功能。一般不建議在定義為模塊函數的方法中使用 `self`。
### **8.8 Mix-in**
現在,我們來詳細討論一下本章開頭就提到的 Mix-in。這里使用 `include` 使類包含模塊(代碼清單 8.11)。
**代碼清單 8.11 mixin_test.rb**
~~~
module M
def meth
"meth"
end
end
class C
include M # 包含M 模塊
end
c = C.new
p c.meth #=> meth
~~~
類 `C` 包含模塊 `M` 后,模塊 `M` 中定義的方法就可以作為類 `C` 的實例方法供程序調用。
另外,如果想知道類是否包含某個模塊,可以使用 `include?` 方法。
~~~
C.include?(M) #=> true
~~~
類 `C` 的實例在調用方法時,Ruby 會按類 `C`、模塊 `M`、類 `C` 的父類 `Object` 這個順序查找該方法,并執行第一個找到的方法。被包含的模塊的作用就類似于虛擬的父類。

**圖 8.6 類的繼承關系**
我們用 `ancestors` 方法和 `superclass` 方法調查類的繼承關系。在代碼清單 8.11 中追加以下代碼并執行,我們就可以通過 `ancestors` 取得繼承關系的列表。進而也就可以看出,被包含的模塊 `M` 也被認為是類 `C` 的一個“祖先”。而 `superclass` 方法則直接返回類 `C` 的父類。
~~~
p C.ancestors #=> [C, M, Object, Kernel, BasicObject]
p C.superclass #=> Object
~~~
> **備注** `ancestors` 方法的返回值中的 `Kernel` 是 Ruby 內部的一個核心模塊,Ruby 程序運行時所需的共通函數都封裝在此模塊中。例如 `p` 方法、`raise` 方法等都是由 `Kernel` 模塊提供的模塊函數。
雖然 Ruby 采用的是不允許多個父類的單一繼承模型,但是通過利用 Mix-in,我們就既可以保持單一繼承的關系,又可以同時讓多個類共享其他功能。
在 Ruby 標準類庫中,`Enumerable` 模塊就是利用 Mix-in 擴展功能的一個典型例子。使用 `each` 方法的類中包含 `Enumerable` 模塊后,就可以使用 `each_with_index` 方法、`collect` 方法等對元素進行排序處理的方法。`Array`、`Hash`、`IO` 類等都包含了 `Enumerable` 模塊(圖 8.7)。這些類雖然沒有繼承這樣的血緣關系,但是從“可以使用 `each` 方法遍歷元素”這一點來看,可以說它們都擁有了某種相似甚至相同的屬性。

**圖 8.7 Enumerable 模塊和各類的關系**
單一繼承的優點就是簡單,不會因為過多的繼承而導致類之間的關系變得復雜。但是另一方面,有時我們又會希望更加積極地重用已有的類,或者把多個類的特性合并為更高級的類,在那樣的情況下,靈活使用單一繼承和 Mix-in,既能使類結構簡單易懂,又能靈活地應對各種需求。
### **8.8.1 查找方法的規則**
首先,我們來了解一下使用 Mix-in 時方法的查找順序。
① **同繼承關系一樣,原類中已經定義了同名的方法時,優先使用該方法。**
~~~
module M
def meth
"M#meth"
end
end
class C
include M # 包含M
def meth
"C#meth"
end
end
c = C.new
p c.meth #=> C#meth
~~~
② **在同一個類中包含多個模塊時,優先使用最后一個包含的模塊。**
~~~
module M1
┊
end
module M2
┊
end
class C
include M1 #=> 包含M1
include M2 #=> 包含M2
end
p C.ancestors #=> [C, M2, M1, Object, Kernel]
~~~
③ **嵌套 `include` 時,查找順序也是線性的,此時的關系如圖 8.8 所示。**
~~~
module M1
┊
end
module M2
┊
end
module M3
include M2 #=> 包含M2
end
class C
include M1 #=> 包含M1
include M3 #=> 包含M3
end
p C.ancestors #=> [C, M3, M2, M1, Object, Kernel]
~~~

**圖 8.8 嵌套 include 時的關系**
④ **相同的模塊被包含兩次以上時,第 2 次以后的會被省略。**
~~~
module M1
┊
end
module M2
┊
end
class C
include M1 #=> 包含M1
include M2 #=> 包含M2
include M1 #=> 包含M1
end
p C.ancestors #=> [C, M2, M1, Object, Kernel, BasicObject]
~~~
### **8.8.2 extend 方法**
在之前的專欄中,我們已經介紹了如何逐個定義單例方法,而利用 `Object#extend` 方法,我們還可以實現批量定義單例方法。`extend` 方法可以使單例類包含模塊,并把模塊的功能擴展到對象中。
~~~
module Edition
def edition(n)
"#{self} 第#{n} 版"
end
end
str = "Ruby 基礎教程"
str.extend(Edition) #=> 將模塊Mix-in 進對象
p str.edition(4) #=> "Ruby 基礎教程第4 版"
~~~
`include` 可以幫助我們突破繼承的限制,通過模塊擴展類的功能;而 `extend` 則可以幫助我們跨過類,直接通過模塊擴展對象的功能。
### **8.8.3 類與 Mix-in**
在 Ruby 中,所有類本身都是 `Class` 類的對象。我們之前也介紹過接收者為類本身的方法就是類方法。也就是說,類方法就是類對象的實例方法。我們可以把類方法理解為:
-
**`Class` 類的實例方法**
-
**類對象的單例方法**
繼承類后,這些方法就會作為類方法被子類繼承。對子類定義單例方法,實際上也就是定義新的類方法。
除了之前介紹的定義類方法的語法外,使用 `extend` 方法也同樣能為類對象追加類方法。下面是使用 `extend` 方法追加類方法,并使用 `include` 方法追加實例方法的一個例子。
~~~
module ClassMethods # 定義類方法的模塊
def cmethod
"class method"
end
end
module InstanceMethods # 定義實例方法的模塊
def imethod
"instance method"
end
end
class MyClass
# 使用extend 方法定義類方法
extend ClassMethods
# 使用include 定義實例方法
include InstanceMethods
end
p MyClass.cmethod #=> "class method"
p Myclass.new.imethod #=> "instance method"
~~~
> **備注** 在 Ruby 中,所有方法的執行,都需要通過作為接收者的某個對象的調用。換句話說,Ruby 的方法(包括單例方法)都一定屬于某個類,并且作為接收者對象的實例方法被程序調用。從這個角度來說,人們只是為了便于識別接收者的類型,才分別使用了“實例方法”和“類方法”這樣的說法。
### **8.9 面向對象程序設計**
“面向對象”這個概念,被廣泛地應用在問題分析、系統設計或程序設計等系統和程序開發領域中。雖然這個概念目前被用在了各種各樣的領域中,但首先使用這個概念的是與程序設計相關的領域。
由于本書是程序設計語言的入門書,因此這里并不會向其他領域過多延伸,而只會介紹與程序設計語言(當然就是 Ruby 了)相關的對象和面向對象方面的基礎知識。
下面,我們暫且不討論具體如何編寫程序,而是先來了解一下編寫程序時的一些思考方法。因為話題比較抽象,所以可能會有點難懂,不過請大家放輕松,耐心地讀下去你就會抓住竅門的。
### **8.9.1 對象是什么**
包括 Ruby 在內,世界上有多種面向對象的程序設計語言。不同的語言,不僅語法不一樣,功能也千差萬別,但它們幾乎都有一個共通點,就是將程序處理的主體作為“對象”來考慮。
一般情況下,程序語言的處理主體是數據。之前提到的數值、字符串、數組等都是簡單的數據。
而面向對象的語言中的“對象”就是指數據(或者說數據的集合)及操作該數據的方法的組合。之前我們提到過 Ruby 里的數值 `3.14` 是 `Float` 類的實例。這個 `3.14` 不僅是表示 3.14 這個數值的數據,還包括與數值相關的操作方法。
~~~
f = 3.14
p f.round #=> 3 (四舍五入)
p f.ceil #=> 4 (進位)
p f.to_i #=> 3 (整數變換)
~~~
像這樣,把數據以及處理數據的操作方法作為對象合并在一起貫穿整體,在面向對象程序設計中是很常見的。例如,將浮點數做四舍五入處理的 `round` 方法是可以被作為 `Float` 類的一部分來提供的,這樣一來,數據以及處理數據的方法的組合也不會出現錯誤。
如果只是簡單的數值處理,并不會有太大的問題,但大部分程序都需要更復雜的數據構成。例如,在一個處理圖片的程序中,圖片的長寬、顏色、包括圖片本身的內容都需要轉換為二進制數據。如果能把一個圖片作為一個零部件來處理,那么像圖冊這樣復雜的應用程序也就變得容易編寫了(圖 8.9)。

**圖 8.9 結構化后的數據**
在開發大型程序的時候,若不將大量的數據整合到一起并根據一定的規則進行整理,程序處理本身的統一性會蕩然無存。面向對象程序設計會把這種歸類統一的數據作為各種各樣的對象來看待。在對象中,數據以及處理數據的方法也是成套存在的,而且還負有處理數據的責任。
另外,就像網絡上的服務器管理的文件一樣,遠程數據也可以作為程序的處理對象來考慮。在網絡程序設計里,Web、郵件等不同的應用程序,都需要遵守各自不同的規范(也稱為協議)。用程序來實現協議的情況下,一般會把管理消息格式、規范等的程序抽象成庫(library)。Ruby 的庫里就有現成的 `Net::HTTP`、`Net::POP` 等類,可以非常輕松地編寫網絡程序。
### **8.9.2 面向對象的特征**
上文中我們簡單地介紹了面向對象程序設計的思考方法,下面就讓我們來整理一下面向對象的特征。
-
**封裝**
所謂封裝(encapsulation),就是指使對象管理的數據不能直接從外部進行操作,數據的更新、查詢等操作都必須通過調用對象的方法來完成。通過封裝,可以防止因把非法數據設置給對象而使程序產生異常的情況發生。
為此,就需要編寫不會讓對象內部產生異常的方法。最理想的做法是,在定義方法時就考慮如何避免錯誤的發生,而不是在使用方法編寫程序時才開始注意。
Ruby 對象在默認情況下是強制被封裝的,因此無法從外部直接訪問 Ruby 對象的實例變量。雖然有像 `attr_accessor`(8.2.4 節)這樣簡單定義訪問級別的方法,但也不要過度使用,建議只在需要公開時才使用。
封裝的另外一個好處就是,可以隱藏對象內部數據處理的具體細節,把內部邏輯抽象地表現出來。例如,通過使用 `Time` 類,就可以進行從系統獲取當前時間、從時間里提取年月日等操作。
~~~
t = Time.now # 從系統獲取當前時間
p t.year #=> 2013(從時間里提取年)
~~~
從系統獲取當前時間時是如何處理的、`Time` 對象內部是以什么形式管理時間的、以及從時間中提取年時要進行何種運算等等,以上這些事情都由 `Time` 類的方法來實現。就算對象內部的數據結構改變了,只要公開給外部的方法名、功能等沒有改變,類的使用者就完全不需要理會內部邏輯作出了怎樣的修改,照常使用即可。相反,類的編寫者只要提供的方法恰當,就可以直接修改類的內部邏輯,而不需要在意類的使用者。可見,封裝對類的編寫者和使用者來說都非常有好處。
-
**多態**
對象是數據及其處理的組合。對象知道數據是怎樣被處理的。換句話說,各個對象都有自己獨有的消息解釋權。一個方法名屬于多個對象(不同對象的處理結果也不一樣)這種現象,用面向對象的術語來說,就是多態(polymorphism)。
例如,我們可以觀察一下對 `Object` 類、`String` 類和 `Float` 類的各對象調用 `to_s` 方法的運行結果,可以看出,不同的類得到的結果是不一樣的。
~~~
obj = Object.new # 對象(Object)
str = "Ruby" # 字符串(String)
num = Math::PI # 數值(Float)
p obj.to_s #=> "#<Object:0x07fa1d6bd1008>"
p str.to_s #=> "Ruby"
p num.to_s #=> "3.141592653589793"
~~~
三者的 `to_s` 方法名一樣,含義也都是“以可以顯示的形式把數據轉換為字符串”,但實際的字符串轉換方式卻因對象而異(圖 8.10)。`String` 類和 `Float` 類都是繼承自 `Object` 類,也都重新定義了從 `Object` 類繼承的 `to_s` 方法,并提供了更適合自己語義的 `to_s` 方法。

※ 雖然方法名一樣,但調用的卻是各個類專用的版本。
**圖 8.10 多態**
### **8.9.3 鴨子類型**
下面,我們來看一種結合對象特征,靈活運用多態的思考方法——鴨子類型(duck typing)。鴨子類型的說法來自于“能像鴨子那樣走路,能像鴨子一樣啼叫的,那一定就是鴨子”這句話。這句話的意思是,對象的特征并不是由其種類(類及其繼承關系)決定的,而是由對象本身具有什么樣的行為(擁有什么方法)決定的。例如,假設我們希望從字符串數組中取出元素,并將字母轉換成小寫后返回結果(代碼清單 8.12)。
**代碼清單 8.12 fetch_and_downcase.rb**
~~~
def fetch_and_downcase(ary, index)
if str = ary[index]
return str.downcase
end
end
ary = ["Boo", "Foo", "Woo"]
p fetch_and_downcase(ary, 1) #=> "foo"
~~~
實際上,除了數組外,我們也可以像下面那樣,把散列傳給該方法處理。
~~~
hash = {0 => "Boo", 1 => "Foo", 2 => "Woo"}
p fetch_and_downcase(hash, 1) #=> "foo"
~~~
`fetch_and_downcase` 方法對傳進來的參數只有兩個要求:
-
**能以 `ary[index]` 形式獲取元素**
-
**獲取的元素可以執行 `downcase` 方法**
只要參數符合這兩個要求,`fetch_and_downcase` 方法并不關心傳進來的到底是數組還是散列。
Ruby 中的變量沒有限制類型,所以不會出現不是某個特定的類的對象,就不能給變量賦值的情況。因此,在程序開始運行之前,我們都無法知道變量指定的對象的方法調用是否正確。
這樣的做法有個缺點,就是增加了程序運行前檢查錯誤的難度。但是,從另外一個角度來看,則可以非常簡單地使沒有明確繼承關系的對象之間的處理變得通用。只要能執行相同的操作,我們并不介意執行者是否一樣;相反,雖然實際上是不同的執行者,但通過定義相同名稱的方法,也可以實現處理通用化。這就是鴨子類型思考問題的方法。
利用鴨子類型實現處理通用化,并不要求對象之間有明確的繼承關系,因此,要想靈活運用,可能還需要花不少功夫。例如剛才介紹的 `obj[index]` 的形式,就被眾多的類作為訪問內部元素的手段而使用。剛開始時,我們可以先有意識地留意這種簡單易懂的方法,然后再由淺入深,慢慢地就可以抓住竅門了。
### **8.9.4 面向對象的例子**
接下來讓我們通過一個實際的例子,來看看對象是如何被構造的。代碼清單 8.13 是一個利用 `Net::HTTP` 類取得 Ruby 官網首頁的 HTML,并將其輸出到控制臺的腳本。
**代碼清單 8.13 http_get.rb**
~~~
1: require "net/http"
2: require "uri"
3: url = URI.parse("http://www.ruby-lang.org/ja/")
4: http = Net::HTTP.start(url.host, url.port)
5: doc = http.get(url.path)
6: puts doc
~~~
程序的第 1、2 行中引用了 `net/http` 庫以及 `uri` 庫。這樣,我們就可以使用 `Net::HTTP` 類和 `URI` 模塊了。第 3 行使用了 `URI` 模塊的 `parse` 方法來解析 URL 的字符串,返回的結果是字符串解析后的 `URI::HTTP` 類的對象。根據 URL 的編寫規則,URL 會被分解成多個屬性。
~~~
require "uri"
url = URI.parse("http://www.ruby-lang.org/ja/")
p url.scheme #=> "http" (體系:URL 的種類)
p url.host #=> "www.ruby-lang.org" (主機名)
p url.port #=> "80" (端口號)
p url.path #=> "/ja/" (路徑)
p url.to_s #=> "http://www.ruby-lang.org/ja/"
~~~
體系(scheme)是指使用哪種通信協議。連接網絡上的服務器時,需要知道服務器的主機名以及端口號。路徑用于定位服務器上管理的文件。`URI::HTTP` 類的作用就是,把 URL 字符串解析后分解出來的信息,以對象的形式再次整合在一起。
需要注意的是,模塊名是 `URI` 而不是 `URL`。URL 指的是 URI 標識符中某種特定種類的東西。關于兩者的關系,這里不再詳細介紹,現階段我們只需要知道 URL 是 URI 的一種就可以了。
讓我們再次回到代碼清單 8.13,在程序第 4 行,把主機名和端口號傳給 `Net::HTTP` 類的 `start` 方法,并創建 `Net::HTTP` 對象。在程序第 5 行,對 `Net::HTTP` 的 `get` 方法指定路徑,獲取文檔內容。最后,在程序第 6 行,把得到的文檔內容輸出到控制臺。由于得到的文檔內容是 `String` 對象,因此后續處理與 `Net::HTTP` 類沒有關系。
調用 `Net::HTTP#get` 方法的時候,對象內部會做以下處理:
① **使用主機名和端口號,與服務器建立通信(叫做 socket,套接字)**
② **使用路徑,創建代表請求信息的 `Net::HTTPRequest` 對象**
③ **對套接字寫入請求信息**
④ **從套接字中讀取數據,并將其保存到代表響應信息的 `Net::HTTPResponse` 對象中**
⑤ **利用 `Net::HTTPResponse` 本身提供的功能,解析響應信息,提取文檔部分并返回。**
流程圖如下所示。

**圖 8.11 http_get.rb 的流程圖**
在這個例子中,URL 解析由 `URI::HTTP` 負責,網絡連接由套接字負責,與信息交換相關的操作由 `Net::HTTPRequest` 和 `Net::HTTPResponse` 負責,通信中必要的套接字、請求、響應等相關操作由 `Net::HTTP` 負責。像這樣,不同的對象各司其職,決定應該如何配置參數、該執行什么樣的處理等事項。
這些事項不僅在新建程序時有用,在擴展、修改已有程序時也非常有用。對象之間通過方法交換信息,而至于這些信息在彼此內部是如何被處理的,則并不需要關心。在生成類時,我們只要牢記把適當的信息交給適當的方法處理,就可以設計出易于讀寫的程序。
重要的是如何自然地寫出程序 5 這除了需要豐富的程序設計經驗外,還需要擁有設計模式等類結構相關的知識。“自然”這樣的說法可能有點夸張,但通過指把事物的外部特征作為參考依據,我們就可以使用與實際事物相近的模型去組織、構建程序。
5這里指符合人腦思維的程序。一般認為,人腦的思維方式是面向對象的。——譯者注。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 1 部分 Ruby 初體驗
- 第 1 章 Ruby 初探
- 第 2 章 便利的對象
- 第 3 章 創建命令
- 第 2 部分 Ruby 的基礎
- 第 4 章 對象、變量和常量
- 第 5 章 條件判斷
- 第 6 章 循環
- 第 7 章 方法
- 第 8 章 類和模塊
- 第 9 章 運算符
- 第 10 章 錯誤處理與異常
- 第 11 章 塊
- 第 3 部分 Ruby 的類
- 第 12 章 數值類
- 第 13 章 數組類
- 第 14 章 字符串類
- 第 15 章 散列類
- 第 16 章 正則表達式類
- 第 17 章 IO 類
- 第 18 章 File 類與 Dir 類
- 第 19 章 Encoding 類
- 第 20 章 Time 類與 Date 類
- 第 21 章 Proc 類
- 第 4 部分 動手制作工具
- 第 22 章 文本處理
- 第 23 章 檢索郵政編碼
- 附錄
- 附錄 A Ruby 運行環境的構建
- 附錄 B Ruby 參考集
- 后記
- 謝辭