块和 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#map 和 Enumerable#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)#each 将 Tuple(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
不要害怕为了可读性或代码重用而使用块,它不会影响最终的可执行文件性能。