第7章 方法

方法是由对象定义的与该对象相关的操作。在Ruby中,对象的所有操作都被封装成方法。

7.1 方法的调用

7.11 简单的方法调用

对象.方法名(参数1, 参数2, ... ,参数n)
不同的方法定义的参数个数和顺序也都不一样,调用方法 时必须按照定义来指定参数。另外,调用方法时 () 是可以省略的。
上面的对象被称为接受者(receiver)。在面向对象的世界中,调用方法被称为“向对象发送消息(message)”,调用的结果就是“对象接收(receive)了消 息”(图 7.1)。也就是说,方法的调用就是把几个参数连同消息一起发送给对象的过程。


图 7.1 将消息发送给对象

7.1.2 带块的方法调用

正如第6章提到的each方法、loop方法,方法本身可以与伴随的块一起被调用。这种与块一起被调用的方法,我们称之为带块的方法。
带块的方法的语法如下:

对象.方法名(参数, ...) do |变量1, 变量2, ...|
    块内容
end

do ~ end 这部分就是所谓的块。除 do ~ end 这一形式外,我们也可以用 {~} 将块改写为其他形式:

对象.方法名(参数, ...) {|变量1, 变量2, ...|
    块内容
}

备注 使用 do ~ end 时,可以省略把参数列表括起来的 ()。使用 { ~ } 时,只有在没有参数的时候才可以省略 (),有一个以上的参数时就不能省略。

7.1.3 运算符形式的方法调用

Ruby 中有些方法看起来很像运算符。四则运算等的二元运算符、-(负号)等的一元运算符、指定数组、散列的元素下标的 [] 等,实际上都是方法。

  • obj + arg1
  • obj =~ arg1
  • -obj
  • !obj
  • obj[arg1]
  • obj[arg1] = arg2

7.2 方法的分类

根据接受者种类的不同,Ruby的方法可以分为3类:

  • 实例方法
  • 类方法
  • 函数式方法

7.2.1 实例方法

实例方法是最常用的方法。假设有一个对象(实例),那么以这个对象为接收者的方法就被称为实例方法。
下面是实例方法的一些例子:

p "10, 20, 30, 40".split(",") #=> ["10", "20", "30", "40"] 
p [1, 2, 3, 4].index(2) #=> 1 
p 1000.to_s #=> "1000"

从上到下,分别以字符串、数组、数值对象为接收者。
对象能使用的实例方法是由对象所属的类决定的。调用对象的实例方法后,程序就会执行对象所属类预先定义好的处理。
虽然相同名称的方法执行的处理大多都是一样的,但具体执行的内容则会根据对象类型的不同而存在差异。例如,几乎所有的对象都有 to_s 方法,这是一 个把对象内容以字符串形式输出的方法。然而,虽然都是字符串形式,但在数值对象与时间对象的情况下,字符串形式以及字符串的创建方法却都不一样。

p 10.to_s #=> "10" 
p Time.now.to_s #=> "2013-03-18 03:17:02 +900"

7.2.2 类方法

接收者不是对象而是类本身的方法,我们称之为类方法。例如,我们在创建对象的时候就用到了类方法。

Array.new # 创建新的数组 
File.open("some_file") # 创建新的文件对象 
Time.now # 创建新的 Time 对象

此外,不直接对实例进行操作,只是对该实例所属的类进行相关操作时,我们也会用到类方法。例如,修改文件名的时候,我们就会使用文件类的类方法。

File.rename(oldname, newname) # 修改文件名

调用类方法时,可以使用 :: 代替 .。在 Ruby 语法中,两者所代表的意思是一样的。 关于类方法,我们会在第 8 章中再进行详细的说明。

7.2.3 函数式方法

没有接收者的方法,我们称之为函数式方法。
虽说是没有接收者,但并不表示该方法就真的没有可作为接收者的对象,只是在函数式方法这个特殊情况下,可以省略接收者而已。

print "hello!" # 在命令行输出字符串 
sleep(10) # 在指定的时间内睡眠,终止程序

函数式方法的执行结果,不会根据接收者的状态而发生变化。程序在执行 print 方法以及 sleep 方法的时候,并不需要知道接收者是谁。反过来说,不需要 接收者的方法就是函数式方法。

方法的标记法

接下来,我们来介绍一下 Ruby 帮助文档中方法名的标记方法。标记某个类的实例方法时,就像 Array#each、Array#inject 这样,可以采用以下标记方法:
类名 # 方法名
类类名名 # 方方法法名名 请注意,这只是写帮助文档或者说明时使用的标记方法,程序里面这么写是会出错的。 另外,类方法的标记方法,就像 Array.new 或者 Array::new 这样,可以采用下面两种写法:
类名.方法名
类名::方法名
这和实际的程序语法是一致的。

7.3 方法的定义

定义方法的一般语法如下:

def 方法名(参数1, 参数2, ...)
    希望执行的处理
end

方法名可由英文字母、数字、下划线组成,但不能以数字开头。

代码清单 7.3 hello_with_default.rb

def hello(name)
    puts "Hello, #{name}"
end

hello("Ruby")

通过 hello 方法中的 name 变量,我们就可以引用执行时传进来的参数。该程序的参数为字符串 Ruby,执行结果如下:
执行示例:

> ruby hello_with_name.rb 
Hello, Ruby.

也可以指定默认值给参数(代码清单 7.3)。参数的默认值,是指在没有指定参数的情况下调用方法时,程序默认使用的值。定义方法时,用参数名 = 值这 样的写法定义默认值。

代码清单 7.3 hello_with_default.rb

def hello(name="Ruby")
    puts "Hello, #{name}"
end

hello()
hello("Bob")

执行示例:

ruby hello_with_default.rb
Hello, Ruby.
Hello,Bob.

方法有多个参数时,从参数列表的右边开始依次指定默认值。例如,希望省略三个参数中的两个时,就可以指定右侧两个参数为默认值。

def func(a, b=1, c=3)
 ┊ 
end

请注意只省略左边的参数或者中间的某个参数是不行的。

7.3.1 方法的返回值

我们可以用return指定方法的返回值。
return 值
下面我们来看看求立方体体积的例子。参数 x、y、z 分别代表长、宽、高。x * y * z 的结果就是方法的返回值。

def volume(x, y, z) 
    return x * y * z 
end p volume(2, 3, 4) #=> 24 
p volume(10, 20, 30) #=> 6000

可以省略 return 语句,这时方法的最后一个表达式的结果就会成为方法的返回值。下面我们再通过求立方体的表面积这个例子,来看看如何省略 return。 这里,area 方法的最后一行的 (xy + yz + zx) * 2 的结果就是方法的返回值。

def area(x, y, z)
    xy = x * y
    yz = y * z
    zx = z * x
    (xy + yz + zx) * 2
end

p area(2, 3, 4)
p area(10, 20, 30)

方法的返回值,也不一定是程序最后一行的执行结果。例如,在下面的程序中,比较两个值的大小,并返回较大的值。if 语句的结果就是方法的返回值,即 a > b 的结果为真时,a 为返回值;结果为假时,则 b 为返回值。

def max(a,b)
    if a > b
        a
    else
        b
    end
end

p max(10, 5) #=> 10

因为可以省略,所以有些人就会感觉好像没什么机会用到 return,但是有些情况下我们会希望马上终止程序,这时 return 就派上用场了。用 return 语句 改写的 max 方法如下所示,大家可以对比一下和之前有什么异同。

def max(a,b)
    if a > b
        return a
    end
    return b       # 这里的return 可以省略
end

p max(10, 5) #=> 10

print 方法只输出参数的内容,返回值为 nil。

p print("1:") #=> 1:nil 
# (显示print 方法的输出结果1: 与p 方法的输出结果nil)

7.3.2 定义带块的方法

之前我们已经介绍过带块的方法,下面就来介绍一下如何定义带块的方法。首先我们来实现 myloop 方法,它与利用块实现循环的 loop 方法的功能是一样的。

代码清单 7.4 myloop.rb

def myloop
    while true
        yield
    end
end

num = 1
myloop do
    puts "num is #{num}"
    break if num > 100
    num *= 2
end

这里第一次出现了 yield,yield 是定义带块的方法时最重要的关键字。调用方法时通过块传进来的处理会在 yield 定义的地方执行。 执行该程序后,num 的值就会像 1、2、4、8、16……这样 2 倍地增长下去,直到超过 100 时程序跳出 myloop 方法。

> ruby myloop.rb 
num is 1 
num is 2 
num is 4 
num is 8 
num is 16 
num is 32 
num is 64 
num is 128

本例的程序中没有参数,如果 yield 部分有参数,程序就会将其当作块变量传到块里。块里面最后的表达式的值既是块的执行结果,同时也可以作为 yield 的返回值在块的外部使用。
关于带块的方法的使用方法,我们会在第 11 章中再详细说明。

7.3.3 参数个数不确定的方法

像下面的例子那样,通过用“* 变量名”的形式来定义参数个数不确定的方法,Ruby 就可以把所有参数封装为数组,供方法内部使用。

def foo(*args) 
    args 
end 

p foo(1, 2, 3) #=> [1, 2, 3]

至少需要指定一个参数的方法可以像下面这样定义:

def meth(arg, *agrs) 
    [arg, args] 
end 

p meth(1) #=> [1, []] 
p meth(1, 2, 3) #=> [1, [2, 3]]

所有不确定的参数都被作为数组赋值给变量 args。“* 变量名”这种形式的参数,只能在方法定义的参数列表中出现一次。只确定首个和最后一个参数名,并 省略中间的参数时,可以像下面这样定义:

def a(a, *b, c) 
    [a, b, c] 
end 

p a(1, 2, 3, 4, 5) #=> [1, [2, 3, 4], 5] 
p a(1, 2) #=> [1, [], 2]

7.3.4 关键字参数

关键字参数是Ruby2.0中的新特性。
在目前未知介绍过的方法定义中,我们都需要定义调用方法时的参数个数以及调用顺序。而使用关键字参数,就可以将参数名与参数值成对地传给方法内部
使用。
使用关键字参数定义方法的语法如下所示:

def 方法名(参数1:参数1的值, 参数2:参数2的值, ...)
    希望执行的处理
end

除了参数名外,使用“参数名 : 值”这样的形式还可以指定参数的默认值。用关键字参数改写计算立方体表面积的 area 方法的程序如下所示:

def area(x: 0, y: 0, z:0)
    xy = x * y
    yz = y * z
    zx = z * x
    (xy + yz + zx) * 2
end

p area(x: 2,y: 3,z: 4)
p area(z: 4,y: 3,x: 2)
p area(x: 2,z: 3 )

这个方法有参数 x、y、z,各自的默认值都为 0。调用该方法时,可以像 x: 2 这样,指定一对实际的参数名和值。在用关键字参数定义的方法中,每个参 数都指定了默认值,因此可以省略任何一个。而且,由于调用方法时也会把参数名传给方法,所以参数顺序可以自由地更改。
不过,如果把未定义的参数名传给方法,程序就会报错,如下所示:

area2(foo: 0) #=> 错误:unknown keyword: foo(ArgumentError)

为了避免调用方法时因指定了未定义的参数而报错,我们可以使用“** 变量名”的形式来 接收未定义的参数。下面这个例子的方法中,除了关键字参数 x、y、z 外,还定义了 **arg 参数。参数 arg 会把参数列表以外的关键字参数以散列对象的形式保存。

def meth(x: 0, y: 0, z: 0, **args) 
    [x, y, z, args] 
end 
p meth(z: 4, y: 3, x: 2) #=> [2, 3, 4, {}] 
p meth(x: 2, z: 3, v: 4, w: 5) #=> [2, 0, 3, {:v=>4, :w=>5}]
  • 关键字参数与普通参数的搭配使用
def func(a, b: 1,c: 2)
    ┊
end

上述这样定义时,a 为必须指定的普通参数,b、c 为关键字参数。调用该方法时,可以像下面这样,首先指定普通参数,然后是关键字参数。

func(1, b: 2, c: 3)
  • 用散列传递参数
    调用用关键字参数定义的方法时,可以使用以符号作为键的散列来传递参数。这样一来,程序就会检查散列的键与定义的参数名是否一致,并将与散列 的键一致的参数名传递给方法。
def area2(x: 0, y: 0, z: 0) 
    xy = x * y 
    yz = y * z 
    zx = z * x (xy + yz + zx ) * 2 
end 

args1 = {x: 2, y: 3, z: 4} 
p area2(args1) #=> 52 

args2 = {x: 2, z: 3} #=> 省略y 
p area2(args2) #=> 12

7.3.5 关于方法调用的一些补充

  • 把数组分解为参数
    将参数传递给方法时,我们也可以先分解数组,然后再将分解后的数组元素作为参数传递给方法。在调用方法时,如果以“* 数组”这样的形式指定参 数,这时传递给方法的就不是数组本身,而是数组的各元素被按照顺序传递给方法。但需要注意的是,数组的元素个数必须要和方法定义的参数个数一 样。
def foo(a, b, c) 
    a + b + c 
 end 
p foo(1, 2, 3) #=> 6 

args1 = [2, 3] 
p foo(1, *args1) #=> 6 

args2 = [1, 2, 3] 
p foo(*args2) #=> 6
  • 把散列作为参数传递
    我们用 { ~ } 这样的形式来表示散列的字面量(literal)。将散列的字面量作为参数传递给方法时可以省略 {}。
def foo(arg) 
    arg 
end 

p foo({"a"=>1, "b"=>2}) #=> {"a"=>1, "b"=>2} 
p foo("a"=>1, "b"=>2) #=> {"a"=>1, "b"=>2} 
p foo(a: 1, b:2) #=> {:a=>1, :b=>2}

当虽然有多个参数,但只将散列作为最后一个参数传递给方法时,可以使用下面的写法:

def bar(arg1, arg2) 
    [arg1, arg2] 
end 
p bar(100, {"a"=>1, "b"=>2}) #=> [100, {"a"=>1, "b"=>2}] 
p bar(100, "a"=>1, "b"=>2) #=> [100, {"a"=>1, "b"=>2}] 
p bar(100, a: 1, b: 2) #=> [100, {:a=>1, :b=>2}]

你可能感兴趣的:(第7章 方法)