块是一段可以传给方法的代码。在其他编程语言中,这被称为匿名函数或 lambda。方法可以把块转换成 Proc,后者代表一段代码。
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
块和 Proc 在 Crystal 中非常常用,比如用于迭代和回调。不过它们也常用来创建 DSL(领域特定语言)。DSL 的例子包括 Web 框架 Kemal 和测试框架 Spec(你提交的 Crystal 解答就是用后者来测试的)。
要让方法能够接收块,需要在最后一个形参前加一个和号(&)。这样会把块转换成 Proc,并把它赋给该形参。之后用yield就会执行传给方法的块。
def my_method(&block)
1 + yield
end
传块有两种方式:用do ... end关键字,或者用花括号({})。两者的主要区别是优先级:do ... end支持多行的块,而花括号({})不支持。
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield是一个关键字,用来执行传给方法的块。也就是说,如果没有调用yield,块就永远不会被执行。它还可以接收实参,这些实参会传递给块。可以传给块的实参数量没有上限。要在块中使用这些实参,需要用到|x, y, ...|语法。其中 x、y 等就是这些形参的名字。
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
在上面的例子中,块收到的实参是2,块把它乘以3并返回6。然后方法在结果上加1并返回7。
如果传给yield的实参数量多于块所期望的形参数量,就会抛出错误。
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
块可以显式书写,也可以隐式书写。隐式块最常见,上面的例子中写的就是隐式块。你甚至可以给块传入不同类型的实参,编译器会推断出这些类型,并创建一个联合类型。
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
书写显式块时,可以指定形参的类型和返回类型。这要用到Arg1Type, Arg2Type, ... -> ResultType语法。其中各个形参用逗号(,)分隔,结果类型则用->分隔。
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
和其他显式类型一样,编译器会强制要求类型符合预期。如果块与方法指定的类型不匹配,就会抛出错误。
块有一种简写形式,适用于只接收一个形参、并且只对它调用一个方法的块。这时不用写|x|,而是在形参名前加&。
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
Proc与块有着非常紧密的联系。关键区别在于,Proc是一种类型,而块不是。Proc表示一个函数指针,可以像变量一样传递。
要定义Proc,需要使用 proc 字面量语法->(args) { ... }。各个形参用逗号(,)分隔,块体用{ ... }分隔。要调用 Proc,需要使用call方法,并把实参传给它。形参的类型是必须写明的。返回类型会从 proc 的主体推断出来,也可以显式给出。
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
捕获块是一种从块创建Proc的方式。做法是在方法的最后一个形参前使用&运算符。这样会把块转换成Proc,并赋给该形参。
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Proc 可以传给期望接收块的方法。当你想根据某些条件传入不同的块时,这会很方便。
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
声明变量时,可以用: Type语法来声明变量的类型。对于 proc,要使用Proc((ArgType, ...), ResultType)语法。最后一个类型始终是返回类型,其余的都是形参类型。也就是说,如果只有一个类型,那它就是返回类型,这个 proc 不接收任何形参。
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
你也可以从方法声明一个 Proc。
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)