# **第 7 章 方法**
方法是由對象定義的與該對象相關的操作。在 Ruby 中,對象的所有操作都被封裝成方法。
### **7.1 方法的調用**
首先,讓我們溫習一下如何調用方法。
### **7.1.1 簡單的方法調用**
調用方法的語法如下所示:
**對象. 方法名( 參數 `1`, 參數 `2`, … , 參數 `n` )**
以對象開頭,中間隔著句點,后面接著是方法名,方法名后面是一排并列的用 `()` 括起來的參數。不同的方法定義的參數個數和順序也都不一樣,調用方法時必須按照定義來指定參數。另外,調用方法時 `()` 是可以省略的。
上面的對象被稱為接收者(receiver)。在面向對象的世界中,調用方法被稱為“向對象發送消息(message)”,調用的結果就是“對象接收(receive)了消息”(圖 7.1)。也就是說,方法的調用就是把幾個參數連同消息一起發送給對象的過程。

**圖 7.1 將消息發送給對象**
### **7.1.2 帶塊的方法調用**
正如第 6 章提到的 `each` 方法、`loop` 方法,方法本身可以與伴隨的塊一起被調用。這種與塊一起被調用的方法,我們稱之為帶塊的方法。
帶塊的方法的語法如下:
**對象. 方法名( 參數, …) `do` | 變量 `1`, 變量 `2`, …|
塊內容
`end`**
`do` ~ `end` 這部分就是所謂的塊。除 `do ~ end` 這一形式外,我們也可以用 `{~}` 將塊改寫為其他形式:
**對象. 方法名( 參數, …) { | 變量 `1`, 變量 `2`, …|
塊內容
}**
> **備注** 使用 `do ~ end` 時,可以省略把參數列表括起來的 `()`。使用 `{ ~ }` 時,只有在沒有參數的時候才可以省略 `()`,有一個以上的參數時就不能省略。
在塊開頭的 `| ~ |` 部分中指定的變量稱為塊變量。在執行塊的時候,塊變量由方法傳到塊內部。不同的方法對應的塊變量的個數、值也都不一樣。之前介紹過的 `times` 方法有一個塊變量,執行塊時,方法會從 0 開始依次把循環次數賦值給塊變量(代碼清單 7.1)。
**代碼清單 7.1 times_with_param.rb**
~~~
5.times do |i|
puts "第#{i} 次循環。"
end
~~~
> **執行示例**
~~~
> ruby times_with_param.rb
第0 次循環。
第1 次循環。
第2 次循環。
第3 次循環。
第4 次循環。
~~~
### **7.1.3 運算符形式的方法調用**
Ruby 中有些方法看起來很像運算符。四則運算等的二元運算符、`-`(負號)等的一元運算符、指定數組、散列的元素下標的 `[]` 等,實際上都是方法。
-
**`obj + arg1`**
-
**`obj =~ arg1`**
-
**`-obj`**
-
**`!obj`**
-
**`obj[arg1]`**
-
**`obj[arg1] = arg2`**
這些雖然與一般的方法調用的語法有點不一樣,但我們可以將 `obj` 理解為接收者,將 `arg1`、`arg2` 理解為參數,這樣一來,它們就又是我們所熟知的方法了。我們可以自由定義這種運算符形式的方法。
> **備注** 在用方法實現的運算符中,有的可以自由地重新定義,有的則不能改變默認的定義。關于運算符,我們會在第 9 章中再詳細說明。
### **7.2 方法的分類**
根據接收者種類的不同,Ruby 的方法可分為 3 類:
-
**實例方法**
-
**類方法**
-
**函數式方法**
下面,我們就來說明一下這 3 種方法。
### **7.2.1 實例方法**
實例方法是最常用的方法。假設有一個對象(實例),那么以這個對象為接收者的方法就被稱為實例方法。
下面是實例方法的一些例子:
~~~
p "10, 20, 30, 40".split(",") #=> ["10", "20", "30", "40"]
p [1, 2, 3, 4].index(2) #=> 1
p 1000.to_s #=> "1000"
~~~
在本例中,從上到下,分別以字符串、數組、數值對象為接收者。
對象能使用的實例方法是由對象所屬的類決定的。調用對象的實例方法后,程序就會執行對象所屬類預先定義好的處理。
雖然相同名稱的方法執行的處理大多都是一樣的,但具體執行的內容則會根據對象類型的不同而存在差異。例如,幾乎所有的對象都有 `to_s` 方法,這是一個把對象內容以字符串形式輸出的方法。然而,雖然都是字符串形式,但在數值對象與時間對象的情況下,字符串形式以及字符串的創建方法卻都不一樣。
~~~
p 10.to_s #=> "10"
p Time.now.to_s #=> "2013-03-18 03:17:02 +900"
~~~
### **7.2.2 類方法**
接收者不是對象而是類本身時的方法,我們稱之為類方法。例如,我們在創建對象的時候就使用到了類方法。
~~~
Array.new # 創建新的數組
File.open("some_file") # 創建新的文件對象
Time.now # 創建新的 Time 對象
~~~
此外,不直接對實例進行操作,只是對該實例所屬的類進行相關操作時,我們也會用到類方法。例如,修改文件名的時候,我們就會使用文件類的類方法。
~~~
File.rename(oldname, newname) # 修改文件名
~~~
類方法也有運算符的形式。
~~~
File.rename(oldname, newname) # 修改文件名
~~~
調用類方法時,可以使用 `::` 代替 `.`。在 Ruby 語法中,兩者所代表的意思是一樣的。
關于類方法,我們會在第 8 章中再進行詳細的說明。
### **7.2.3 函數式方法**
沒有接收者的方法,我們稱之為函數式方法。
雖說是沒有接收者,但并不表示該方法就真的沒有可作為接收者的對象,只是在函數式方法這個特殊情況下,可以省略接收者而已。
~~~
print "hello!" # 在命令行輸出字符串
sleep(10) # 在指定的時間內睡眠,終止程序
~~~
函數式方法的執行結果,不會根據接收者的狀態而發生變化。程序在執行 `print` 方法以及 `sleep` 方法的時候,并不需要知道接收者是誰。反過來說,不需要接收者的方法就是函數式方法。
> **專欄**
> **方法的標記法**
> 接下來,我們來介紹一下 Ruby 幫助文檔中方法名的標記方法。標記某個類的實例方法時,就像 `Array#each`、`Array#inject` 這樣,可以采用以下標記方法:
> **類名 # 方法名**
> 請注意,這只是寫幫助文檔或者說明時使用的標記方法,程序里面這么寫是會出錯的。
> 另外,類方法的標記方法,就像 `Array.new` 或者 `Array::new` 這樣,可以采用下面兩種寫法:
> **類名 . 方法名**
> **類名 :: 方法名**
> 這和實際的程序語法是一致的。
### **7.3 方法的定義**
定義方法的一般語法如下:
**`def` 方法名( 參數 `1`, 參數 `2`, …)
希望執行的處理
`end`**
方法名可由英文字母、數字、下劃線組成,但不能以數字開頭。
**代碼清單 7.2 hello_with_name.rb**
~~~
def hello(name)
puts "Hello, #{name}."
end
hello("Ruby")
~~~
> **備注** 雖然在說明如何定義實例方法或類方法之前,應該先說明如何定義類,但關于類的定義我們還未說明。因此,現在我們只需掌握一點,即定義了方法后,就能像省略接收者的函數式方法那樣調用方法了。
通過 `hello` 方法中的 `name` 變量,我們就可以引用執行時傳進來的參數。該程序的參數為字符串 `Ruby`,執行結果如下:
> **執行示例**
~~~
> ruby hello_with_name.rb
Hello, Ruby.
~~~
也可以指定默認值給參數(代碼清單 7.3)。參數的默認值,是指在沒有指定參數的情況下調用方法時,程序默認使用的值。定義方法時,用參數名 = 值這樣的寫法定義默認值。
**代碼清單 7.3 hello_with_default.rb**
~~~
def hello(name="Ruby")
puts "Hello, #{name}."
end
hello() # 省略參數的調用
hello("Newbie") # 指定參數的調用
~~~
> **執行示例**
~~~
> ruby hello_with_default.rb
Hello, Ruby.
Hello, Newbie.
~~~
方法有多個參數時,從參數列表的右邊開始依次指定默認值。例如,希望省略三個參數中的兩個時,就可以指定右側兩個參數為默認值。
~~~
def func(a, b=1, c=3)
┊
end
~~~
請注意只省略左邊的參數或者中間的某個參數是不行的。
### **7.3.1 方法的返回值**
我們可以用 `return` 指定方法的返回值。
**`return` 值**
下面我們來看看求立方體體積的例子。參數 `x`、`y`、`z` 分別代表長、寬、高。`x * y * z` 的結果就是方法的返回值。
~~~
def volume(x, y, z)
return x * y * z
end
p volume(2, 3, 4) #=> 24
p volume(10, 20, 30) #=> 6000
~~~
可以省略 `return` 語句,這時方法的最后一個表達式的結果就會成為方法的返回值。下面我們再通過求立方體的表面積這個例子,來看看如何省略 `return`。這里,`area` 方法的最后一行的 `(xy + yz + zx) * 2` 的結果就是方法的返回值。
~~~
def area(x, y, z)
xy = x * y
yz = y * z
zx = z * x
(xy + yz + zx) * 2
end
p area(2, 3, 4) #=> 52
p area(10, 20, 30) #=> 2200
~~~
方法的返回值,也不一定是程序最后一行的執行結果。例如,在下面的程序中,比較兩個值的大小,并返回較大的值。`if` 語句的結果就是方法的返回值,即 `a > b` 的結果為真時,`a` 為返回值;結果為假時,則 `b` 為返回值。
~~~
def max(a, b)
if a > b
a
else
b
end
end
p max(10, 5) #=> 10
~~~
因為可以省略,所以有些人就會感覺好像沒什么機會用到 `return`,但是有些情況下我們會希望馬上終止程序,這時 `return` 就派上用場了。用 `return` 語句改寫的 `max` 方法如下所示,大家可以對比一下和之前有什么異同。
~~~
def max(a, b)
if a > b
return a
end
return b # 這里的return 可以省略
end
p max(10, 5) #=> 10
~~~
如果省略 `return` 的參數,程序則返回 `nil`。方法的目的是程序處理,所以 Ruby 允許沒有返回值的方法。Ruby 中有很多返回值為 `nil` 的方法,第 1 章中介紹的 `print` 方法就是其中一。
`print` 方法只輸出參數的內容,返回值為 `nil`。
~~~
p print("1:") #=> 1:nil
# (顯示print 方法的輸出結果1: 與p 方法的輸出結果nil)
~~~
### **7.3.2 定義帶塊的方法**
之前我們已經介紹過帶塊的方法,下面就來介紹一下如何定義帶塊的方法。
首先我們來實現 `myloop` 方法,它與利用塊實現循環的 `loop` 方法的功能是一樣的。
**代碼清單 7.4 myloop.rb**
~~~
def myloop
while true
yield # 執行塊
end
end
num = 1 # 初始化num
myloop do
puts "num is #{num}" # 輸出num
break if num > 100 # num 超過 100 時跳出循環
num *= 2 # num 乘2
end
~~~
這里第一次出現了 `yield`,`yield` 是定義帶塊的方法時最重要的關鍵字。調用方法時通過塊傳進來的處理會在 `yield` 定義的地方執行。
執行該程序后,`num` 的值就會像 1、2、4、8、16……這樣 2 倍地增長下去,直到超過 100 時程序跳出 `myloop` 方法。
> **執行示例**
~~~
> ruby myloop.rb
num is 1
num is 2
num is 4
num is 8
num is 16
num is 32
num is 64
num is 128
~~~
本例的程序中沒有參數,如果 `yield` 部分有參數,程序就會將其當作塊變量傳到塊里。塊里面最后的表達式的值既是塊的執行結果,同時也可以作為 `yield` 的返回值在塊的外部使用。
關于帶塊的方法的使用方法,我們會在第 11 章中再詳細說明。
### **7.3.3 參數個數不確定的方法**
像下面的例子那樣,通過用“`*` 變量名”的形式來定義參數個數不確定的方法,Ruby 就可以把所有參數封裝為數組,供方法內部使用。
~~~
def foo(*args)
args
end
p foo(1, 2, 3) #=> [1, 2, 3]
~~~
至少需要指定一個參數的方法可以像下面這樣定義:
~~~
def meth(arg, *agrs)
[arg, args]
end
p meth(1) #=> [1, []]
p meth(1, 2, 3) #=> [1, [2, 3]]
~~~
所有不確定的參數都被作為數組賦值給變量 `args`。“`*` 變量名”這種形式的參數,只能在方法定義的參數列表中出現一次。只確定首個和最后一個參數名,并省略中間的參數時,可以像下面這樣定義:
~~~
def a(a, *b, c)
[a, b, c]
end
p a(1, 2, 3, 4, 5) #=> [1, [2, 3, 4], 5]
p a(1, 2) #=> [1, [], 2]
~~~
### **7.3.4 關鍵字參數**
關鍵字參數是 Ruby 2.0 中的新特性。
在目前為止介紹過的方法定義中,我們都需要定義調用方法時的參數個數以及調用順序。而使用關鍵字參數,就可以將參數名與參數值成對地傳給方法內部使用。
使用關鍵字參數定義方法的語法如下所示:
**`def` 方法名(參數 `1:` 參數 `1` 的值, 參數 `2:` 參數 `2` 的值, …)
希望執行的處理
`end`**
除了參數名外,使用“參數名 : 值”這樣的形式還可以指定參數的默認值。用關鍵字參數改寫計算立方體表面積的 `area` 方法的程序如下所示:
~~~
def area2(x: 0, y: 0, z: 0)
xy = x * y
yz = y * z
zx = z * x
(xy + yz + zx ) * 2
end
p area2(x: 2, y: 3, z: 4) #=> 52
p area2(z: 4, y: 3, x: 2) #=> 52 (改變參數的順序)
p area2(x: 2, z: 3) #=> 12 (省略y)
~~~
這個方法有參數 `x`、`y`、`z`,各自的默認值都為 0。調用該方法時,可以像 `x: 2` 這樣,指定一對實際的參數名和值。在用關鍵字參數定義的方法中,每個參數都指定了默認值,因此可以省略任何一個。而且,由于調用方法時也會把參數名傳給方法,所以參數順序可以自由地更改。
不過,如果把未定義的參數名傳給方法,程序就會報錯,如下所示:
~~~
area2(foo: 0) #=> 錯誤:unknown keyword: foo(ArgumentError)
~~~
為了避免調用方法時因指定了未定義的參數而報錯,我們可以使用“`**` 變量名”的形式來 接收未定義的參數。下面這個例子的方法中,除了關鍵字參數 `x`、`y`、`z` 外,還定義了 `**arg` 參數。參數 `arg` 會把參數列表以外的關鍵字參數以散列對象的形式保存。
~~~
def meth(x: 0, y: 0, z: 0, **args)
[x, y, z, args]
end
p meth(z: 4, y: 3, x: 2) #=> [2, 3, 4, {}]
p meth(x: 2, z: 3, v: 4, w: 5) #=> [2, 0, 3, {:v=>4, :w=>5}]
~~~
-
**關鍵字參數與普通參數的搭配使用**
關鍵字參數可以與普通參數搭配使用。
~~~
def func(a, b: 1, c:2)
┊
end
~~~
上述這樣定義時,`a` 為必須指定的普通參數,`b`、`c` 為關鍵字參數。調用該方法時,可以像下面這樣,首先指定普通參數,然后是關鍵字參數。
~~~
func(1, b: 2, c: 3)
~~~
-
**用散列傳遞參數**
調用用關鍵字參數定義的方法時,可以使用以符號作為鍵的散列來傳遞參數。這樣一來,程序就會檢查散列的鍵與定義的參數名是否一致,并將與散列的鍵一致的參數名傳遞給方法。
~~~
def area2(x: 0, y: 0, z: 0)
xy = x * y
yz = y * z
zx = z * x
(xy + yz + zx ) * 2
end
args1 = {x: 2, y: 3, z: 4}
p area2(args1) #=> 52
args2 = {x: 2, z: 3} #=> 省略y
p area2(args2) #=> 12
~~~
### **7.3.5 關于方法調用的一些補充**
下面補充說明一下調用方法時傳遞參數的方法。
-
**把數組分解為參數**
將參數傳遞給方法時,我們也可以先分解數組,然后再將分解后的數組元素作為參數傳遞給方法。在調用方法時,如果以“`*` 數組”這樣的形式指定參數,這時傳遞給方法的就不是數組本身,而是數組的各元素被按照順序傳遞給方法。但需要注意的是,數組的元素個數必須要和方法定義的參數個數一樣。
~~~
def foo(a, b, c)
a + b + c
end
p foo(1, 2, 3) #=> 6
args1 = [2, 3]
p foo(1, *args1) #=> 6
args2 = [1, 2, 3]
p foo(*args2) #=> 6
~~~
-
**把散列作為參數傳遞**
我們用 `{ ~ }` 這樣的形式來表示散列的字面量(literal)。將散列的字面量作為參數傳遞給方法時可以省略 `{}`。
~~~
def foo(arg)
arg
end
p foo({"a"=>1, "b"=>2}) #=> {"a"=>1, "b"=>2}
p foo("a"=>1, "b"=>2) #=> {"a"=>1, "b"=>2}
p foo(a: 1, b:2) #=> {:a=>1, :b=>2}
~~~
當雖然有多個參數,但只將散列作為最后一個參數傳遞給方法時,可以使用下面的寫法:
~~~
def bar(arg1, arg2)
[arg1, arg2]
end
p bar(100, {"a"=>1, "b"=>2}) #=> [100, {"a"=>1, "b"=>2}]
p bar(100, "a"=>1, "b"=>2) #=> [100, {"a"=>1, "b"=>2}]
p bar(100, a: 1, b: 2) #=> [100, {:a=>1, :b=>2}]
~~~
第 3 種形式是把符號作為鍵的散列傳遞給方法,與使用關鍵字參數調用方法的形式一模一樣。其實,關鍵字參數就是模仿這種將散列作為參數傳遞的寫法而設計出來的。使用關鍵字參數定義方法,既可以對鍵進行限制,又可以定義參數的默認值。因此建議大家在實際編寫程序的時候多嘗試使用關鍵字參數。
> **專欄**
> **如何書寫簡明易懂的程序**
> 程序不只是為了讓計算機理解、執行而存在的,還要能便于人們讀寫。即使是實現相同功能的程序,有的可能通俗易懂,有的卻晦澀難懂。程序是否易懂,除了與程序的設計和架構有關外,程序的外觀也起著很重要的作用。而通過注意下面列舉的 3 點,就可以使程序變得更漂亮。
> - > **換行和;**
> - > **縮進(`indent`)**
> - > **空白**
> 下面,我們就依次來看看它們到底有什么魔法可以使我們的程序變漂亮。
> - > **換行和`;`**
> Ruby 語法的特征之一就是換行可作為語句結束的標志使用。
> 除了可以使用換行表示語句結束外,我們還可以使用 `;`。這樣一來,一行程序里就可以寫多條語句,例如,
~~~
str = "hello"; print str
~~~
> 這樣的寫法與下面的寫法具有一樣的效果。
~~~
str = "hello"
print str
~~~
> 使用該語法時,把換行看作是一種自然的語句間隔,會更加便于我們讀寫程序。比起把多個操作都寫在一行里,適當的換行是書寫簡明易懂的程序的第一步。
> 然而,過多地使用 `;`,往往會使程序變得難以讀懂。因此,在使用 `;` 之前,應問問自己是不是非使用不可。經過仔細的考量,如果覺得使用后會使程序變得易懂的話再使用也不遲。順便說一下,筆者平時也很少會用到 `;`。
> - > **縮進**
> 縮進,也就是使文字“后退”。這是在程序行的開頭輸入適當的空白字符來強調程序整體感的一種書寫方法。在本書中,我們用兩個空白表示一個縮進1。
> 在下面的例子中,為了表示 `print` 方法的那兩行程序是 `if ~ end` 的內部處理,程序進行了縮進。
> if a == 1
~~~
print message1
print message2
~~~
> end
> 插入循環等的時候,會使用更深的縮進。這樣一來,語句和循環的對應關系就會變得一目了然。
> while a < 100
~~~
while b < 20
b = b + 1
print b
end
a = a + 1
print a
~~~
> end
> 下面列舉了一些需要使用縮進的情況。
> - > **條件分支**
> if a > 0
~~~
some_metod()
~~~
> else
~~~
other_metod()
~~~
> end
> - > **循環**
> while i < 10
~~~
method()
i = i - 1
~~~
> end
> - > **塊**
> some_value.each do |i|
~~~
i.method()
~~~
> end
> - > **方法、類等的定義**
> def foo
~~~
print "hello"
~~~
> end
> 使用縮進時,需要遵守以下事項。
> - > **不要突然使用縮進**
> x=10 y=20
~~~
z=30 # 不好的例子
~~~
> - > **確保縮進的整齊**
> if (foo)
~~~
if (bar)
if (buz) # 縮進得太多了
end
end
~~~
> end
> - > **空白**
> 空白存在于程序的各個角落。使用空白時,我們需要注意以下事項。
> - > **確保空白長度整齊,保持良好的平衡感**
> 特別是,如果在運算符前后使用的空白長度不一樣,程序就很可能出現莫名其妙的錯誤。例如,計算 `a` 加 `b` 時,不同的空白寫法,得到的可能是完全不一樣的結果。
> `a+b` ○好的寫法
> `a + b` ○好的寫法
> `a +b` ×不好的寫法
> `a+ b` ×不好的寫法
> `a +b` 表示調用參數為 `+b` 的方法 `a`,整個表達式容易被誤認為 `a(+b)`,因此不是好的寫法。可見,在 `+` 前后書寫空白時,要確保平衡。
> - > **良好的編碼風格**
> 養成良好的編碼風格,首先應該從參考其他人的 Ruby 程序,并模仿他們的寫法開始。編寫程序的技巧也好,良好的編碼風格也好,想要熟練掌握,都要大量閱讀其他人寫的程序。2
1這也是 Ruby 社區里面一個約定俗成的規矩。——譯者注
2現在在 Ruby 社區最流行的 Web 框架 Rails 就是不錯的參考教材之一。——譯者注
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 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 參考集
- 后記
- 謝辭