Pr

Proc 与块 属于 Crystal

1 个练习

关于 Proc 与块

块是一段可以传给方法的代码。在其他编程语言中,这被称为匿名函数或 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是一个关键字,用来执行传给方法的块。也就是说,如果没有调用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,需要使用 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 传给方法

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

你也可以从方法声明一个 Proc。

def my_method(a, b)
  a + b
end

my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)
通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 Proc 与块

练习已锁定

再解锁 1 个练习即可练习 Proc 与块