跳至內容

區塊與程序

方法可以接受一段程式碼區塊,該程式碼區塊會使用 yield 關鍵字執行。例如

def twice(&)
  yield
  yield
end

twice do
  puts "Hello!"
end

以上程式會印出 "Hello!" 兩次,每次 yield 一次。

要定義一個接收區塊的方法,只需在其內部使用 yield,編譯器就會知道。您可以宣告一個虛擬的區塊參數,以 & 符號 (&) 為首碼的最後一個參數來使其更加明顯。在上面的範例中,我們這樣做了,使引數匿名(僅寫入 &)。但它可以給定一個名稱

def twice(&block)
  yield
  yield
end

在此範例中,區塊參數名稱並不重要,但在更進階的使用中會很重要。

要叫用方法並傳遞區塊,您可以使用 do ... end{ ... }。所有這些都是等效的

twice() do
  puts "Hello!"
end

twice do
  puts "Hello!"
end

twice { puts "Hello!" }

使用 do ... end{ ... } 之間的差異在於 do ... end 會繫結到最左邊的呼叫,而 { ... } 會繫結到最右邊的呼叫

foo bar do
  something
end

# The above is the same as
foo(bar) do
  something
end

foo bar { something }

# The above is the same as

foo(bar { something })

這樣做的原因是允許使用 do ... end 來建立網域特定語言 (DSL),使其讀起來像簡單的英語

open file "foo.cr" do
  something
end

# Same as:
open(file("foo.cr")) do
  something
end

您不會希望以上內容是

open(file("foo.cr") do
  something
end)

重載

兩個方法,一個產生值而另一個不產生值,會被視為不同的重載,如 重載 一節所述。

Yield 引數

yield 表達式類似於呼叫,可以接收引數。例如

def twice(&)
  yield 1
  yield 2
end

twice do |i|
  puts "Got #{i}"
end

以上會印出 "Got 1" 和 "Got 2"。

也有大括號表示法可用

twice { |i| puts "Got #{i}" }

您可以 yield 許多值

def many(&)
  yield 1, 2, 3
end

many do |x, y, z|
  puts x + y + z
end

# Output: 6

區塊可以指定比產生值的引數更少的參數

def many(&)
  yield 1, 2, 3
end

many do |x, y|
  puts x + y
end

# Output: 3

指定比產生值的引數更多的區塊參數是錯誤的

def twice(&)
  yield
  yield
end

twice do |i| # Error: too many block parameters
end

每個區塊參數都具有該位置中每個 yield 表達式的型別。例如

def some(&)
  yield 1, 'a'
  yield true, "hello"
  yield 2, nil
end

some do |first, second|
  # first is Int32 | Bool
  # second is Char | String | Nil
end

底線 也允許作為區塊參數

def pairs(&)
  yield 1, 2
  yield 2, 4
  yield 3, 6
end

pairs do |_, second|
  print second
end

# Output: 246

單一參數簡短語法

如果區塊只有一個參數並在其上叫用方法,則該區塊可以用簡短語法引數取代。

這個

method do |param|
  param.some_method
end

method { |param| param.some_method }

都可以寫成

method &.some_method

或像這樣

method(&.some_method)

在任一情況下,&.some_method 都是傳遞給 method 的引數。此引數在語法上等同於區塊變體。它只是語法糖,沒有任何效能上的損失。

如果方法有其他必要的引數,則簡短語法引數也應在方法的引數清單中提供。

["a", "b"].join(",", &.upcase)

等同於

["a", "b"].join(",") { |s| s.upcase }

引數也可以與簡短語法引數一起使用

["i", "o"].join(",", &.upcase(Unicode::CaseOptions::Turkic))

也可以叫用運算子

method &.+(2)
method(&.[index])

yield 值

yield 表達式本身具有一個值:區塊的最後一個表達式。例如

def twice(&)
  v1 = yield 1
  puts v1

  v2 = yield 2
  puts v2
end

twice do |i|
  i + 1
end

以上會印出 "2" 和 "3"。

yield 表達式的值主要用於轉換和篩選值。這方面最好的範例是 Enumerable#mapEnumerable#select

ary = [1, 2, 3]
ary.map { |x| x + 1 }         # => [2, 3, 4]
ary.select { |x| x % 2 == 1 } # => [1, 3]

虛擬轉換方法

def transform(value, &)
  yield value
end

transform(1) { |x| x + 1 } # => 2

最後一個表達式的結果是 2,因為 transform 方法的最後一個表達式是 yield,其值是區塊的最後一個表達式。

型別限制

可以使用 &block 語法來限制使用 yield 的方法中區塊的型別。例如

def transform_int(start : Int32, &block : Int32 -> Int32)
  result = yield start
  result * 2
end

transform_int(3) { |x| x + 2 } # => 10
transform_int(3) { |x| "foo" } # Error: expected block to return Int32, not String

break

區塊內的 break 表達式會提早從方法中退出

def thrice(&)
  puts "Before 1"
  yield 1
  puts "Before 2"
  yield 2
  puts "Before 3"
  yield 3
  puts "After 3"
end

thrice do |i|
  if i == 2
    break
  end
end

以上會印出 "Before 1" 和 "Before 2"。thrice 方法由於 break 的關係,沒有執行 puts "Before 3" 表達式。

break 也可以接受引數:這些會變成方法的回傳值。例如

def twice(&)
  yield 1
  yield 2
end

twice { |i| i + 1 }         # => 3
twice { |i| break "hello" } # => "hello"

第一個呼叫的值是 3,因為 twice 方法的最後一個表達式是 yield,它會取得區塊的值。第二個呼叫的值是 "hello",因為已執行 break

如果有條件的 break,則呼叫的回傳值型別會是區塊值的型別和許多 break 的型別的聯集

value = twice do |i|
  if i == 1
    break "hello"
  end
  i + 1
end
value # :: Int32 | String

如果 break 收到許多引數,它們會自動轉換為 Tuple

values = twice { break 1, 2 }
values # => {1, 2}

如果 break 沒有收到引數,則與收到單一 nil 引數相同

value = twice { break }
value # => nil

如果在多個巢狀區塊中使用 break,則只會跳脫最接近的封閉區塊

def foo(&)
  pp "before yield"
  yield
  pp "after yield"
end

foo do
  pp "start foo1"
  foo do
    pp "start foo2"
    break
    pp "end foo2"
  end
  pp "end foo1"
end

# Output:
# "before yield"
# "start foo1"
# "before yield"
# "start foo2"
# "end foo1"
# "after yield"

請注意,您不會取得兩個 "after yield" 也不會取得 "end foo2"

next

區塊內的 next 表達式會提早從區塊 (而非方法) 中退出。例如

def twice(&)
  yield 1
  yield 2
end

twice do |i|
  if i == 1
    puts "Skipping 1"
    next
  end

  puts "Got #{i}"
end

# Output:
# Skipping 1
# Got 2

next 表達式接受引數,這些引數會給出叫用區塊的 yield 表達式的值

def twice(&)
  v1 = yield 1
  puts v1

  v2 = yield 2
  puts v2
end

twice do |i|
  if i == 1
    next 10
  end

  i + 1
end

# Output
# 10
# 3

如果 next 收到許多引數,它們會自動轉換為 Tuple。如果它沒有收到引數,則與收到單一 nil 引數相同。

with ... yield

可以使用 with 關鍵字來修改 yield 表達式,以指定一個物件作為區塊中方法呼叫的預設接收者

class Foo
  def one
    1
  end

  def yield_with_self(&)
    with self yield
  end

  def yield_normally(&)
    yield
  end
end

def one
  "one"
end

Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one }  # => "one"

解包區塊參數

區塊參數可以指定括在括號中的子參數

array = [{1, "one"}, {2, "two"}]
array.each do |(number, word)|
  puts "#{number}: #{word}"
end

以上只是這個的語法糖

array = [{1, "one"}, {2, "two"}]
array.each do |arg|
  number = arg[0]
  word = arg[1]
  puts "#{number}: #{word}"
end

這表示任何以整數回應 [] 的型別都可以在區塊參數中解包。

參數解包可以是巢狀的。

ary = [
  {1, {2, {3, 4}}},
]

ary.each do |(w, (x, (y, z)))|
  w # => 1
  x # => 2
  y # => 3
  z # => 4
end

支援 Splat 參數。

ary = [
  [1, 2, 3, 4, 5],
]

ary.each do |(x, *y, z)|
  x # => 1
  y # => [2, 3, 4]
  z # => 5
end

對於 Tuple 參數,您可以利用自動 Splat,而不需要括號

array = [{1, "one", true}, {2, "two", false}]
array.each do |number, word, bool|
  puts "#{number}: #{word} #{bool}"
end

Hash(K, V)#eachTuple(K, V) 傳遞給區塊,因此迭代鍵值對會使用自動 Splat

h = {"foo" => "bar"}
h.each do |key, value|
  key   # => "foo"
  value # => "bar"
end

效能

當使用帶有 yield 的區塊時,區塊會**永遠**內嵌:不會涉及閉包、呼叫或函式指標。這表示這個

def twice(&)
  yield 1
  yield 2
end

twice do |i|
  puts "Got: #{i}"
end

與寫這個完全相同

i = 1
puts "Got: #{i}"
i = 2
puts "Got: #{i}"

舉例來說,標準函式庫在整數型別中包含了 times 方法,讓你可以這樣寫:

3.times do |i|
  puts i
end

這看起來很炫,但它有像 C 語言的 for 迴圈一樣快嗎?答案是:是的!

這是 Int#times 的定義:

struct Int
  def times(&)
    i = 0
    while i < self
      yield i
      i += 1
    end
  end
end

由於沒有捕捉變數的區塊永遠會被內聯,所以上面的方法調用寫成這樣完全相同

i = 0
while i < 3
  puts i
  i += 1
end

不用害怕為了可讀性或程式碼重複使用而使用區塊,這不會影響最終可執行檔的效能。