跳到内容

块和 Proc

方法可以接受一个代码块,该代码块使用 yield 关键字执行。例如

def twice(&)
  yield
  yield
end

twice do
  puts "Hello!"
end

上面的程序打印了 "Hello!" 两次,每次打印一次 yield

要定义接收块的方法,只需在其中使用 yield,编译器就会知道。您可以通过声明一个虚拟块参数来使这一点更加明显,该参数表示为以 ampersand (&) 为前缀的最后一个参数。在上面的示例中,我们做到了这一点,使参数匿名(只写 &)。但它可以被赋予一个名称

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 表达式类似于一个调用,可以接收参数。例如

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

每个块参数都具有该位置上每个产量表达式的类型。例如

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 表达式本身有一个值:块的最后一个表达式。例如

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,它的值是块的最后一个表达式。

类型限制

使用 yield 的方法中的块类型可以使用 &block 语法进行限制。例如

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 方法没有执行 puts "Before 3" 表达式,因为执行了 break

break 也可以接受参数:这些参数将成为方法的返回值。例如

def twice(&)
  yield 1
  yield 2
end

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

第一个调用的值为 3,因为 twice 方法的最后一个表达式是 yield,它获取块的值。第二个调用的值为 "hello",因为执行了 break

如果有条件断点,则调用的返回值类型将是块的值类型和多个 break 的类型之间的联合

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

如果 break 接收多个参数,它们将自动转换为 元组

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 接收多个参数,它们将自动转换为 元组。如果它没有接收参数,则它与接收单个 nil 参数相同。

with ... 产量

可以使用 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

支持散列参数。

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

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

对于 元组 参数,您可以利用自动散列功能,无需使用括号

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

Hash(K, V)#eachTuple(K, V) 传递给块,因此使用自动展开迭代键值对。

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 循环一样快吗?答案是:是的!

这是 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

不要害怕为了可读性或代码重用而使用块,它不会影响最终的可执行文件性能。