为什么模块调用的块不能修改在 Ruby 中实现 类 的对象?

Why a block invoked by a Module can't modify objects from implementing classes in Ruby?

我有一些数据保存在深度嵌套的哈希和数组中,我 运行 在数据的文本编码方面遇到了麻烦。我确实知道文本是用 "UTF-8" 编码的,所以我决定遍历每个元素并强制编码。

因此,我为 Enumerable 模块创建了一个名为 deep_each 的方法:

module Enumerable
  def deep_each(&block)
    self.each do |element|
      if element.is_a? Enumerable then
        element.deep_each(&block)
      else
        block[element]
      end
    end
  end
end

并希望能够使用以下方法调用修复数据:

deephash.deep_each {|element| element.force_encoding("UTF-8") if element.class == String}

但结果令人失望:

deephash.deep_each {|element| element.force_encoding("UTF-8") if element.class == String}

> RuntimeError: can't modify frozen String
> from (pry):16:in `force_encoding'

然后我将函数向下移动到层次结构 "Array" 和 "Hash" 类:

class Hash
  def deep_each(&block)
    self.each do |element|
      if [Array, Hash].include? element.class then
        element.deep_each(&block)
      else
        block[element]
      end
    end
  end
end

class Array
  def deep_each(&block)
    self.each do |element|
      if [Array, Hash].include? element.class then
        element.deep_each(&block)
      else
        block[element]
      end
    end
  end
end

令人惊讶的是,同样的调用现在有效了。

我在这里违反了什么约束,如何为所有 Enumerables 定义一个方法而不为每个 Enumerables 定义它?

据我所知,您的 Enumerable 版本和 Array/Hash 猴子补丁应该得到完全相同的错误。我愿意。您确定在这两种情况下都使用相同的 deephash 吗?

通常当您在散列上循环 each 时,您会将键和值都传递给块。您正在将单个值 element 传递给块。这是一个包含键和值的数组:

irb> {a:1, b:2}.each {|el| puts el.inspect }
[:a, 1]
[:b, 2]

您的 deep_each 检查这是否是一个 Enumerable,确实是,所以它在列表中调用 deep_each。然后,最后,您到达叶子并调用键和值上的块。该块检查它是否正在使用字符串,如果是,则强制编码。

如果您的哈希键是一个字符串,您将尝试对其进行变异。但是散列键被冻结,因此 RuntimeError: can't modify frozen String 被引发。

irb> {a: {b: {c: "abc"}}}.deep_each { |el| el << "efg" if String === el}
=> {:a=>{:b=>{:c=>{:d=>"abcefg"}}}}
irb> {a: {b: {"c" => "abc"}}}.deep_each { |el| el << "efg" if String === el}
RuntimeError: can't modify frozen String
str = "\xE2\x82\xAC" #Euro sign in UTF-8
puts str.encoding  #=> UTF-8
puts str  #=> Euro sign in a UTF-8 enabled terminal window

File.open('data.txt', 'w:utf-8') do |f|
  f.write("#{str}\n")
end

Encoding.default_external = 'ISO-8859-1'
str = File.read('data.txt') 
puts str.encoding  #=> ISO-8859-1

arr = [
  {a: str},
  {b: 'world'},
]

arr[0][:a].force_encoding('utf-8')
puts arr[0][:a].encoding  #=> UTF-8
puts arr[0][:a]  #=> Euro sign in a UTF-8 enabled terminal window

如果您张贴以下示例会更说明问题:我 运行 遇到了数据文本编码方面的问题

Finally, it looks like writing the method for each class separately makes more sense. For the Hash I need to use each_value rather than each

你可以这样做:

iterator_for = Hash.new(:each)  #When a non-existent key is looked up, return :each

iterator_for.update({
  Hash => :each_value,
})

data = [
  %w{ hello world goodbye },
  {"a" => "red", "b" => "blue"},
]

data.each do |element|
  element.send(iterator_for[element.class]) do |x|
    puts x
  end

  puts '-' * 20
end

--output:--
hello
world
goodbye
--------------------
red
blue
--------------------