在Ruby中要定義方法,是使用def來定義,例如,以下是個求最大公因數的方法定義:
def gcd(m, n)
n == 0 ? m : gcd(n, m % n)
end
puts gcd(20, 30) # 顯示 10
在上例中,gcd是函式名稱,m與n為參數(Parameter)名稱,如果要傳回值可使用return,如果沒有指定return,則以最後一個陳述句執行結果的傳回值,作為函式的傳回值。
在某些語言中,這稱之為定義函式,但在Ruby中def確實是定義物件上的方法,只不過上例中沒有指明方法由誰擁有,呼叫gcd時也沒有指定訊息接收者。
你也可以明確指定方法由哪個物件擁有,呼叫時也可以指定訊息接收者。例如。
obj = Object.new
def obj.gcd(m, n)
n == 0 ? m : gcd(n, m % n)
end
puts obj.gcd(20, 30) # 顯示 10
上例中,為obj定義了gcd單例方法(Singleton method)。如果在頂層環境中定義方法時,沒有指定方法由哪個物件擁有,則方法是由Object擁有的私有(private)實例方法(Instance method)。
例如,上面第一個範例的gcd方法,相當於以下寫法:
class Object
# 以下定義了gcd實例方法
def gcd(m, n)
n == 0 ? m : gcd(n, m % n)
end
private :gcd
end
定義在頂層的方法沒有指定擁有者時,就是Object擁有的私有實例方法,Object為所有類別的父類別,因此任何類別或模組定義中,就可以直接呼叫。
之後還會談到,呼叫方法時沒有指定訊息接收者,預設以self為訊息接收者,如果是呼叫私有方法,不用也不能撰寫self(除了一個特例,之後會談到),因為私有方法只能在物件內部使用,不可透過「物件.訊息」的方式呼叫。例如:
>> def some
>> print "some...."
>> end
=> nil
>> some
some....=> nil
>> self.some
NoMethodError: private method `some' called for main:Object
from (irb):10
from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>
>> print "some...."
>> end
=> nil
>> some
some....=> nil
>> self.some
NoMethodError: private method `some' called for main:Object
from (irb):10
from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>
以上是def定義方法時的細節,實際上初學者,將沒有指定擁有者的方法當作函式來看待,會是比較容易理解的方式。
在Ruby中不支援其它語言重載方法的概念(例如Java),也就是在Ruby中同一個名稱空間中,不能有相同的方法名稱。如果你定義了兩個方法具有相同的名稱但擁有不同的參數個數,則後者定義會覆蓋前者定義。例如:
>> def sum(a, b)
>> a + b
>> end
=> nil
>> def sum(a, b, c)
>> a + b + c
>> end
=> nil
>> sum(1, 2, 3)
=> 6
>> sum(1, 2)
ArgumentError: wrong number of arguments (2 for 3)
from (irb):29:in `sum'
from (irb):33
from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>
>> a + b
>> end
=> nil
>> def sum(a, b, c)
>> a + b + c
>> end
=> nil
>> sum(1, 2, 3)
=> 6
>> sum(1, 2)
ArgumentError: wrong number of arguments (2 for 3)
from (irb):29:in `sum'
from (irb):33
from C:/Winware/Ruby192/bin/irb:12:in `<main>'
>>
由於Ruby是動態語言,只需在設計時確認傳入方法的物件所擁有的特性或方法,無需採方法重載中,依型態不同來區別所呼叫方法的部份,至於依參數個數不同來區別的方法重載概念,在Ruby中可以使用預設引數(Argument)來解決。例如:
# encoding: Big5
def sum(a, b, c = 0)
a + b + c
end
puts sum(10, 20, 30) # 顯示 60
puts sum(10, 20) # 顯示 30
像sum這種加總數字的需求,事先可能不知道要傳入的引數個數,可以在定義方法的參數時使用*,表示該參數接受不定長度引數。例如:
def sum(*numbers)
total = 0
numbers.each do |number|
total += number
end
total
end
puts sum(1, 2) # 顯示 3
puts sum(1, 2, 3) # 顯示 6
puts sum(1, 2, 3, 4) # 顯示 10
你傳入方法的引數,會被收集在一個陣列中,再設定給numbers參數。在 陣列型態 中提過,*可以用來拆解陣列,將元素逐一指定給數個變數,這個語法也適用在方法的參數指定,你可以將一個陣列傳入,只要在傳入時加上*,則陣列中每個元素會自動指定給各個參數。例如:
def sum(a, b, c)
a + b + c
end
numbers = [1, 2, 3]
puts sum(*numbers) # 顯示 6
如果方法中傳回陣列,也可以如此指定。例如:
def some
[1, 2, 3]
end
x, y, z = some
puts "#{x}, #{y}, #{z}" # 顯示 1, 2, 3
方法中的參數若沒有預設引數,或使用*設定接受不定長度引數,則呼叫方法時該參數不一定要接收引數。如果方法中的參數混用必要引數與非必要引數,則引數一律優先滿足必要引數的參數。例如:
>> def some(a, *b, c, d)
>> p a, b, c, d
>> end
=> nil
>> some(1, 2, 3, 4, 5)
1
[2, 3]
4
5
=> [1, [2, 3], 4, 5]
>> some(1, 2, 3, 4)
1
[2]
3
4
=> [1, [2], 3, 4]
>> some(1, 2, 3)
1
[]
2
3
=> [1, [], 2, 3]
>>
>> p a, b, c, d
>> end
=> nil
>> some(1, 2, 3, 4, 5)
1
[2, 3]
4
5
=> [1, [2, 3], 4, 5]
>> some(1, 2, 3, 4)
1
[2]
3
4
=> [1, [2], 3, 4]
>> some(1, 2, 3)
1
[]
2
3
=> [1, [], 2, 3]
>>
參數中必要引數的部份一徑優先分派引數,所以在sum(1, 2, 3)時,a、c、d為必要引數,所以被指定了1、2、3,因為沒有引數了,所以b是空陣列。類似地:
>> def some(a, b = 10, c, d)
>> p a, b, c, d
>> end
=> nil
>> some(1, 2, 3, 4)
1
2
3
4
=> [1, 2, 3, 4]
>> some(1, 2, 3)
1
10
2
3
=> [1, 10, 2, 3]
>>
>> p a, b, c, d
>> end
=> nil
>> some(1, 2, 3, 4)
1
2
3
4
=> [1, 2, 3, 4]
>> some(1, 2, 3)
1
10
2
3
=> [1, 10, 2, 3]
>>
因為a、c、d為必要引數,所以some(1, 2, 3)時,被指定了1、2、3,因為沒有引數了,所以b會採預設值。如果混用預設引數與接收不定長度引數的參數,則在分配完必要引數之後,接下來再分配預設引數,剩下的才給接受不定長度引數的參數。例如:
>> def some(a, b = 10, *c, d)
>> p a, b, c, d
>> end
=> nil
>> some(1, 2, 3, 4, 5)
1
2
[3, 4]
5
=> [1, 2, [3, 4], 5]
>> some(1, 2, 3, 4)
1
2
[3]
4
=> [1, 2, [3], 4]
>> some(1, 2, 3)
1
2
[]
3
=> [1, 2, [], 3]
>> some(1, 2)
1
10
[]
2
=> [1, 10, [], 2]
>>
>> p a, b, c, d
>> end
=> nil
>> some(1, 2, 3, 4, 5)
1
2
[3, 4]
5
=> [1, 2, [3, 4], 5]
>> some(1, 2, 3, 4)
1
2
[3]
4
=> [1, 2, [3], 4]
>> some(1, 2, 3)
1
2
[]
3
=> [1, 2, [], 3]
>> some(1, 2)
1
10
[]
2
=> [1, 10, [], 2]
>>
接收不定長度引數的參數,必須在預設引數的右邊,否則會發生錯誤。
如果方法的最後一個參數接受雜湊物件,例如:
>> def config(name, props)
>> puts "name = #{name}"
>> puts props
>> end
=> nil
>> config "system", {"p1" => "v1", "p2" => "v2", "p3" => "v3"}
name = system
{"p1"=>"v1", "p2"=>"v2", "p3"=>"v3"}
=> nil
>>
>> puts "name = #{name}"
>> puts props
>> end
=> nil
>> config "system", {"p1" => "v1", "p2" => "v2", "p3" => "v3"}
name = system
{"p1"=>"v1", "p2"=>"v2", "p3"=>"v3"}
=> nil
>>
則{}可以省略,例如:
>> config "system",
?> "p1" => "v1",
?> "p2" => "v2",
?> "p3" => "v3"
name = system
{"p1"=>"v1", "p2"=>"v2", "p3"=>"v3"}
=> nil
>> config "system",
?> :p1 => "v1",
?> :p2 => "v2",
?> :p3 => "v3"
name = system
{:p1=>"v1", :p2=>"v2", :p3=>"v3"}
=> nil
>> config "system",
?> p1: "v1",
?> p2: "v2",
?> p3: "v3"
name = system
{:p1=>"v1", :p2=>"v2", :p3=>"v3"}
=> nil
>>
?> "p1" => "v1",
?> "p2" => "v2",
?> "p3" => "v3"
name = system
{"p1"=>"v1", "p2"=>"v2", "p3"=>"v3"}
=> nil
>> config "system",
?> :p1 => "v1",
?> :p2 => "v2",
?> :p3 => "v3"
name = system
{:p1=>"v1", :p2=>"v2", :p3=>"v3"}
=> nil
>> config "system",
?> p1: "v1",
?> p2: "v2",
?> p3: "v3"
name = system
{:p1=>"v1", :p2=>"v2", :p3=>"v3"}
=> nil
>>
這樣的方法呼叫方式,讓程式原始碼更像是個組態檔案,廣用於如Rails之類的框架中。通常提供這樣機制的方法:
config "system",
"p1" => "v1",
"p2" => "v2",
"p3" => "v3"
"p1" => "v1",
"p2" => "v2",
"p3" => "v3"
也可以提供另一種設定方式:
config "system",
"p1", "v1",
"p2", "v2",
"p3", "v3"
"p1", "v1",
"p2", "v2",
"p3", "v3"
則定義方法時,最後一個參數可以使用*設定為不定長度引數,如此兩種呼叫方式都可以支援:
>> def config(name, *props)
>> puts "name = #{name}"
>> puts props
>> end
=> nil
>> config "system",
?> "p1" => "v1",
?> "p2" => "v2",
?> "p3" => "v3"
name = system
{"p1"=>"v1", "p2"=>"v2", "p3"=>"v3"}
=> nil
>> config "system",
?> "p1", "v1",
?> "p2", "v2",
?> "p3", "v3"
name = system
p1
v1
p2
v2
p3
v3
=> nil
>>
>> puts "name = #{name}"
>> puts props
>> end
=> nil
>> config "system",
?> "p1" => "v1",
?> "p2" => "v2",
?> "p3" => "v3"
name = system
{"p1"=>"v1", "p2"=>"v2", "p3"=>"v3"}
=> nil
>> config "system",
?> "p1", "v1",
?> "p2", "v2",
?> "p3", "v3"
name = system
p1
v1
p2
v2
p3
v3
=> nil
>>
在Ruby中,方法中還可以定義方法,可以使用區域方法將某個函式中的演算組織為更小的單元,例如,在 選 擇排序 的實作時,每次會從未排序部份選擇一個最小值放置到已排序部份之後,在底下的範例中,尋找最小值的演算就實作為區域方法的方式:
def selection(number)
# 找出未排序中最小值
def min(nums, m, j)
if j == nums.length
m
elsif nums[j] < nums[m]
min(nums, j, j + 1)
else
min(nums, m, j + 1)
end
end
for i in 0..(number.length - 1)
m = min(number, i, i + 1)
if i != m
number[i], number[m] = number[m], number[i]
end
end
end
number = [1, 5, 2, 3, 9, 7]
selection(number)
print number # 顯示 [1, 2, 3, 5, 7, 9]
不過在Ruby中,區域方法不可以直接存取包裹它的外部方法之參數(或宣告在區域方法前的區域變數)。關於變數範圍,之後還會細部討論。