跳转到内容

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)

理解代码块、Proc 和方法

[编辑 | 编辑源代码]

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 代码块的定义有多么接近,这些代码块已经绑定到一组局部变量。

关于 Procs 的更多信息

[编辑 | 编辑源代码]

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

华夏公益教科书