map 方法的工作原理
要查看您的方法如何运行,让我们修改您的代码以添加一些中间变量和一些puts 语句以显示这些变量的值。
def map(s)
new = []
i = 0
n = s.length
puts "s has length #{n}"
while i < n
puts "i = #{i}"
e = s[i]
puts " Yield #{e} to the block"
rv = yield(e)
puts " The block's return value is #{rv}. Push #{rv} onto new"
new.push(rv)
puts " new now equals #{new}"
i += 1
end
puts "We now return the value of new"
new
end
现在让我们使用其中一个感兴趣的块来执行该方法。
s = [1, 2, 3, -9]
map(s) { |n| n * 2 }
#=> [2, 4, 6, -18] (return value of method)
显示如下。
s has length 4
i = 0
Yield 1 to the block
The block's return value is 2. Push 2 onto new
new now equals [2]
i = 1
Yield 2 to the block
The block's return value is 4. Push 4 onto new
new now equals [2, 4]
i = 2
Yield 3 to the block
The block's return value is 6. Push 6 onto new
new now equals [2, 4, 6]
i = 3
Yield -9 to the block
The block's return value is -18. Push -18 onto new
new now equals [2, 4, 6, -18]
We now return the value of new
使用不同的s 值和不同的块来执行这个修改后的方法可能会很有趣。
Array#map 的替代品?
这是Array#map(或Enumerable#map,但现在让我们只考虑Array#map)的替代品吗?正如您在顶层定义的那样,您的map 是Object 类的实例方法:
Object.instance_methods.include?(:map) #=> true
它必须被调用map([1,2,3]) { |n| ... } 而Array#map 被调用[1,2,3].map { |n| ... }。因此,要让您的方法 map 替代 Array#map,您需要将其定义如下。
class Array
def map
new = []
i = 0
while i < length
new.push(yield(self[i]))
i += 1
end
new
end
end
[1, 2, 3, -9].map { |n| n * 2 }
#=> [2, 4, 6, -18]
简化
我们可以将这个方法简化如下。
class Array
def map
new = []
each { |e| new << yield(e) }
new
end
end
[1, 2, 3, -9].map { |n| n * 2 }
#=> [2, 4, 6, -18]
或者,更好:
class Array
def map
each_with_object([]) { |e,new| new << yield(e) }
end
end
见Enumerable#each_with_object。
注意while i < length 等价于while i < self.length,因为self.,如果省略,是隐含的,因此是多余的。同样,each { |e| new << yield(e) } 等价于self.each { |e| new << yield(e) },each_with_object([]) { ... } 等价于self.each_with_object([]) { ... }。
我们完成了吗?
如果我们仔细检查文档Array#map,我们会发现该方法有两种形式。第一个是map 占用一个块。我们的方法Array#map 模仿了这种行为,这是满足给定rspec 测试所需的唯一行为。
然而,还有第二种形式,map 没有给定一个块,在这种情况下它返回一个枚举数。这允许我们将方法链接到另一个方法。例如(使用 Ruby 的Array#map),
['cat', 'dog', 'pig'].map.with_index do |animal, i|
i.even? ? animal.upcase : animal
end
#=> ["CAT", "dog", "PIG"]
我们可以修改我们的Array#map 以合并第二个行为,如下所示。
class Array
def map
if block_given?
each_with_object([]) { |e,new| new << yield(e) }
else
to_enum(:map)
end
end
end
[1, 2, 3, -9].map { |n| n * 2 }
#=> [2, 4, 6, -18]
['cat', 'dog', 'pig'].map.with_index do |animal, i|
i.even? ? animal.upcase : animal
end
#=> ["CAT", "dog", "PIG"]
请参阅 Kernel#block_given? 和 Object#to_enum。
备注
您可以使用arr 而不是s 作为保存数组的变量,因为s 通常表示字符串,就像h 通常表示哈希一样。通常避免使用作为核心 Ruby 方法名称的变量和自定义方法的名称。这也反对你使用new作为变量名,因为有很多核心方法命名为new。