如何解决Ruby Enumerators 链接究竟是如何工作的?
考虑以下代码:
[1,2,3].map.with_index { |x,i| x * i }
# => [0,6]
这究竟是如何工作的?
我对 map
的心智模型是它迭代并在每个元素上应用一个函数。 with_index
是否以某种方式将函数传递给枚举器 [1,3].map
,在这种情况下,该函数是什么?
这个 SO thread 显示了枚举器如何传递数据,但没有回答问题。实际上,如果您将 map
替换为 each
,则行为会有所不同:
[1,3].each.with_index { |x,i| x * i }
# => [1,3]
map
似乎携带了必须应用函数的信息,除了携带要迭代的数据之外。这是如何运作的?
解决方法
Todd 的回答非常好,但我觉得看到更多 Ruby 代码可能会有所帮助。具体来说,让我们尝试在 each
上写 map
和 Array
。
我不会直接使用任何 Enumerable
或 Enumerator
方法,所以我们会看到它是如何在幕后工作的(我仍然会使用 for
循环,并且那些技术上调用#each
在幕后,但这只是作弊)
首先,有 each
。 each
很简单。它遍历数组并对每个元素应用一个函数,然后返回原始数组。
def my_each(arr,&block)
for i in 0..arr.length-1
block[arr[i]]
end
arr
end
很简单。现在如果我们不传递一个块怎么办。让我们稍微改变一下以支持它。我们实际上希望延迟做 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
因此,如果我们不传递一个块,我们会创建一个 Enumerator
,当它被消费时,调用 my_each
,使用枚举器 yield 对象作为一个块。 y
对象是一个有趣的东西,但您可以将其视为最终将传入的块。因此,在
my_each([1,2,3]).with_index { |x,i| x * i }
将 y
视为类似于 { |x,i| x * i }
位。这比那要复杂一些,但这就是想法。
顺便说一下,在 Ruby 2.7 及更高版本上,Enumerator::Yielder
对象有自己的 #to_proc
,所以如果您使用的是最新的 Ruby 版本,则可以这样做
Enumerator.new do |y|
my_each(arr,&y)
end
而不是
Enumerator.new do |y|
my_each(arr) { |*x| y.yield(*x) }
end
现在让我们将此方法扩展到 map
。用块写 map
很容易。就像 each
一样,但我们累积结果。
def my_map(arr,&block)
result = []
for i in 0..arr.length-1
result << block[arr[i]]
end
result
end
很简单。现在如果我们不传递一个块怎么办?让我们对 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
现在,Enumerator
知道,无论何时最终获得一个块,它都会在最后对该块使用 my_map
。我们可以看到这两个函数实际上在数组上表现得像 map
和 each
do
my_each([1,i| x * i } # [1,3]
my_map ([1,i| x * i } # [0,6]
所以你的直觉很准
map
似乎携带了必须应用函数的信息,除了携带要迭代的数据之外。这是如何运作的?
这正是它的作用。 map
创建一个 Enumerator
,它的块知道在最后调用 map
,而 each
做同样的事情,但使用 each
。当然,实际上,出于效率和引导的原因,所有这些都是用 C 实现的,但基本思想仍然存在。
在没有块的情况下使用 Array#map 只会返回一个枚举器,然后将每个元素提供给 Enumerator#with_index,块的结果作为集合返回。它并不复杂,类似于(但可能比)以下代码。使用 Ruby 3.0.1:
results = []
[1,3].each_with_index { results << _1 * _2 }
results
#=> [0,6]
使用 Array#each 不会从块中返回一个集合。它只返回 self 或另一个枚举器,因此预期行为因设计而异。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。