Ruby 枚举器链接到底是如何工作的?

Jon*_*ara 5 ruby enumerator

考虑以下代码:

[1,2,3].map.with_index { |x, i| x * i }
# => [0,2,6]
Run Code Online (Sandbox Code Playgroud)

这到底是如何运作的?

我的思维模型map是它迭代并在每个元素上应用一个函数。是否with_index以某种方式将函数传递给 enumerator [1,2,3].map,在这种情况下该函数会是什么?

这个SO 线程显示了枚举器如何传递数据,但没有回答问题。事实上,如果你替换map为each那么行为是不同的:

[1,2,3].each.with_index { |x, i| x * i }
# => [1,2,3]
Run Code Online (Sandbox Code Playgroud)

map除了携带要迭代的数据之外,似乎还携带必须应用函数的信息。这是如何运作的?

Sil*_*olo 5

Todd 的回答非常好,但我觉得看到更多 Ruby 代码可能会有所帮助。具体来说,让我们尝试自己写 。eachmapArray

我不会直接使用任何Enumerable或Enumerator方法,所以我们看看它是如何在幕后工作的(我仍然会使用for循环,并且那些技术上#each在幕后调用,但这只是一点作弊)

首先,有each. each简单。它会迭代数组并对每个元素应用一个函数,然后返回原始数组。

def my_each(arr, &block)
  for i in 0..arr.length-1
    block[arr[i]]
  end
  arr
end
Run Code Online (Sandbox Code Playgroud)

够简单的。现在如果我们不通过一个区块怎么办?让我们稍微改变一下以支持这一点。我们实际上想要推迟做事的行为,each以允许Enumerator做它的事情

def my_each(arr, &block)
  if block
    for i in 0..arr.length-1
      block[arr[i]]
    end
    arr
  else
    Enumerator.new do |y|
      my_each(arr) { |*x| y.yield(*x) }
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

因此,如果我们不传递一个块,我们会创建一个Enumerator,当使用时,调用my_each,使用枚举器yield 对象作为块。该y对象是一个有趣的东西,但您可以将其视为基本上是您最终将传入的块。因此,在

my_each([1, 2, 3]).with_index { |x, i| x * i }
Run Code Online (Sandbox Code Playgroud)

想象一下y就像{ |x, i| x * i }比特一样。比这更复杂一点,但这就是想法。

顺便说一句,在 Ruby 2.7 及更高版本上,该Enumerator::Yielder对象有自己的#to_proc,因此如果您使用的是最新的 Ruby 版本,您可以这样做

Enumerator.new do |y|
  my_each(arr, &y)
end
Run Code Online (Sandbox Code Playgroud)

而不是

Enumerator.new do |y|
  my_each(arr) { |*x| y.yield(*x) }
end
Run Code Online (Sandbox Code Playgroud)

现在让我们将这种方法扩展到map. map用块书写很容易。就像each我们积累结果一样。

def my_map(arr, &block)
  result = []
  for i in 0..arr.length-1
    result << block[arr[i]]
  end
  result
end
Run Code Online (Sandbox Code Playgroud)

够简单的。现在如果我们不通过一个区块怎么办?让我们做与我们所做的完全相同的事情my_each。也就是说,我们将创建一个Enumerator,并在其中Enumerator调用my_map.

def my_map(arr, &block)
  if block
    result = []
    for i in 0..arr.length-1
      result << block[arr[i]]
    end
    result
  else
    Enumerator.new do |y|
      my_map(arr) { |*x| y.yield(*x) }
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

现在,它Enumerator知道,每当它最终获得一个块时,它都会my_map在最后使用该块。我们可以看到这两个函数在数组上的实际行为类似于map和eachdo

my_each([1, 2, 3]).with_index { |x, i| x * i } # [1, 2, 3]
my_map ([1, 2, 3]).with_index { |x, i| x * i } # [0, 2, 6]
Run Code Online (Sandbox Code Playgroud)

所以你的直觉很准确

map除了携带要迭代的数据之外,似乎还携带必须应用函数的信息。这是如何运作的?

这正是它的作用。map创建一个who 块知道在最后Enumerator调用,而 则执行相同的操作,但使用. 当然,实际上,出于效率和引导的原因,所有这些都是用 C 实现的,但基本思想仍然存在。mapeacheach