Ruby 程式設計/語法/方法呼叫
Ruby 中的方法是一組表示式,它返回一個值。使用方法,可以將程式碼組織成子例程,這些子例程可以從程式的其他區域輕鬆呼叫。其他語言有時將其稱為函式。方法可以定義為類的一部分,也可以單獨定義。
方法使用以下語法呼叫
method_name(parameter1, parameter2,…)
無論是否帶引數,Ruby 都允許不使用括號進行方法呼叫
method_name
results = method_name parameter1, parameter2
要連結方法呼叫,需要使用括號;例如
results = method_name(parameter1, parameter2).reverse
方法使用關鍵字 def 後跟方法名來定義。方法引數在方法名後的括號中指定。方法體由此定義的上方和下方 end 關鍵字包圍。按照慣例,由多個單片語成的函式名稱的每個單詞之間用下劃線隔開。
示例
def output_something(value)
puts value
end
方法返回執行的最後一個語句的值。以下程式碼返回 x+y 的值。
def calculate_value(x,y)
x + y
end
也可以使用顯式 return 語句從函式返回一個值,在函式宣告結束之前。當需要終止迴圈或從函式返回條件表示式的結果時,這很有用。
請注意,如果在程式碼塊中使用“return”,實際上會從函式中跳出,這可能不是你想要的。要終止程式碼塊,請使用 break。可以將一個值傳遞給 break,該值將作為程式碼塊的結果返回
six = (1..10).each {|i| break i if i > 5}
在這種情況下,six 的值將為 6。
可以在方法定義期間指定預設引數值,以在未將值傳遞給方法時替換引數的值。
def some_method(value='default', arr=[])
puts value
puts arr.length
end
some_method('something')
上面的方法呼叫將輸出
something
0
以下程式碼是 Ruby 1.8 中的語法錯誤
def foo( i = 7, j ) # Syntax error in Ruby 1.8.7 Unexpected ')', expecting '='
return i + j
end
上面的程式碼將在 1.9.2 中執行,並且在邏輯上等同於下面的程式碼片段
def foo( j, i = 7)
return i + j
end
方法的最後一個引數可以字首一個星號 (*),有時被稱為“splat”運算子。這表示可以將更多引數傳遞給函式。這些引數將被收集起來,建立一個陣列。
def calculate_value(x,y,*otherValues)
puts otherValues
end
calculate_value(1,2,'a','b','c')
在上面的示例中,輸出將為 ['a', 'b', 'c']。
星號運算子也可以放在方法呼叫中的陣列引數前面。在這種情況下,陣列將被展開,並將值傳遞進去,就好像它們由逗號隔開一樣。
arr = ['a','b','c']
calculate_value(*arr)
與
calculate_value('a','b','c')
Ruby 允許的另一種技術是在呼叫函式時給出雜湊,這讓你可以同時使用命名引數和可變長度引數。
def accepts_hash( var )
print "got: ", var.inspect # will print out what it received
end
accepts_hash :arg1 => 'giving arg1', :argN => 'giving argN'
# => got: {:argN=>"giving argN", :arg1=>"giving arg1"}
你可以看到,accepts_hash 的引數被捲成了一個雜湊 變數。這種技術在 Ruby on Rails API 中被大量使用。
還要注意 accepts_hash 函式呼叫的引數周圍缺少括號,並且在 :arg1 => '...' 程式碼周圍也沒有 { } 雜湊宣告語法。上面的程式碼等效於更詳細的
accepts_hash( :arg1 => 'giving arg1', :argN => 'giving argN' ) # argument list enclosed in parens
accepts_hash( { :arg1 => 'giving arg1', :argN => 'giving argN' } ) # hash is explicitly created
現在,如果要將程式碼塊傳遞給函式,則需要括號。
accepts_hash( :arg1 => 'giving arg1', :argN => 'giving argN' ) { |s| puts s }
accepts_hash( { :arg1 => 'giving arg1', :argN => 'giving argN' } ) { |s| puts s }
# second line is more verbose, hash explicitly created, but essentially the same as above
在 Ruby 2.0 之後的版本中,還可以使用新的內建關鍵字引數,這使得上面的技術稍微容易一些。新的語法如下
def test_method(a, b, c:true, d:false)
puts a,b,c,d
end
上面的函式現在可以這樣呼叫:test_method(1,2)、test_method(1,2, c: somevalue)、test_method(1,2, d:someothervalue)、test_method(1,2, c:somevalue, d:someothervalue),甚至 test_method(1,2, d: someothervalue, c: somevalue) 。在這個示例中,除非要立即將結果連結到另一個函式或方法,否則括號不是必需的。請注意,這確實意味著你“必須”為 (在本例中) 你傳遞給函式的 'c' 和 'd' 值指定名稱,無論何時要包含它們。像 test_method(1,2,3,4) 這樣的方法呼叫將不起作用。
關鍵字引數在存在許多可能傳遞給函式的非必需選項時尤其有用。在使用時指定名稱會使結果函式呼叫非常易讀。
與星號類似,取地址符號 (&) 也可以放在函式宣告的最後一個引數前面。這表示函式期望傳遞一個程式碼塊。將建立一個 Proc 物件,並將其分配給包含傳入程式碼塊的引數。
與取地址符號運算子類似,在方法呼叫期間,字首有取地址符號的 Proc 物件將被它包含的程式碼塊替換。然後可以使用Yield。
def method_call
yield
end
method_call(&someBlock)
Ruby 為程式設計師提供了一組從函數語言程式設計領域借鑑的強大功能,即閉包、高階函式和一等函式 [1]。這些功能在 Ruby 中透過程式碼塊、Proc 物件和方法(也是物件)實現——這些概念密切相關,但存在細微的差異。實際上,我發現自己對這個主題感到非常困惑,難以理解程式碼塊、Proc 和方法之間的區別,並且不確定使用它們的最佳實踐。此外,由於我有一些 Lisp 背景和多年的 Perl 經驗,我不確定 Ruby 概念如何對映到來自其他程式語言的類似習慣用法,例如 Lisp 的函式和 Perl 的子例程。在篩選了數百個新聞組帖子後,我發現我不是唯一遇到這個問題的人,事實上,許多“Ruby 新手”都在努力理解相同的想法。
在本文中,我闡述了我對 Ruby 這個方面的理解,這是透過對 Ruby 書籍、文件和 comp.lang.ruby 的大量研究得出的,真誠地希望其他人也能發現它有用。
厚顏無恥地從 Ruby 文件中摘錄,Proc 定義如下:Proc 物件是已繫結到一組區域性變數的程式碼塊。繫結後,程式碼可以在不同的上下文中呼叫,並且仍然可以訪問這些變數。
還提供了一個有用的示例
def gen_times(factor)
return Proc.new {|n| n*factor }
end
times3 = gen_times(3) # 'factor' is replaced with 3
times5 = gen_times(5)
times3.call(12) #=> 36
times5.call(5) #=> 25
times3.call(times5.call(4)) #=> 60
在 Ruby 中,Proc 扮演著函式的角色。更準確地說,應該稱它們為函式物件,因為在 Ruby 中,一切都是物件。這種物件在民間傳說中有一個名字——函子。函子被定義為一個物件,可以像呼叫普通函式一樣被呼叫,通常使用相同的語法,這正是 Proc 的本質。
在維基百科上,閉包被定義為一個函式,它引用了其詞法上下文中的自由變數。從示例和前面的定義可以明顯看出,Ruby Procs 也可以充當閉包。注意它與 Ruby 程式碼塊的定義有多麼接近,這些程式碼塊已經繫結到一組區域性變數。
Ruby 中的 Procs 是第一類物件,因為它們可以在執行時建立,儲存在資料結構中,作為引數傳遞給其他函式,並作為其他函式的返回值返回。實際上,gen_times 示例展示了所有這些標準,除了“作為引數傳遞給其他函式”。這個可以如下呈現:
def foo (a, b)
a.call(b)
end
putser = Proc.new {|x| puts x}
foo(putser, 34)
還有一種建立 Procs 的簡寫符號——Kernel 方法 lambda [2](我們很快就會談到方法,但現在假設 Kernel 方法類似於全域性函式,可以從程式碼中的任何地方呼叫)。使用 lambda,可以將前面示例中的 Proc 物件建立重寫為:
putser = lambda {|x| puts x}
實際上,lambda 和 Proc.new 之間有兩個細微的差別。首先,引數檢查。Ruby 文件對 lambda 的描述是:等同於 Proc.new,除了生成的 Proc 物件在被呼叫時會檢查傳遞的引數數量。以下是一個演示此差異的示例:
pnew = Proc.new {|x, y| puts x + y}
lamb = lambda {|x, y| puts x + y}
# works fine, printing 6
pnew.call(2, 4, 11)
# throws an ArgumentError
lamb.call(2, 4, 11)
其次,Proc 處理返回值的方式有所不同。從 Proc.new 返回會從封閉方法返回(就像從程式碼塊返回一樣,我們稍後會詳細介紹)。
def try_ret_procnew
ret = Proc.new { return "Baaam" }
ret.call
"This is not reached"
end
# prints "Baaam"
puts try_ret_procnew
而從 lambda 返回則更符合慣例,返回到其呼叫者。
def try_ret_lambda
ret = lambda { return "Baaam" }
ret.call
"This is printed"
end
# prints "This is printed"
puts try_ret_lambda
考慮到這一點,我建議使用 lambda 而不是 Proc.new,除非嚴格需要後者的行為。除了比 Proc.new 少兩個字元之外,它的行為也更不容易讓人意外。
簡單地說,方法也是程式碼塊。但是,與 Procs 不同,方法不會繫結到周圍的區域性變數。相反,它們繫結到某個物件,並可以訪問該物件的例項變數 [3]。
class Boogy
def initialize
@id = 15
end
def arbo
puts "The id is #{@id}"
end
end
# initializes an instance of Boogy
b = Boogy.new
b.arbo
# prints "The id is 15"
思考方法時,一個有用的習慣用語是,你正在向定義了該方法的物件傳送訊息。給定一個 *接收者*——一個定義了某個方法的物件——我們向它傳送一個訊息,該訊息包含方法的名稱,並可以選擇提供該方法將接收到的引數。在上面的示例中,呼叫 arbo 方法而不帶任何引數,類似於只發送包含“arbo”作為引數的訊息。
Ruby 透過在 Object 類中包含 send 方法(它是 Ruby 中所有物件的父類)來更直接地支援訊息傳送習慣用法。因此,以下三行等同於 arbo 方法呼叫:
# method is called on the object, with no arguments
b.arbo
# method/message name is given as a string
b.send("arbo")
# method/message name is given as a symbol
b.send(:arbo)
注意,方法也可以在所謂的“頂層”作用域中定義,該作用域不在任何使用者定義的類中。例如:
def say (something)
puts something
end
say "Hello"
雖然看起來方法 say 是“獨立的”,但實際上並非如此——Ruby 會默默地將它放入 Object 類中,該類表示你的應用程式的作用域。
def say (something)
puts something
end
say "Hello"
Object.send(:say, "Hello") # this will be the same as the above line
但這並不重要,實際上,say 可以被視為一個獨立的方法。順便說一下,這在某些語言(如 C 和 Perl)中被稱為“函式”。以下 Proc 在很多方面類似:
say = lambda {|something| puts something}
say.call("Hello")
# same effect
say["Hello"]
在 Proc 的上下文中,[] 構造與 call 同義 [4]。但是,方法比 Procs 更靈活,並且支援 Ruby 的一項非常重要的功能,我將在解釋什麼是程式碼塊之後立即介紹。
程式碼塊與 Procs 密切相關,以至於許多新手在嘗試弄清楚它們之間的實際區別時會感到頭疼。我將嘗試用一個(希望不是太俗套的)比喻來幫助理解。在我看來,程式碼塊是未出生的 Procs。程式碼塊是幼蟲,Procs 是昆蟲。程式碼塊不能獨立存在——它為程式碼準備好,以便它真正“活”起來,只有當它被繫結並轉換為 Proc 時,它才開始“活”起來。
# a naked block can't live in Ruby
# this is a compilation error !
{puts "hello"}
# now it's alive, having been converted
# to a Proc !
pr = lambda {puts "hello"}
pr.call
就這麼簡單嗎?所有的喧囂就只是為了這個嗎?不,絕不。Ruby 的設計者 Matz 發現,雖然將 Procs 傳遞給方法(以及其他 Procs)很好,並且允許使用高階函式以及各種奇特的函式式內容,但有一種常見的情況勝過其他所有情況——將單個程式碼塊傳遞給一個方法,該方法會利用它來做一些有用的事情,例如迭代。作為一個非常有才華的設計師,Matz 認為強調這種特殊情況是值得的,並使其變得更簡單、更有效。
毫無疑問,任何在 Ruby 上花費過至少幾個小時的程式設計師都曾見過以下 Ruby 光輝的示例(或者非常類似的示例)。
10.times do |i|
print "#{i} "
end
numbers = [1, 2, 5, 6, 9, 21]
numbers.each do |x|
puts "#{x} is " + (x >= 3 ? "many" : "few")
end
squares = numbers.map {|x| x * x}
(注意,do |x| ... end 等同於 { |x| ... }。)
在我看來,這樣的程式碼是讓 Ruby 成為乾淨、可讀、很棒語言的一部分。幕後發生的事情非常簡單,或者至少可以用非常簡單的方式描述。也許 Ruby 的實現方式與我將要描述的方式並不完全相同,因為最佳化考慮因素肯定在發揮作用,但它絕對足夠接近真相,可以作為一種比喻來理解。
每當一個程式碼塊附加到方法呼叫時,Ruby 會自動將它轉換為一個 Proc 物件,但它沒有顯式名稱。但是,該方法可以透過 yield 語句訪問此 Proc。請看以下示例以說明:
def do_twice
yield
yield
end
do_twice {puts "Hola"}
方法 do_twice 被定義並呼叫,並附帶一個程式碼塊。儘管該方法在它的引數列表中沒有明確要求程式碼塊,但 yield 可以呼叫該程式碼塊。這可以用更明確的方式使用 Proc 引數來實現:
def do_twice(what)
what.call
what.call
end
do_twice lambda {puts "Hola"}
這等同於前面的示例,但使用程式碼塊與 yield 更乾淨、最佳化效果更好,因為只將一個程式碼塊傳遞給方法。使用 Proc 方法,可以傳遞任意數量的程式碼塊。
def do_twice(what1, what2, what3)
2.times do
what1.call
what2.call
what3.call
end
end
do_twice( lambda {print "Hola, "},
lambda {print "querido "},
lambda {print "amigo\n"})
需要注意的是,許多人不喜歡傳遞程式碼塊,而更喜歡使用顯式 Procs。他們的理由是,程式碼塊引數是隱式的,必須檢視整個方法程式碼才能確定是否有對 yield 的呼叫,而 Proc 是顯式的,可以在引數列表中立即找到。雖然這僅僅是個人喜好問題,但瞭解這兩種方法至關重要。
與號運算子可以在幾種情況下用於在程式碼塊和 Procs 之間進行顯式轉換。理解它們的工作原理是值得的。
還記得我說過,儘管附加的程式碼塊在幕後被轉換為一個 Proc,但它在方法內部無法作為 Proc 訪問嗎?好吧,如果在方法引數列表的最後一個引數前面加上一個與號,則附加到該方法的程式碼塊會被轉換為一個 Proc 物件,並被分配給該最後一個引數。
def contrived(a, &f)
# the block can be accessed through f
f.call(a)
# but yield also works !
yield(a)
end
# this works
contrived(25) {|x| puts x}
# this raises ArgumentError, because &f
# isn't really an argument - it's only there
# to convert a block
contrived(25, lambda {|x| puts x})
與號的另一個(我認為更有效的)用法是反向轉換——將 Proc 轉換為程式碼塊。這非常有用,因為許多 Ruby 的優秀內建函式,尤其是迭代器,期望接收一個程式碼塊作為引數,有時將 Proc 傳遞給它們會更方便。以下示例取自 Pragmatic Programmers 撰寫的優秀著作“Programming Ruby”
print "(t)imes or (p)lus: "
times = gets
print "number: "
number = Integer(gets)
if times =~ /^t/
calc = lambda {|n| n*number }
else
calc = lambda {|n| n+number }
end
puts((1..10).collect(&calc).join(", "))
collect 方法期望一個程式碼塊,但在這種情況下,使用 Proc 提供程式碼塊非常方便,因為 Proc 是使用從使用者那裡獲得的知識構建的。在 calc 前面的與號確保將 Proc 物件 calc 轉換為程式碼塊,並作為附加程式碼塊傳遞給 collect。
與號還允許實現 Ruby 程式設計師中非常常見的習慣用法:將方法名稱傳遞給迭代器。假設我想將 Array 中的所有單詞轉換為大寫。我可以這樣做:
words = %w(Jane, aara, multiko)
upcase_words = words.map {|x| x.upcase}
p upcase_words
這很好,而且有效,但我感覺它有點太冗長了。upcase 方法本身應該傳遞給 map,而不需要單獨的程式碼塊和明顯多餘的 x 引數。幸運的是,正如我們之前所見,Ruby 支援向物件傳送訊息的習慣用法,並且方法可以透過它們的名稱來引用,這些名稱被實現為 Ruby 符號。例如:
p "Erik".send(:upcase)
這從字面上來說,是向物件“Erik”傳送訊息/方法 upcase。此功能可用於以優雅的方式實現 map {|x| x.upcase},我們將使用與號來實現它!正如我所說,當在方法呼叫中將與號加到某個 Proc 前面時,它會將該 Proc 轉換為程式碼塊。但如果我們不是把它加到 Proc 前面,而是加到其他物件前面呢?然後,Ruby 的隱式型別轉換規則就會生效,並會在該物件上呼叫 to_proc 方法,以嘗試將其轉換為 Proc。我們可以利用這一點為 Symbol 實現 to_proc,並實現我們想要的功能。
class Symbol
# A generalized conversion of a method name
# to a proc that runs this method.
#
def to_proc
lambda {|x, *args| x.send(self, *args)}
end
end
# Voilà !
words = %w(Jane, aara, multiko)
upcase_words = words.map(&:upcase)
在 Ruby 中,你可以在“只有一個物件”上定義方法。
a = 'b'
def a.some_method
'within a singleton method just for a'
end
>> a.some_method
=> 'within a singleton method just for a'
或者,你可以使用 define_(singleton_)method,它也會保留定義周圍的作用域。
a = 'b'
a.define_singleton_method(:some_method) {
'within a block method'
}
a.some_method
Ruby 有許多由直譯器呼叫的特殊方法。例如
class Chameleon
alias __inspect__ inspect
def method_missing(method, *arg)
if (method.to_s)[0..2] == "to_"
@identity = __inspect__.sub("Chameleon", method.to_s.sub('to_','').capitalize)
def inspect
@identity
end
self
else
super #method_missing overrides the default Kernel.method_missing
#pass on anything we weren't looking for so the Chameleon stays unnoticed and uneaten ;)
end
end
end
mrlizard = Chameleon.new
mrlizard.to_rock
這做了一些愚蠢的事情,但 method_missing 是 Ruby 超程式設計的重要組成部分。在 Ruby on Rails 中,它被廣泛用於動態建立方法。
另一個特殊方法是 initialize,Ruby 在建立類例項時會呼叫它,但這屬於下一章: 類。
Ruby 並沒有真正意義上的函式。相反,它有兩個略微不同的概念 - 方法和 Proc(正如我們所見,它們只是其他語言所說的函式物件或函子)。兩者都是程式碼塊 - 方法繫結到物件,而 Proc 繫結到作用域中的區域性變數。它們的用途大不相同。
方法是面向物件程式設計的基石,而 Ruby 是一種純粹的 OO 語言(一切都是物件),方法是 Ruby 本質的一部分。方法是 Ruby 物件執行的操作 - 如果你喜歡訊息傳遞的習慣用法,那就是它們接收的訊息。
Proc 使強大的函數語言程式設計正規化成為可能,它將程式碼變成 Ruby 的一等公民物件,允許實現高階函式。它們與 Lisp 的 lambda 表示式非常接近(毫無疑問,Ruby 的 Proc 建構函式 lambda 的起源就在這裡)。
塊的結構乍一看可能令人困惑,但實際上很簡單。用我的比喻來說,塊是一個未出生的 Proc - 它處於中間狀態,尚未繫結到任何東西。我認為,在不丟失任何理解的情況下,理解 Ruby 中塊的最簡單方法就是將塊視為 Proc 的一種形式,而不是一個單獨的概念。我們唯一需要將塊與 Proc 區分開來的時候是它們作為最後一個引數傳遞給方法的特殊情況,該方法可以使用 yield 訪問它們。
我想就是這樣了。我確信,為了撰寫這篇文章,我所做的研究消除了我對這裡介紹的概念的許多誤解。我希望其他人也能從中學習。如果你看到任何你不贊同的地方 - 從明顯的錯誤到細微的錯誤,請隨時修改本書。
[1] 似乎在純粹的理論解釋中,Ruby 沒有真正意義上的頭等函式。但是,正如本文所示,Ruby 完全能夠滿足頭等函式的大多數要求,即函式可以在程式執行期間建立、儲存在資料結構中、作為引數傳遞給其他函式以及作為其他函式的值返回。
[2] lambda 有一個同義詞 - proc,它被認為是“輕度過時”(主要是因為 proc 和 Proc.new 有細微的差別,這令人困惑)。換句話說,只使用 lambda。
[3] 這些是“例項方法”。Ruby 還支援“類方法”和“類變數”,但這不在本文的討論範圍之內。
[4] 更準確地說,call 和 [] 都指 Proc 類的相同方法。是的,Proc 物件本身也有方法!