crystal-chinaChina
  • 文档
  • Github
  • 注册
  • 登录
目录
  • 前言
  • 简介
  • 安装 ➤
    • 包管理
  • 写给 Rubyists ➤
    • 类型
    • 方法
    • 代码块
    • 杂项
    • 性能因素(WIP)
    • 迁移 Ruby 代码到 Crystal
  • 基础知识
  • 查找性能瓶颈 (WIP)
  • 交叉编译

代码块

最后编辑于: 2025年03月09日

§ 代码块参数的自动展开(auto expanding)

下面的 Ruby 代码是工作的:

   1  
   2  [[1, "A"], [2, "B"]].each do |a, b|
   3    p a, b
   4  end
   5  
   6  # 1
   7  # "A"
   8  # 2
   9  # "B"
  10  

因为 Enumerable 对象的的元素(这里是一个子数组)作为参数传递给代码块时,会自动根据 代码块参数形参的个数 auto expanding,但是 Crystal 不会自动这样做(这也避免了一些潜在的 bug)

   1  
   2  In 1.cr:1:27
   3  
   4   1 | [[1, "A"], [2, "B"]].each do |a, b|
   5                                 ^
   6  Error: too many block parameters (given 2, expected maximum 1)
   7  

错误消息告诉我们,期望的代码块参数个数是一个,但是我们提供了两个。

正确的做法是将所有参数使用圆括号括起来,其内部会执行 unpack: a, b = [1, "A"]

   1  
   2  [[1, "A"], [2, "B"]].each do |(a, b)|
   3    p a, b
   4  end # => ok
   5  

嵌套的 unpacking 也是可以的。(since 1.10.0)

   1  
   2  ary = [
   3    {1, {2, {3, 4}}} # => A Tuple
   4  ]
   5  
   6  ary.each do |(w, (x, (y, z)))|
   7    w # => 1
   8  
   9    x # => 2
  10    y # => 3
  11    z # => 4
  12  end
  13  

Ruby 里的 Splat 参数也是支持的。

   1  
   2  ary = [
   3    [1, 2, 3, 4, 5],
   4  ]
   5  
   6  ary.each do |(x, *y, z)|
   7    x # => 1
   8    y # => [2, 3, 4]
   9    z # => 5
  10  end
  11  

不过,当 Hash 或 NamedTuple 作为可枚举对象传递键值对给代码块时, 是不需要 unpacking 直接可以工作的(也比较符合直觉)。

   1  
   2  {1 => "A",2 => "B"}.each do |a, b|
   3    p a, b
   4  end
   5  
   6  # 1
   7  # "A"
   8  # 2
   9  # "B"
  10  
   1  
   2  {foo: 1, bar: 2}.each do |a, b|
   3    p a, b
   4  end
   5  
   6  # :foo
   7  # 1
   8  # :bar
   9  # 2
  10  

§ yield(non-captured block) 和 &block(captured block)

在较早版本的 Ruby 中,yield 比 &block 拥有稍微较好的性能,但功能通常不做明确的区分。 在最近版本的 Ruby 中, 两者性能已经没有什么性能差异了,但是如果你使用 Crystal, 必须 明白 yield 和 &block, 两者的实现方式有着根本性的不同。

当使用 yield 的时候,在方法体中,代码块并没有作为一个变量在方法内被引用,即,一定 无法将 block 作为方法返回值返回,我们称其为 non-captured block.

当使用 &block 时,显然,其自身作为一个 Proc 类型的参数在方法中可以直接访问,自身就是 一个针对代码块的引用,并有可能作为方法的返回值被返回,形成闭包,我们称其为 captured block。 作为一个将类型安全放在首位的程序语言,captured block 无法被内联,因此性能不如 yield 好。

下面是使用 yield 的情形:

   1  
   2  def foo(&)
   3    [1,2,3].each do |e|
   4      yield e
   5    end
   6  end
   7  
   8  foo { |x| puts x }
   9  

当使用 --release 模式编译时,代码块中的代码,总是会被 llvm 内联化,实际生成代码 看起来像这个样子:

   1  
   2  def foo
   3    puts 1
   4    puts 2
   5    puts 3
   6  end
   7  
   8  foo
   9  

没有任何性能惩罚!


下面是使用 &block 的例子,编译器无法内联化,因此性能差于使用 yield.

   1  
   2  def foo(&block : Int32 -> Nil)
   3    [1,2,3].each do |e|
   4      block.call(e)
   5    end
   6  end
   7  
   8  foo { |x| puts x }
   9  
💡 小提示

注意:无论你是否在方法中(作为闭包)实际上真正返回(captured)了这个 block,内部 统统按照闭包处理。

💡 小提示

而且,是否是 captured block 与你使用花括号 {} 或者是 do .... end 方式定义无关。

§ 使用 &block 并且存在代码块参数时,必须增加精确的类型声明

正如上面的例子,当你使用 &block 形式,并且 block 接受参数时,则必须指定参数类型 和返回值类型 &block : Int32 -> Nil, 如果具有多个参数,使用逗号分隔。

例如:

   1  
   2  def foo(&block : Int32, Int32 -> Int32)
   3    block.call(1, 2)
   4  end
   5  
   6  p! foo {|x,y| x + y} # => 3
   7  

如果你希望 block 没有返回值(即:返回 nil ), 你可以省略 -> 后面的类型,下面的两种写法是等价的:(他们甚至全局共享同样的 Proc 对象)

   1  
   2  def foo1(&block : Int32 ->)
   3    block
   4  end
   5  
   6  def foo2(&block : Int32 -> Nil)
   7    block
   8  end
   9  
  10  f1 = foo1 {|x| x }
  11  f2 = foo2 {|x| x }
  12  
  13  p! f1,f2
  14  f1 # => #<Proc(Int32, Nil):0x55fa01b79f90>
  15  f2 # => #<Proc(Int32, Nil):0x55fa01b79fb0>
  16  
  17  p! f1.call(1), f2.call(2) # => nil,nil
  18  

如果你接受 block 返回啥类型都可以,使用 _ 作为返回值即可。

   1  
   2  def foo(&block : Int32 -> _)
   3    block
   4  end
   5  
   6  f1 = foo { |x| x + 1 }
   7  p! typeof(f1) # => Proc(Int32, Int32), 这里自动推断返回 Int32
   8  
   9  f2 = foo {|x| x.to_s }
  10  p! typeof(f2) # => Proc(Int32, String), 这里自动推断返回 String
  11  

上面 p! 返回的类型似乎和我们声明的方式不太一样,这其实是另一种更通用,偏内部实现方式 的写法,如果你用过 haskell 这样的函数语言,一定感觉很熟悉,Proc 声明中的最后一个类型 总是代表代码块的返回值,因此是无法省略的。

   1  
   2  def foo(&block : Proc(Int32, Int32, Int32)) # => 最后一个类型代表返回值
   3    block.call(1, 2)
   4  end
   5  
   6  p! foo {|x,y| x + y} # => 3
   7  

§ self 含义

在 Crystal 的代码块中,没有自己的 self, 它和代码块被调用时的上下文共享同样的 self。

例如:下面的代码中,self 总是属于方法 foo。

   1  
   2  class Foo
   3    def foo
   4      p "self in the method: #{self}"
   5      [1].each {|x| p "self in the block: #{self}"}
   6    end
   7  end
   8  
   9  Foo.new.foo
  10  
  11  # => "self in the method: #<Foo:0x7f5f107d4fb0>"
  12  # => "self in the block: #<Foo:0x7f5f107d4fb0>"
  13  

Crystal 中,自然没有类似 instance_eval { ... } 或 instance_exec { ... } 这样的 hack

但是 Crystal 支持通过 with self yield 将 self 传递到代码块中,再通过 Object#itself 来访问。

例如:

   1  
   2  class Adder
   3    getter x : Int32, y : Int32
   4  
   5    def initialize(@x, @y)
   6    end
   7  
   8    def calc(&)
   9      with self yield
  10    end
  11  end
  12  
  13  adder = Adder.new(1, 2)
  14  p! adder.calc { itself.x + itself.y } # => 3
  15  
💡 小提示

&block 不支持 with 语法

§ &. 含义完全不同

Ruby 中,&. 被称作安全调用操作符(Safe Navigation Operator),例如:

   1  
   2  nil.upcase.reverse # => NoMethodError: undefined method `upcase' for nil
   3  nil&.upcase&.reverse # => nil 之上使用 &. 调用,不会报错,而是总是返回 nil
   4  

而 Crystal 中 &. 含义完全不同,我们称其为 block Short one-parameter (invoke) syntax (block 短调用形式), 它是Ruby 中 &: 用法的一个增强替代,

例如,代码块只有一个代码块参数,并且代码块的内部只是在这个参数之上调用一个方法, 此时,我们可以使用 & 代表那个唯一的参数,如果在其之上调用 upcase 方法,直接写做: &.upcase, 例如, 下面的两个用法是等价的。

   1  
   2  ["hello", "world"].map {|x| x.upcase }
   3  ["hello", "world"].map &.upcase
   4  

它看起来和 Ruby 版本的 ["hello", "world"].map &:upcase 相似,但是更强大,因为, 它调用的方法还允许接受自己的参数,甚至允许链式调用。

例如:

   1  
   2  (1..10).map &.**(3) # 等价于:(1..10).map {|x| x.**(3) }, 结果为:[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
   3  ["hello", "world"].map(&.upcase.reverse) # => ["OLLEH", "DLROW"]
   4  

§ return

Crystal 不允许从顶层空间直接 return,仅在方法定义的上下文中才允许 return

这在和 block 一起使用时,尤其注意,例如, 上面的代码最后一行如果使用 return 会报错。

💡 小提示

adder.calc { return itself.x + itself.y } # Error: can't return from top level

previous_page方法

代码块

next_page杂项
欢迎在评论区留下你的见解、问题或建议

正在预览...
正在读取评论...
Crystal China
admin@crystal-china.org
githubtwittercrystal-lang