【问题标题】:Why a block invoked by a Module can't modify objects from implementing classes in Ruby?为什么模块调用的块不能修改 Ruby 中实现类的对象?
【发布时间】:2015-05-09 07:35:38
【问题描述】:

我有一些数据保存在深度嵌套的哈希和数组中,我在数据的文本编码方面遇到了麻烦。我知道文本是用“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'

然后我将函数向下移动到“数组”和“哈希”类:

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 定义它?

【问题讨论】:

  • 在第一种情况下,您尝试修改哈希键(以及值)
  • @FrederickCheung 你能解释一下吗?如果您指的是使用each_valueeach 方法,那么我认为这不是问题所在。 each 在第二个 sn-p 中同样有效,而对第一个 snipper 无效(在 Enumerator 方法中)。

标签: ruby class module enumerable


【解决方案1】:

据我所知,您的 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

【讨论】:

  • 你完全正确。我发现了错误。秘密就在这里:deephash.deep_each {|element| element.force_encoding("UTF-8") if (element.class == String and element.encoding != Encoding::UTF_8)}。我在中途修改了块,在对字符串施加新编码之前检查字符串的当前编码。由于 Hash 键默认为 UTF-8,所以没有报错。最后,看起来分别为每个类编写方法更有意义。对于哈希,我需要使用each_value 而不是each
  • @retrography,您可以将单个方法保留在 Enumerable 中——但您可以创建类型/迭代器对的哈希,而不是使用一堆 if-else 语句检查类型。请参阅我的答案底部的示例。
【解决方案2】:
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

如果您发布以下示例会更能说明问题:我遇到了数据的文本编码问题

最后好像是为每个类分别写方法 更有意义。对于哈希我需要使用 each_value 而不是 每个

你可以这样做:

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
--------------------

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-02-04
    • 2012-11-18
    • 2018-07-09
    • 1970-01-01
    • 2014-04-11
    • 2021-09-13
    • 2011-04-20
    • 1970-01-01
    相关资源
    最近更新 更多