💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# **第 7 章 方法** 方法是由对象定义的与该对象相关的操作。在 Ruby 中,对象的所有操作都被封装成方法。 ### **7.1 方法的调用** 首先,让我们温习一下如何调用方法。 ### **7.1.1 简单的方法调用** 调用方法的语法如下所示: **对象. 方法名( 参数 `1`, 参数 `2`, … , 参数 `n` )** 以对象开头,中间隔着句点,后面接着是方法名,方法名后面是一排并列的用 `()` 括起来的参数。不同的方法定义的参数个数和顺序也都不一样,调用方法时必须按照定义来指定参数。另外,调用方法时 `()` 是可以省略的。 上面的对象被称为接收者(receiver)。在面向对象的世界中,调用方法被称为“向对象发送消息(message)”,调用的结果就是“对象接收(receive)了消息”(图 7.1)。也就是说,方法的调用就是把几个参数连同消息一起发送给对象的过程。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01dfdd5bb.png) **图 7.1 将消息发送给对象** ### **7.1.2 带块的方法调用** 正如第 6 章提到的 `each` 方法、`loop` 方法,方法本身可以与伴随的块一起被调用。这种与块一起被调用的方法,我们称之为带块的方法。 带块的方法的语法如下: **对象. 方法名( 参数, …) `do` | 变量 `1`, 变量 `2`, …|  块内容 `end`** `do` ~ `end` 这部分就是所谓的块。除 `do ~ end` 这一形式外,我们也可以用 `{~}` 将块改写为其他形式: **对象. 方法名( 参数, …) { | 变量 `1`, 变量 `2`, …|  块内容 }** > **备注** 使用 `do ~ end` 时,可以省略把参数列表括起来的 `()`。使用 `{ ~ }` 时,只有在没有参数的时候才可以省略 `()`,有一个以上的参数时就不能省略。 在块开头的 `| ~ |` 部分中指定的变量称为块变量。在执行块的时候,块变量由方法传到块内部。不同的方法对应的块变量的个数、值也都不一样。之前介绍过的 `times` 方法有一个块变量,执行块时,方法会从 0 开始依次把循环次数赋值给块变量(代码清单 7.1)。 **代码清单 7.1 times_with_param.rb** ~~~ 5.times do |i| puts "第#{i} 次循环。" end ~~~ > **执行示例** ~~~ > ruby times_with_param.rb 第0 次循环。 第1 次循环。 第2 次循环。 第3 次循环。 第4 次循环。 ~~~ ### **7.1.3 运算符形式的方法调用** Ruby 中有些方法看起来很像运算符。四则运算等的二元运算符、`-`(负号)等的一元运算符、指定数组、散列的元素下标的 `[]` 等,实际上都是方法。 - **`obj + arg1`** - **`obj =~ arg1`** - **`-obj`** - **`!obj`** - **`obj[arg1]`** - **`obj[arg1] = arg2`** 这些虽然与一般的方法调用的语法有点不一样,但我们可以将 `obj` 理解为接收者,将 `arg1`、`arg2` 理解为参数,这样一来,它们就又是我们所熟知的方法了。我们可以自由定义这种运算符形式的方法。 > **备注** 在用方法实现的运算符中,有的可以自由地重新定义,有的则不能改变默认的定义。关于运算符,我们会在第 9 章中再详细说明。 ### **7.2 方法的分类** 根据接收者种类的不同,Ruby 的方法可分为 3 类: - **实例方法** - **类方法** - **函数式方法** 下面,我们就来说明一下这 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) # 修改文件名 ~~~ 类方法也有运算符的形式。 ~~~ 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.2 hello_with_name.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("Newbie") # 指定参数的调用 ~~~ > **执行示例** ~~~ > ruby hello_with_default.rb Hello, Ruby. Hello, Newbie. ~~~ 方法有多个参数时,从参数列表的右边开始依次指定默认值。例如,希望省略三个参数中的两个时,就可以指定右侧两个参数为默认值。 ~~~ 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) #=> 52 p area(10, 20, 30) #=> 2200 ~~~ 方法的返回值,也不一定是程序最后一行的执行结果。例如,在下面的程序中,比较两个值的大小,并返回较大的值。`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 ~~~ 如果省略 `return` 的参数,程序则返回 `nil`。方法的目的是程序处理,所以 Ruby 允许没有返回值的方法。Ruby 中有很多返回值为 `nil` 的方法,第 1 章中介绍的 `print` 方法就是其中一。 `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 # 初始化num myloop do puts "num is #{num}" # 输出num break if num > 100 # num 超过 100 时跳出循环 num *= 2 # 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 关键字参数** 关键字参数是 Ruby 2.0 中的新特性。 在目前为止介绍过的方法定义中,我们都需要定义调用方法时的参数个数以及调用顺序。而使用关键字参数,就可以将参数名与参数值成对地传给方法内部使用。 使用关键字参数定义方法的语法如下所示: **`def` 方法名(参数 `1:` 参数 `1` 的值, 参数 `2:` 参数 `2` 的值, …)  希望执行的处理 `end`** 除了参数名外,使用“参数名 : 值”这样的形式还可以指定参数的默认值。用关键字参数改写计算立方体表面积的 `area` 方法的程序如下所示: ~~~ def area2(x: 0, y: 0, z: 0) xy = x * y yz = y * z zx = z * x (xy + yz + zx ) * 2 end p area2(x: 2, y: 3, z: 4) #=> 52 p area2(z: 4, y: 3, x: 2) #=> 52 (改变参数的顺序) p area2(x: 2, z: 3) #=> 12 (省略y) ~~~ 这个方法有参数 `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}] ~~~ 第 3 种形式是把符号作为键的散列传递给方法,与使用关键字参数调用方法的形式一模一样。其实,关键字参数就是模仿这种将散列作为参数传递的写法而设计出来的。使用关键字参数定义方法,既可以对键进行限制,又可以定义参数的默认值。因此建议大家在实际编写程序的时候多尝试使用关键字参数。 > **专栏** > **如何书写简明易懂的程序** > 程序不只是为了让计算机理解、执行而存在的,还要能便于人们读写。即使是实现相同功能的程序,有的可能通俗易懂,有的却晦涩难懂。程序是否易懂,除了与程序的设计和架构有关外,程序的外观也起着很重要的作用。而通过注意下面列举的 3 点,就可以使程序变得更漂亮。 > - > **换行和;** > - > **缩进(`indent`)** > - > **空白** > 下面,我们就依次来看看它们到底有什么魔法可以使我们的程序变漂亮。 > - > **换行和`;`** > Ruby 语法的特征之一就是换行可作为语句结束的标志使用。 > 除了可以使用换行表示语句结束外,我们还可以使用 `;`。这样一来,一行程序里就可以写多条语句,例如, ~~~ str = "hello"; print str ~~~ > 这样的写法与下面的写法具有一样的效果。 ~~~ str = "hello" print str ~~~ > 使用该语法时,把换行看作是一种自然的语句间隔,会更加便于我们读写程序。比起把多个操作都写在一行里,适当的换行是书写简明易懂的程序的第一步。 > 然而,过多地使用 `;`,往往会使程序变得难以读懂。因此,在使用 `;` 之前,应问问自己是不是非使用不可。经过仔细的考量,如果觉得使用后会使程序变得易懂的话再使用也不迟。顺便说一下,笔者平时也很少会用到 `;`。 > - > **缩进** > 缩进,也就是使文字“后退”。这是在程序行的开头输入适当的空白字符来强调程序整体感的一种书写方法。在本书中,我们用两个空白表示一个缩进1。 > 在下面的例子中,为了表示 `print` 方法的那两行程序是 `if ~ end` 的内部处理,程序进行了缩进。 > if a == 1 ~~~ print message1 print message2 ~~~ > end > 插入循环等的时候,会使用更深的缩进。这样一来,语句和循环的对应关系就会变得一目了然。 > while a < 100 ~~~ while b < 20 b = b + 1 print b end a = a + 1 print a ~~~ > end > 下面列举了一些需要使用缩进的情况。 > - > **条件分支** > if a > 0 ~~~ some_metod() ~~~ > else ~~~ other_metod() ~~~ > end > - > **循环** > while i < 10 ~~~ method() i = i - 1 ~~~ > end > - > **块** > some_value.each do |i| ~~~ i.method() ~~~ > end > - > **方法、类等的定义** > def foo ~~~ print "hello" ~~~ > end > 使用缩进时,需要遵守以下事项。 > - > **不要突然使用缩进** > x=10 y=20 ~~~ z=30 # 不好的例子 ~~~ > - > **确保缩进的整齐** > if (foo) ~~~ if (bar) if (buz) # 缩进得太多了 end end ~~~ > end > - > **空白** > 空白存在于程序的各个角落。使用空白时,我们需要注意以下事项。 > - > **确保空白长度整齐,保持良好的平衡感** > 特别是,如果在运算符前后使用的空白长度不一样,程序就很可能出现莫名其妙的错误。例如,计算 `a` 加 `b` 时,不同的空白写法,得到的可能是完全不一样的结果。 > `a+b`    ○好的写法 > `a + b`  ○好的写法 > `a +b`   ×不好的写法 > `a+ b`   ×不好的写法 > `a +b` 表示调用参数为 `+b` 的方法 `a`,整个表达式容易被误认为 `a(+b)`,因此不是好的写法。可见,在 `+` 前后书写空白时,要确保平衡。 > - > **良好的编码风格** > 养成良好的编码风格,首先应该从参考其他人的 Ruby 程序,并模仿他们的写法开始。编写程序的技巧也好,良好的编码风格也好,想要熟练掌握,都要大量阅读其他人写的程序。2 1这也是 Ruby 社区里面一个约定俗成的规矩。——译者注 2现在在 Ruby 社区最流行的 Web 框架 Rails 就是不错的参考教材之一。——译者注