方法

方法實作程式碼的功能。以下是簡單的方法定義

def one_plus_one
  1 + 1
end

方法定義包含 def 關鍵字、方法名稱、方法主體、return 值和 end 關鍵字。呼叫方法時,將執行方法主體。此方法傳回 2

自 Ruby 3.0 起,也有一個簡寫語法,適用於僅包含一個表達式的方法

def one_plus_one = 1 + 1

此部分僅涵蓋定義方法。另請參閱 呼叫方法的語法文件

Method 名稱

Method 名稱可以是其中一個運算子,或必須從字母或第八位元組設定為 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 相容,因為鍵盤上都有按鍵可以輸入這些字元。

Method 名稱可以用 ! (驚嘆號)、? (問號) 或 = (等號) 結尾。

驚嘆號方法 (方法名稱結尾的 !) 的呼叫和執行方式與任何其他方法相同。然而,根據慣例,帶有驚嘆號的方法被視為危險的。在 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 算子的方法名稱。每個算子只接受一個引數。算子後方是算子的典型用途或名稱。為算子建立替代意義可能會造成混淆,因為使用者預期加號用來加總、減號用來減法等。此外,您無法變更算子的優先順序。

+

加法

-

減法

*

乘法

**

次方

/

除法

%

模數除法,String#%

&

AND

^

XOR (異或)

>>

右移

<<

左移,附加

==

相等

!=

不相等

===

大小寫相等。請參閱 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

範圍

定義函式的標準語法

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)

Array 分解

您可以使用引數中的額外括號分解(解壓縮或從中提取值)Array

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

my_method([1, 2])

這會列印

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

如果引數在 Array 中有額外的元素,它們將被忽略

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

my_method([1, 2, 3])

這與上述輸出相同。

您可以使用 * 來收集其餘引數。這將 Array 拆分為第一個元素和其餘部分

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

my_method([1, 2, 3])

這會列印

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

如果引數回應 to_ary,則將分解引數。您只應在可以將物件用於 Array 的位置時定義 to_ary。

使用內部括號只使用一個已傳送的引數。如果引數不是 Array,它將被指定給分解中的第一個引數,並且分解中的其餘引數將為 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

使用關鍵字或非空關鍵字展開呼叫此類方法會導致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 起,定義和呼叫中 ... 之前可以有前導引數(但在定義中,它們只能是沒有預設值的定位引數)。

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

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

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

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

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

請注意,在轉送呼叫中省略括號可能會導致意外結果

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

Exception 處理

方法有一個隱含的例外處理區塊,因此你不需要使用 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。有關更多詳細資訊,請參閱 例外處理 頁面。