递归块展开错误:在递归时产生了 yield。

3

我想在Crystal中实现Python的os.walk方法。我试图以递归方式进行操作,但是编译器告诉我要注意递归yielding,因为在编译时会无限递归生成代码。这就是我所写的:

def walk(d = @root, &block)
  d = Dir.new(d) if d.is_a?(String)
  dirs, files = d.entries.partition { |s| Dir.exists?(File.join(d.path, s)) }
  if Dir.exists?(d.path)
    yield d.path, dirs, files
    dirs.each do |dir_name|
      # recursively yield
      walk File.join(d.path, dir_name), do |a, b, c|
        yield a, b, c
      end
    end
  end
end
1个回答

5

社区的几位成员在 Gitter 上给了我一些有用的指导,我想在这里分享一下我的经验。答案是,您不能递归地使用 yield,但必须使用 block 变量(稍后会进行解释)。这是我最终得出的结果:

def walk(d = @root, &block : String, Array(String), Array(String) -> )
  d = Dir.new(d) if d.is_a?(String)
  dirs, files = d.children.partition { |s| Dir.exists?(File.join(d.path, s)) }
  block.call(d.path, dirs, files)
  dirs.each do |dir_name|
    walk File.join(d.path, dir_name), &block
  end
end

这里的技巧是,不使用 yield 关键字,而是使用 block.call 代替并转发你的块。这实际上已经在文档中提到了,但有点微妙。编译时,如果有 yield,编译器会在 yield 的位置内联块(就我所知)。当使用 block.call 时,将创建一个函数,这就是我们需要输入块参数的原因。如果不给它类型,block.call 将期望 0 个参数。要通过一些东西,只需以此方法签名中的相似方式进行输入即可。
基于以上解释,当你使用 yield 时,很容易理解为什么不需要向 block 添加类型,而且它也能正常工作。重要的是要理解为什么在一个情况下创建了一个关闭函数,而在另一个情况下编译器内联了你的代码,这就是为什么在 yieldblock.call 之间存在性能差异的原因。

使用Dir#each_child而不是Dir#entries,您无需过滤掉...。此外,在为该路径的条目创建迭代器后调用Dir.exists?是不合逻辑的。 - Johannes Müller
看了一下,Dir#childrenDir#entries 的替代品 - 谢谢!至于 Dir#exists,我不是在检查存在性,而是在检查路径是否为目录。如果有更好的方法,请告诉我,但是看看 Dir#exists 的代码,它似乎只是一个围绕着 File.stat 的包装器,并检查它是否为目录。不知道如何以更便宜的方式检查路径是否为目录。 - mlobl
您不需要检查 d.path 是否存在。如果不存在,Dir.new 已经会抛出异常了。 - Johannes Müller
哦,我现在明白你的意思了(之前看错了行数)。好观点,谢谢。我刚刚更新了。 - mlobl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接