方法

方法实现了你程序的功能。这是一个简单的方法定义

def one_plus_one
  1 + 1
end

方法定义由 def 关键字、方法名、方法体、return 值和 end 关键字组成。调用时,方法将执行方法体。此方法返回 2

自 Ruby 3.0 起,对于只有单个表达式的方法也有了简写语法

def one_plus_one = 1 + 1

本节仅涵盖方法定义。另请参阅 调用方法的语法文档

方法名

方法名可以是运算符之一,或者必须以字母或第八位设置的字符开头。它可以包含字母、数字、_(下划线)或第八位设置的字符。约定使用下划线来分隔多单词方法名中的单词

def method_name
  puts "use underscores to separate words"
end

Ruby 程序必须用 US-ASCII 兼容的字符集编写,例如 UTF-8、ISO-8859-1 等。在这些字符集中,如果第八位被设置,则表示一个扩展字符。Ruby 允许方法名和其他标识符包含此类字符。Ruby 程序不能包含某些字符,如 ASCII NUL (\x00)。

以下是有效 Ruby 方法的示例

def hello
  "hello"
end

def こんにちは
  puts "means hello in Japanese"
end

通常方法名是 US-ASCII 兼容的,因为输入它们的按键在所有键盘上都存在。

方法名可以以 !(感叹号)、?(问号)或 =(等号)结尾。

感叹号方法(方法名末尾的 !)的调用和执行方式与其他任何方法一样。但是,按照约定,带有感叹号的方法被认为是危险的。在 Ruby 的核心库中,危险方法意味着当一个方法以感叹号 (!) 结尾时,表示与非感叹号等效方法相比,它会永久修改其接收者。几乎所有情况下,Ruby 核心库都会为每个不修改接收者的感叹号方法(方法名不以 ! 结尾)提供一个非感叹号的对应项(方法名以 ! 结尾)。此约定对于 Ruby 核心库通常是正确的,但对于其他 Ruby 库可能不一定适用。

按照约定,以问号结尾的方法返回布尔值,但它们不一定只返回 truefalse。通常,它们会返回一个对象来表示真值(或“真值”)。

以等号结尾的方法表示赋值方法。

class C
  def attr
    @attr
  end

  def attr=(val)
    @attr = val
  end
end

c = C.new
c.attr      #=> nil
c.attr = 10 # calls "attr=(10)"
c.attr      #=> 10

不能使用简写语法定义赋值方法。

这些是各种 Ruby 运算符的方法名。每个运算符只接受一个参数。运算符后面是运算符的典型用法或名称。为运算符创建替代含义可能会引起混淆,因为用户期望加号用于相加,减号用于相减等。此外,您无法更改运算符的优先级。

+

add

-

减法

*

乘法

**

/

除法

%

模运算,String#%

&

逻辑与

|

逻辑或

^

异或(exclusive OR)

>>

右移

<<

左移,追加

==

等于

!=

不等于

===

相等性比较。请参阅 Object#===

=~

模式匹配。(不只是针对正则表达式)

!~

不匹配

<=>

比较,又称宇宙飞船运算符。请参阅 Comparable

<

小于

<=

小于等于

>

大于

>=

大于等于

要定义一元减号和一元加号方法,请在运算符后加上 @,如 +@

class C
  def -@
    puts "you inverted this object"
  end
end

obj = C.new

-obj # prints "you inverted this object"

需要 @ 来区分一元减号和加号运算符与二元减号和加号运算符。

您也可以在一元波浪号和非 (!) 方法后跟 @,但这不是必需的,因为没有二元波浪号和非运算符。

一元方法接受零个参数。

此外,还可以定义元素引用和赋值方法:分别是 [][]=。两者都可以接受一个或多个参数,而元素引用可以不接受任何参数。

class C
  def [](a, b)
    puts a + b
  end

  def []=(a, b, c)
    puts a * b + c
  end
end

obj = C.new

obj[2, 3]     # prints "5"
obj[2, 3] = 4 # prints "10"

返回值

默认情况下,方法返回方法体内最后求值过的表达式。在上例中,最后(也是唯一)求值过的表达式是简单的求和 1 + 1return 关键字可用于明确指定方法返回一个值。

def one_plus_one
  return 1 + 1
end

它还可用于在最后表达式求值之前让方法返回。

def two_plus_two
  return 2 + 2
  1 + 1  # this expression is never evaluated
end

请注意,对于赋值方法,在使用赋值语法时将忽略返回值。相反,将返回参数

def a=(value)
  return 1 + value
end

p(self.a = 5) # prints 5

调用方法时将返回实际返回值

p send(:a=, 5) # prints 6

Scope

定义方法的标准语法

def my_method
  # ...
end

将方法添加到类。您可以使用 class 关键字在特定类上定义实例方法

class C
  def my_method
    # ...
  end
end

可以在另一个对象上定义方法。您可以像这样定义“类方法”(在类上定义的方法,而不是类的实例)

class C
  def self.my_method
    # ...
  end
end

然而,这仅仅是 Ruby 更强大的语法能力的特例,即向任何对象添加方法的能��。类是对象,因此添加类方法只是向 Class 对象添加方法。

向对象添加方法的语法如下

greeting = "Hello"

def greeting.broaden
  self + ", world!"
end

greeting.broaden # returns "Hello, world!"

self 是一个关键字,指的是编译器当前正在考虑的对象,这可能使上面在定义类方法时使用 self 更加清晰。事实上,将 hello 方法添加到 String 类中的示例可以重写为

def String.hello
  "Hello, world!"
end

这样定义的方法称为“单例方法”。broaden 只会存在于字符串实例 greeting 上。其他字符串将没有 broaden

覆盖

当 Ruby 遇到 def 关键字时,如果该方法已存在,它不会将其视为错误:它只是重新定义它。这被称为覆盖。与扩展核心类一样,这是一种潜在危险的能力,应谨慎使用,因为它可能导致意外结果。例如,考虑以下 irb 会话

>> "43".to_i
=> 43
>> class String
>>   def to_i
>>     42
>>   end
>> end
=> nil
>> "43".to_i
=> 42

这将有效地破坏任何使用 String#to_i 方法从字符串解析数字的代码。

参数

方法可以接受参数。参数列表跟在方法名后面

def add_one(value)
  value + 1
end

调用时,add_one 方法的用户必须提供一个参数。该参数是方法体中的局部变量。然后方法会将该参数加一并返回值。如果给定 1,此方法将返回 2

参数周围的括号是可选的

def add_one value
  value + 1
end

在简写方法定义中,括号是强制的

# OK
def add_one(value) = value + 1
# SyntaxError
def add_one value = value + 1

多个参数用逗号分隔

def add_values(a, b)
  a + b
end

调用时,必须按确切顺序提供参数。换句话说,参数是位置参数。

默认值

参数可以有默认值

def add_values(a, b = 1)
  a + b
end

默认值不必出现在前面,但带有默认值的参数必须一起分组。这是可以的

def add_values(a = 1, b = 2, c)
  a + b + c
end

这将引发 SyntaxError

def add_values(a = 1, b, c = 1)
  a + b + c
end

默认参数值可以引用已作为局部变量求值的参数,并且参数值始终从左到右求值。所以这是允许的

def add_values(a = 1, b = a)
  a + b
end
add_values
# => 2

但这将引发 NameError(除非定义了一个名为 b 的方法)

def add_values(a = b, b = 1)
  a + b
end
add_values
# NameError (undefined local variable or method `b' for main:Object)

数组 解构

您可以使用参数中的额外括号来解构(解包或提取值)数组

def my_method((a, b))
  p a: a, b: b
end

my_method([1, 2])

这将打印:

{:a=>1, :b=>2}

如果参数中的 数组 有额外的元素,它们将被忽略

def my_method((a, b))
  p a: a, b: b
end

my_method([1, 2, 3])

这与上面的输出相同。

您可以使用 * 来收集剩余的参数。这会将 数组 分为第一个元素和其余部分

def my_method((a, *b))
  p a: a, b: b
end

my_method([1, 2, 3])

这将打印:

{:a=>1, :b=>[2, 3]}

如果参数响应 to_ary,则该参数将被解构。您应该只在可以使用您的对象代替 数组 时定义 to_ary

仅使用内部括号会消耗发送的参数之一。如果参数不是 数组,它将被分配给解构中的第一个参数,并且解构中的其余参数将为 nil

def my_method(a, (b, c), d)
  p a: a, b: b, c: c, d: d
end

my_method(1, 2, 3)

这将打印:

{:a=>1, :b=>2, :c=>nil, :d=>3}

您可以任意嵌套解构

def my_method(((a, b), c))
  # ...
end

数组/哈希参数

在参数前加上 * 会将任何剩余的参数转换为数组

def gather_arguments(*arguments)
  p arguments
end

gather_arguments 1, 2, 3 # prints [1, 2, 3]

数组参数必须出现在任何关键字参数之前。

可以在开头或中间收集参数

def gather_arguments(first_arg, *middle_arguments, last_arg)
  p middle_arguments
end

gather_arguments 1, 2, 3, 4 # prints [2, 3]

如果调用者在所有位置参数之后提供了关键字,则数组参数将捕获一个 哈希 作为最后一项。

def gather_arguments(*arguments)
  p arguments
end

gather_arguments 1, a: 2 # prints [1, {:a=>2}]

但是,这仅在方法未声明任何关键字参数时发生。

def gather_arguments_keyword(*positional, keyword: nil)
 p positional: positional, keyword: keyword
end

gather_arguments_keyword 1, 2, three: 3
#=> raises: unknown keyword: three (ArgumentError)

另外,请注意,裸 * 可用于忽略参数

def ignore_arguments(*)
end

您也可以在调用方法时使用裸 * 来将参数直接传递给另一个方法

def delegate_arguments(*)
  other_method(*)
end

关键字参数

关键字参数类似于带有默认值的位置参数

def add_values(first: 1, second: 2)
  first + second
end

任意关键字参数将使用 ** 接受

def gather_arguments(first: nil, **rest)
  p first, rest
end

gather_arguments first: 1, second: 2, third: 3
# prints 1 then {:second=>2, :third=>3}

调用带有关键字参数的方法时,参数可以按任何顺序出现。如果调用者发送了未知的关键字参数,并且该方法不接受任意关键字参数,则会引发 ArgumentError

要要求特定的关键字参数,请不要为关键字参数包含默认值

def add_values(first:, second:)
  first + second
end
add_values
# ArgumentError (missing keywords: first, second)
add_values(first: 1, second: 2)
# => 3

混合使用关键字参数和位置参数时,所有位置参数必须出现在任何关键字参数之前。

另外,请注意 ** 可用于忽略关键字参数

def ignore_keywords(**)
end

您也可以在调用方法时使用 ** 将关键字参数委托给另一个方法

def delegate_keywords(**)
  other_method(**)
end

要将方法标记为接受关键字,但实际上不接受关键字,您可以使用 **nil

def no_keywords(**nil)
end

调用此类方法时使用关键字或非空关键字 splat 将导致 ArgumentError。此语法受支持,以便以后向方法添加关键字而不会影响向后兼容性。

如果方法定义不接受任何关键字,并且未使用 **nil 语法,则调用方法时提供的任何关键字都将被转换为 哈希 位置参数

def meth(arg)
  arg
end
meth(a: 1)
# => {:a=>1}

块参数

块参数由 & 指示,必须放在最后

def my_method(&my_block)
  my_block.call(self)
end

最常用于将块传递给另一个方法

def each_item(&block)
  @items.each(&block)
end

如果您只打算将块传递给另一个方法而不会对其进行其他操作,则不必为其命名

def each_item(&)
  @items.each(&)
end

如果您只打算调用块而不以其他方式操作它或将其发送到另一个方法,则首选使用没有显式块参数的 yield。此方法等同于本节中的第一个方法

def my_method
  yield self
end

参数转发

自 Ruby 2.7 起,提供了一个全参数转发语法

def concrete_method(*positional_args, **keyword_args, &block)
  [positional_args, keyword_args, block]
end

def forwarding_method(...)
  concrete_method(...)
end

forwarding_method(1, b: 2) { puts 3 }
#=>  [[1], {:b=>2}, #<Proc:...skip...>]

使用转发 ... 的调用仅在定义为 ... 的方法中可用。

def regular_method(arg, **kwarg)
  concrete_method(...) # Syntax error
end

自 Ruby 3.0 起,在定义和调用中都可以有前导参数(但在定义中它们只能是 positional arguments without default values)。

def request(method, path, **headers)
  puts "#{method.upcase} #{path} #{headers}"
end

def get(...)
  request(:GET, ...) # leading argument in invoking
end

get('https://ruby-lang.cn', 'Accept' => 'text/html')
# Prints: GET https://ruby-lang.cn {"Accept"=>"text/html"}

def logged_get(msg, ...) # leading argument in definition
  puts "Invoking #get: #{msg}"
  get(...)
end

logged_get('Ruby site', 'https://ruby-lang.cn')
# Prints:
#   Invoking #get: Ruby site
#   GET https://ruby-lang.cn {}

请注意,省略转发调用中的括号可能会导致意外结果

def log(...)
  puts ...  # This would be treated as `puts()...',
            # i.e. endless range from puts result
end

log("test")
# Prints: warning: ... at EOL, should be parenthesized?
# ...and then empty line

异常 处理

方法具有隐含的异常处理块,因此您无需使用 beginend 来处理异常。这

def my_method
  begin
    # code that may raise an exception
  rescue
    # handle exception
  end
end

可以写成

def my_method
  # code that may raise an exception
rescue
  # handle exception
end

类似地,如果您希望即使在引发异常时也始终运行代码,可以使用 ensure 而不使用 beginend

def my_method
  # code that may raise an exception
ensure
  # code that runs even if previous code raised an exception
end

您还可以将 rescueensure 和/或 else 结合使用,而无需 beginend

def my_method
  # code that may raise an exception
rescue
  # handle exception
else
  # only run if no exception raised above
ensure
  # code that runs even if previous code raised an exception
end

如果您希望只为方法的一部分捕获异常,请使用 beginend。有关更多详细信息,请参阅异常处理页面。