跳转到内容

Ruby Programming/Syntax/Method Calls

维基教科书,自由的教学读本
上一项: 控制结构 索引 下一项: 类别

方法(Method)

[编辑]

什么是方法(method)? 在个体导向编程中,我们不希望外部直接操作个体内的资料;因为个体本身更了解这些资料应如何操作,故透过它提供的方式操作资料较为合适。这么想吧,我们传一些讯息给个体,这些讯息将引起个体做出某些动作或有意义的回应。我们只需知道传什么讯息可以让个体做哪些工作,而不需关心个体内部的实际工作内容。那些允许我们要求个体完成或处理讯息的工作,就称之为个体的方法或行为。“方法”或“行为”这两种词汇皆有使用。

在 Ruby 中,我们以句号 (.) 调用个体的方法。个体为主词置于句号左侧,方法为动词置于句号右侧。 Ruby 是在程式运行途中,依变数当前所指涉之个体决定如何调用方法回应讯息。

ruby> "abcdef".length
   6

直觉可知这是在问此字串个体之长度为何。技术上,我们说“调用 "abcdef" 之个体方法 length”。

其他个体对“长度”的诠译可能有些许差异,或者完全不同。

ruby> foo = "abc"
   "abc"
ruby> foo.length
   3
ruby> foo = ["abcde", "fghij"]
   ["abcde", "fghij"]
ruby> foo.length
   2

长度的意义根据我们所谈及的对象不同而有所差异。我们第一次问 foo 之长度时,它指涉一个字串,故长度的意义为此字串的字元数。第二次问长度时,foo指涉一个阵列,在大多数情形下,此时长度的意义是指阵列的内容数量,故答案为 2 。

根据实际指涉之个体以及传递之讯息决定工作方法的方式,接近我们使用自然语言时的表达习惯:同一词语会依对象不同而具有不同的意义。此为个体导向编程的特征,我们将之称为多型(polymorphism)

当个体接收到一个它无法理解的讯息时,将唤起(raised)错误:

ruby> foo = 5
   5
ruby> foo.length
ERR: (eval):1: undefined method `length' for 5(Fixnum)

故我们需要知道个体接受哪些方法,虽然我们不须知道这些方法如何工作。若方法需要引数(arguments)时,引数通常用小括弧包起,但可以也省略。

If you don't have code that needs to use method result immediately, Ruby allows to specify parameters ommitting parenthesis:

  object.method(arg1, arg2)
  object.method arg1, arg2

  results = method_name parameter1, parameter2           # calling method, not using parentheses
  
  # you need to use parentheses, if you want to work with immediate result from the method
  # if method returned an array, and we want to get element order reversed
  results = method_name(parameter1, parameter2).reverse

Ruby 有一个特殊变数称为self,它指涉发生行为之个体。因为 我们经常使用 "self.",所以在个体本身内部使用时,我们可以省略它。


self.method_name(args)
#is the same as
method_name(args)

传统上的函数呼叫(function call)在 Ruby 中亦被视为方法调用。那些函数性的方法仍然具有类似传统程式语言的函数之行为特征。在 Ruby 中谈到“函数”时,通常表示那些并非真正的个体方法,而是纯粹的演算法或抽象的叙述。

Ruby doesn’t really have functions. Rather, it has two slightly different concepts - methods and Procs (which are, as we have seen, simply what other languages call function objects, or functors). Both are blocks of code - methods are bound to Objects, and Procs are bound to the local variables in scope. Their uses are quite different.

Methods are the cornerstone of object-oriented programming, and since Ruby is a pure-OO language (everything is an object), methods are inherent to the nature of Ruby. Methods are the actions Ruby objects do - the messages they receive, if you prefer the message sending idiom.

Procs make powerful functional programming paradigms possible, turning code into a first-class object of Ruby allowing to implement high-order functions. They are very close kin to Lisp’s lambda forms (there’s little doubt about the origin of Ruby’s Proc constructor lambda)

The construct of a block may at first be confusing, but it turns out to be quite simple. A block is, as my metaphor goes, an unborn Proc - it is a Proc in an intermediate state, not bound to anything yet. I think that the simplest way to think about blocks in Ruby, without losing any comprehension, would be to think that blocks are really a form of Procs, and not a separate concept. The only time when we have to think of blocks as slighly different from Procs is the special case when they are passed as the last argument to a method which may then access them using yield.

That’s about it, I guess. I know for sure that the research I conducted for this article cleared many misunderstandings I had about the concepts presented here. I hope others will learn from it as well. If you see anything you don’t agree with - from glaring errors to nitpicky inaccuracies, feel free to comment - I’ll be happy to discuss any remarks and fix my mistakes.


方法定义

[编辑]

要定义一个方法时,使用def 方法名称,用end结束。 参数(parameters)方法名称后面,可用括号包住或不用皆可且多个参数(parameters)使用逗号区隔。

Example:

 def output_something(value)
   puts value 
 end
 def output_something value
   puts value
 end

回传值

[编辑]

函式会回传最后一个执行的叙述的值,下面的范例会回传x+y的结果:

 def calculate_value(x,y)
   x + y
 end

An explicit return statement can also be used to return from function with a value, prior to the end of the function declaration. This is useful when you want to terminate a loop or return from a function as the result of a conditional expression.

如果你在区块内使用了return,你将会直接跳出函式,而得到的值可能不是你所想要的。 单纯要离开区块,可以用break

 six = (1..10).each {|i| break i if i > 5}

在这个程式内,six的值将会是6

Default Values

[编辑]

A default parameter value can be specified during method definition to replace the value of a parameter if it is not passed into the method or the parameter's value is nil.

 def some_method(value='default', arr=[])
   puts value
   puts arr.length
 end
 some_method('something')

The method call above will output:

 something
 0

Variable Length Argument List, Asterisk Operator

[编辑]

The last parameter of a method may be preceded by an asterisk(*), which is sometimes called the 'splat' operator. This indicates that more parameters may be passed to the function. Those parameters are collected up and an array is created.

 def calculate_value(x,y,*otherValues)
   puts otherValues
 end
 
 calculate_value(1,2,'a','b','c')

In the example above the output would be ['a', 'b', 'c'].

The asterisk operator may also precede an Array argument in a method call. In this case the Array will be expanded and the values passed in as if they were separated by commas.

 arr = ['a','b','c']
 calculate_value(*arr)

has the same result as:

 calculate_value('a','b','c')

Another technique that Ruby allows is to give a Hash when invoking a function, and that gives you best of all worlds: named parameters, and variable argument length.

 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"}

You see, the arguments to accepts_hash got rolled up into one hash variable. This technique is used in Ruby On Rails API heavily.

Notice missing parens around arguments to accepts_hash function, and notice there is no { } Hash declaration syntax around the :arg1 => '...' code. The above code is equivalent to more verbose:

 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

Now, if you are going to pass a code block to function, you need parentheses.

 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

The Ampersand Operator

[编辑]

Much like the asterisk, the ampersand(&) may precede the last parameter of a function declaration. This indicates that the function expects a code block to be passed in. A Proc object will be created and assigned to the parameter containing the block passed in.

Also similar to the ampersand operator, a Proc object preceded by an ampersand during a method call will be replaced by the block that it contains. Yield may then be used.

 def method_call
   yield
 end
 method_call(&someBlock)

了解 blocks, Procs and methods

[编辑]

Ruby 可以将程式码参数化,Ruby 称被参数化的程式码为 block (程式码区块)。Ruby 语法以 {||} 表示一个程式码区块,其中的 || 为参数列宣告,若无参数则可省略。

Ruby 的 Proc 类似 ECMAScript 的 Function。在 ECMAScript 中使用关键字 function 即可配置一个 Function 实例。 Ruby 则使用 Kernel::procKernel::lambda 方法,或是直接建构一个 Proc 实例(Proc.new),需提供一个 block 作为引数。

Ruby: proc { |arguments| codes }
ECMAScript: function(arguments) { codes }

Procs

[编辑]

Shamelessly ripping from the Ruby documentation, Procs are defined as follows: Proc objects are blocks of code that have been bound to a set of local variables. Once bound, the code may be called in different contexts and still access those variables.

A useful example is also provided:


 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

Procs play the role of functions in Ruby. It is more accurate to call them function objects, since like everything in Ruby they are objects. Such objects have a name in the folklore - functors. A functor is defined as an object to be invoked or called as if it were an ordinary function, usually with the same syntax, which is exactly what a Proc is.

From the example and the definition above, it is obvious that Ruby Procs can also act as closures. On Wikipedia, a closure is defined as a function that refers to free variables in its lexical context. Note how closely it maps to the Ruby definition blocks of code that have been bound to a set of local variables.

More on Procs

[编辑]

Procs in Ruby are first-class objects, since they can be created during runtime, stored in data structures, passed as arguments to other functions and returned as the value of other functions. Actually, the gen_times example demonstrates all of these criteria, except for “passed as arguments to other functions”. This one can be presented as follows:


 def foo (a, b)
     a.call(b)
 end
 
 putser = Proc.new {|x| puts x}
 foo(putser, 34)

There is also a shorthand notation for creating Procs - the Kernel method lambda [2] (we’ll come to methods shortly, but for now assume that a Kernel method is something akin to a global function, which can be called from anywhere in the code). Using lambda the Proc object creation from the previous example can be rewritten as:

 putser = lambda {|x| puts x}

Actually, there are two slight differences between lambda and Proc.new. First, argument checking. The Ruby documentation for lambda states: Equivalent to Proc.new, except the resulting Proc objects check the number of parameters passed when called.. Here is an example to demonstrate this:


 lamb = lambda {|x, y| puts x + y}
 pnew = Proc.new {|x, y| puts x + y}
 # works fine, printing 6
 pnew.call(2, 4, 11)
 
 # throws an ArgumentError
 lamb.call(2, 4, 11)

Second, there is a difference in the way returns are handled from the Proc. A return from Proc.new returns from the enclosing method (acting just like a return from a block, more on this later):


 def try_ret_procnew
     ret = Proc.new { return "Baaam" }
     ret.call
     "This is not reached"
 end
 
 # prints "Baaam"
 puts try_ret_procnew

While return from lambda acts more conventionally, returning to its caller:


 def try_ret_lambda
     ret = lambda { return "Baaam" }
     ret.call
     "This is printed"
 end
 
 # prints "This is printed"
 puts try_ret_lambda

With this in light, I would recommend using lambda instead of Proc.new, unless the behavior of the latter is strictly required. In addition to being way cooler a whopping two characters shorter, its behavior is less surprising.

Ruby's lambda is unusual in that choice of parameter names does affect behavior:

 x = 3
 lambda{|x| "x still refers to the outer variable"}.call(4)
 puts x  # x is now 4, not 3

Simply put, a method is also a block of code. However, unlike Procs, methods are not bound to the local variables around them. Rather, they are bound to some object and have access to its instance variables [3]:


 class Boogy
     def initialize
         @dix = 15
     end
 
     def arbo
         puts "#{@dix} ha\n"
     end
 end
 # initializes an instance of Boogy
 b = Boogy.new
 # prints "15 ha"
 b.arbo

A useful idiom when thinking about methods is sending messages. Given a receiver - an object that has some method defined, we can send it a message - by calling the method, optionally providing some arguments. In the example above, calling arbo is akin to sending a message “arbo”, without arguments. Ruby supports the message sending idiom more directly, by including the send method in class Object (which is the parent of all objects in Ruby). So the following two lines are equivalent to the arbo method call:


 # method/message name is given as a string
 b.send("arbo")
 
 # method/message name is given as a symbol
 b.send(:arbo)

Note that methods can also be defined in the “top-level” scope, not inside any class. For example:


 def say (something)
     puts something
 end
 
 say "Hello"

While it seems that say is “free-standing”, it is not. When methods such as this are defined, Ruby silently tucks them into the Object class. But this doesn’t really matter, and for all practical purposes say can be seen as an independent method. Which is, by the way, just what’s called a “function” in some languages (like C and Perl). The following Proc is, in many ways similar:


 say = lambda {|something| puts something}
 
 say.call("Hello")
 # same effect
 say["Hello"]

The [] construct is a synonym to call in the context of Proc [4]. Methods, however, are more versatile than procs and support a very important feature of Ruby, which I will present right after explaining what blocks are.

Block and Proc

[编辑]

程式码区块无法单独存在,只能作为 Ruby 指令或呼叫方法时的引数。Ruby 会主动将程式码区块参数化,程序员仅需利用流程指令 yield 即可将流程转移到被参数化的程式码区块中运行。

我们亦可透过 Proc 引用程式码区块。细节后述。

不论是用程式码区块或是 Proc ,都可以用 Kernel::block_given? 方法判断使用者有无传递程式码区块。

# Block way. 這種用法較常見。
def nP(n)
    if block_given?
        yield n    # yield to black
    else
        puts 'no block'
    end
end

#用例:
nP('hello') {|n| puts n}
nP(10) do |n|
    n.times do
        puts 'a'
    end
end

#Proc way. 細節後述。

def nP(n, &p)
    if block_given?
        p.call n    # call proc p
    else
        puts 'no block'
    end
end

#用例:
nP('hello') {|n| puts n}
nP(10) do |n|
    n.times do
        puts 'a'
    end
end

ECMAScript 只有 Function 类,没有 block 与 proc 的区分。上述的 Ruby 程式,以 ECMAScript 表达如下列所示:

function nP(n, p) {
    return p(n);
}

nP(10, function(n){print(n)});
nP(10,
  function(n) {
    for (var i = 0; i < n; ++i)
      print('a');
  }
);

Block and Method

[编辑]

定义方法时,若参数名称前冠上 & 符号, Ruby 将自动转换程式码区块为 Proc 实例(隐性调用 Proc.new),令其成为具名参数。

def nP(n, &p)
    p.call n
end

def mP(m, p)
    p.call m
end

nP('john') {|name| puts name}

mP('bob', Proc.new {|name| puts name} )
mP('bob', proc {|name| puts name} )  # Kernel::proc

大多数情形,我们只需要传递一段程式 (一个程式码区块) ,所以 Ruby 提供了自动转换程式码区块为 Proc 实例的机制。作为待转换为具名参数的程式码区块引数,必须位于方法定义之参数列的最后一个。

如果要传递多段程式,则不适用上述转换机制。程序员必须明确指示处理动作。

def xP(n, &p1, &p2)  # ERROR!
end

def xP(n, p1, p2)
    p1.call n
    p2.call n
end

xP(5,
    proc {|n| puts n },
    proc {|n| n.times {puts 'a'} }
)

通常应用于动态系结的场合。

class DP
    def initialize(&p)
        @do = p
    end
    def do(n)
        @do.call n
    end
end

d = DP.new {|n| puts n}
d.do 5

The ampersand (&)

[编辑]

Another (IMHO far more efficacious) use of the ampersand is the other-way conversion - converting a Proc into a block. This is very useful because many of Ruby’s great built-ins, and especially the iterators, expect to receive a block as an argument, and sometimes it’s much more convenient to pass them a Proc. The following example is taken right from the excellent “Programming Ruby” book by the pragmatic programmers:


 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(", "))

The collect method expects a block, but in this case it is very convenient to provide it with a Proc, since the Proc is constructed using knowledge gained from the user. The ampersand preceding calc makes sure that the Proc object calc is turned into a code block and is passed to collect as an attached block.

The ampersand also allows the implementation of a very common idiom among Ruby programmers: passing method names into iterators. Assume that I want to convert all words in an Array to upper case. I could do it like this:


 words = %w(Jane, aara, multiko)
 upcase_words = words.map {|x| x.upcase}
 
 p upcase_words

This is nice, and it works, but I feel it’s a little bit too verbose. The upcase method itself should be given to map, without the need for a separate block and the apparently superfluous x argument. Fortunately, as we saw before, Ruby supports the idiom of sending messages to objects, and methods can be referred to by their names, which are implemented as Ruby Symbols. For example:


 p "Erik".send(:upcase)

This, quite literally, says send the message/method upcase to the object “Erik”. This feature can be utilized to implement the map {|x| x.upcase} in an elegant manner, and we’re going to use the ampersand for this ! As I said, when the ampersand is prepended to some Proc in a method call, it converts the Proc to a block. But what if we prepend it not to a Proc, but to another object ? Then, Ruby’s implicit type conversion rules kick in, and the to_proc method is called on the object to try and make a Proc out of it. We can use this to implement to_proc for Symbol and achieve what we want:


 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
 
 # Voila !
 words = %w(Jane, aara, multiko)
 upcase_words = words.map(&:upcase)

迭代器(Iterator)

[编辑]

迭代作动词时指: 反复、重复,对应英文 iterate ;作名词时对应英文 iteration 。根据前述字源,迭代器意为一种会反复做相同工作的事物,对应英文 iterator 。在 Ruby 中,每个迭代器都是一种个体的方法。

语法:

個體.迭代器 do |參數|
   ...程式區塊...
end
個體.送代器 { |參數|
   ...程式區塊...
}

范例:

5.times do |c|
   puts c
end

如上例,times 是一个迭代器(iterator),会做 5 次迭代(iteration)。

撰写程式码时,常有各式各样的情形需要循环处理。在 C 语言中,我们用 forwhile。例如:

for (str = "abcdefg"; *str != '\0'; str++) {
    /* 處理動作 */
}

C 语言的 for(...) 语法帮助我们建立抽象的回圈动作,但仍要求程序员了解字串内部结构的细节,才能写出测试 *str 是否为 null 字元的程式码。这使得 C 语言感觉像是低阶语言。高阶语言应提供更有弹性的迭代。看看下列的 sh 指令手稿的做法:

#!/bin/sh
for i in *.[ch]; do
    # ... 針對每一個檔案做某件事
done

目前目录下所有 C 语言源码及标头档案都会被处理,由命令外层负责一一取得档案名称。这种作法看来比 C 语言高阶多了,不是吗?

但还有需要考量之事: 程式语言能否为我们自定的资料型态提供如同内建资料型态一般的迭代方式?个体导向编程 (OOP) 时,我们时常定义一个又一个的资料型态,所以须认真对待这问题。

为达上述目的,每种个体导向程式语言各有一些方便进行迭代的能力。有些语言为此目的提供了特殊的类别; Ruby 则允许我们直接定义迭代内容。

Ruby 的 String(字串) 型态有些实用的迭代器:

irb> "xyz".each_byte{ |c|
irb*     printf "<%c>", c
irb> }; puts "\n"
<x><y><z>
=> nil

each_byte 是处理字串中每一个字元的迭代器。每个字元都会被代入参数 c 。若以类似 C 语言的程式码来表达,可改写成...

irb> s = "xyz"; i = 0
=> 0
irb> while i < s.length
irb>     printf "<%c>", s[i]
irb>     i+=1
irb> end; puts "\n"
<x><y><z>
=> nil

... 然而, each_byte 迭代器的作法较简单,即使未来大幅改变 String 类别的结构,通常仍可继续运作。迭代的其中一项好处在其有助于对此类变化设计耐用的程式码;一般而言确实是良好的程式编写特性。

另一个有用的字串迭代器是 each_line

irb> "a\nb\nc\n".each_line{ |l|
irb*     print l
irb> }
a
b
c
=> "a\nb\nc\n"

用迭代器比在 C 语言中做同样的事要容易得多。

事实上,Ruby 的 for 叙述就是使用调用个体的 each 迭代器进行反复作业。字串的 eacheach_line 的效果相同,所以上述范例可以 for 改写为:

irb> for l in "a\nb\nc\n"
irb>     print l
irb> end
a
b
c
=> "a\nb\nc\n"

我们可以在回圈中结合 retry 叙述令回圈重新开始工作。

irb> c = 0
=> 0
irb> for i in 0..4
irb>     print i
irb>     if i == 2 and c == 0
irb>         c = 1
irb>         print "\n"
irb>         retry
irb>     end
irb> end; print "\n"
012
01234
=> nil

上述回圈原本应该只执行 5 次迭代,印出 01234 。但途中做了一次 retry ,故令回圈重新自初始状态 (i 为 0) 时开始工作。将上述范例中的 retry 换成 redo ,只会令回圈再做一次现行迭代,其输出内容为:

012
234

此时回圈并未重新开始,仅反复一次 i 为 2 时的工作。

yield

[编辑]

我们可将一个程式码区块传递给迭代器,并于迭代器中以 yield 叙述将程式流程转移至该程式码区块。下列范例定义了名为 repeat 的迭代器,它会依据引数所指定之次数,反复执行程式码区块内容。

irb> def repeat(num)
irb>     while num > 0
irb>         yield
irb>         num -= 1
irb>     end
irb> end
=> nil
irb> repeat(3) { puts "foo" }
foo
foo
foo
=> nil

利用 retry ,我们可定义一个拟似 Ruby 标准 while 叙述作用的迭代器。

irb> def WHILE(cond)
irb>     return if not cond
irb>     yield
irb>     retry
irb> end
=> nil
irb> i = 0; WHILE(i < 3) { print i; i+=1 }
012=> nil
上一项: 控制结构 索引 下一项: 类别