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 对象本身也有方法!