def 定義方法


在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>'
>>


以上是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>'
>>


由於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]
>>


參數中必要引數的部份一徑優先分派引數,所以在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]
>>


因為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]
>>


接收不定長度引數的參數,必須在預設引數的右邊,否則會發生錯誤。

如果方法的最後一個參數接受雜湊物件,例如:
>> 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
>> 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"

也可以提供另一種設定方式:
config "system",
           "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
>>


在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中,區域方法不可以直接存取包裹它的外部方法之參數(或宣告在區域方法前的區域變數)。關於變數範圍,之後還會細部討論。