如何在Ruby中复制哈希?

230

我承认我是一位Ruby新手(目前正在编写rake脚本)。在大多数语言中,复制构造函数很容易找到。但在Ruby中,我搜索了半个小时也没有找到。我想创建哈希表的副本,以便我可以修改它而不影响原始实例。

以下是一些预期不起作用的方法:

h0 = {  "John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
h1=Hash.new(h0)
h2=h1.to_hash

同时,我已经采用了这种不太优雅的解决方法。

def copyhash(inputhash)
  h = Hash.new
  inputhash.each do |pair|
    h.store(pair[0], pair[1])
  end
  return h
end

如果你正在处理普通的Hash对象,提供的答案是很好的。但如果你正在处理来自你无法控制的类似Hash的对象,你应该考虑是否要复制与Hash相关联的单例类。请参见https://dev59.com/FGkw5IYBdhLWcg3wBmGA。 - Sim
13个回答

257

clone方法是Ruby内置的标准方法,用于执行浅复制 (shallow-copy):

h0 = {"John" => "Adams", "Thomas" => "Jefferson"}
# => {"John"=>"Adams", "Thomas"=>"Jefferson"}
h1 = h0.clone
# => {"John"=>"Adams", "Thomas"=>"Jefferson"}
h1["John"] = "Smith"
# => "Smith"
h1
# => {"John"=>"Smith", "Thomas"=>"Jefferson"}
h0
# => {"John"=>"Adams", "Thomas"=>"Jefferson"}

请注意,行为可能被覆盖:

此方法可能具有特定于类的行为。 如果是这样,则该行为将在类的#initialize_copy方法下记录。


33
为了那些没有阅读其他答案的人更加清晰明确,这里添加一个更明确的评论:这是进行浅拷贝。 - grumpasaurus
#initialize_copy文档似乎不存在于Hash中,即使在Hash文档页面http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-initialize_copy上有链接。 - philwhln
17
对于其他的Ruby初学者,“浅拷贝”意味着第一级以下的每个对象仍然是一个引用。 - RobW
10
请注意,对于嵌套散列(如其他答案中所述),此方法对我不起作用。我使用了Marshal.load(Marshal.dump(h)) - bheeshmar
为什么使用clone而不是dup?在这种情况下,您需要复制单例类吗?https://dev59.com/FGkw5IYBdhLWcg3wBmGA - Sim
显示剩余2条评论

200

正如其他人所指出的,clone可以实现这一点。请注意,对哈希表(hash)进行clone操作将创建一个浅拷贝,也就是说:

h1 = {:a => 'foo'} 
h2 = h1.clone
h1[:a] << 'bar'
p h2                # => {:a=>"foobar"}

发生的情况是哈希表的引用被复制了,但是这些引用所指向的对象并没有被复制。

如果你想要进行深层复制,则需要:

def deep_copy(o)
  Marshal.load(Marshal.dump(o))
end

h1 = {:a => 'foo'}
h2 = deep_copy(h1)
h1[:a] << 'bar'
p h2                # => {:a=>"foo"}

deep_copy适用于可被序列化的任何对象。大多数内置数据类型(例如Array、Hash、String等)都可以被序列化。

序列化(Marshalling)是Ruby中用来表示序列化的术语。通过序列化,对象以及它所引用的其他对象将会被转换为一系列字节,这些字节随后将被用于创建与原始对象相似的另一个对象。


6
"K.Carpenter,这不是浅拷贝吗?它共享原始数据的一部分。根据我的理解,深拷贝是指不共享原始数据的任何部分,因此修改一个不会影响另一个。" - Wayne Conrad
1
Marshal.load(Marshal.dump(o)) 究竟是如何进行深度复制的?我真的不太理解背后发生了什么。 - Muntasir Alam
1
这也凸显了一个问题,如果你执行 h1[:a] << 'bar',你会修改原始对象(即 h1[:a] 指向的字符串),但是如果你执行 h1[:a] = "#{h1[:a]}bar",你将创建一个新的字符串对象,并将 h1[:a] 指向它,而 h2[:a] 仍然指向旧的(未修改的)字符串。 - Max Williams
1
注意:通过Marshal方法进行克隆可能会导致远程代码执行。https://ruby-doc.org/core-2.2.0/Marshal.html#module-Marshal-label-Security+considerations - Jesse Aldridge
1
@JesseAldridge 如果Marshal.load的输入不可信,则为真,并且要记住的一个好警告。 在这种情况下,它的输入来自我们自己进程中的Marshal.dump。 我认为在这种情况下,Marshal.load是安全的。 - Wayne Conrad
显示剩余2条评论

90

2
Rails 3在哈希中深度复制数组存在问题,而Rails 4已经修复了这个问题。 - pdobb
1
谢谢指出,当使用dup或clone时,我的哈希仍然受到影响。 - Esgi Dendyanri
初学者请注意,这是可在activesupport gem中使用的,而无需使用Rails。 - mtjhax

13

Hash可以从现有的哈希创建新的哈希:

irb(main):009:0> h1 = {1 => 2}
=> {1=>2}
irb(main):010:0> h2 = Hash[h1]
=> {1=>2}
irb(main):011:0> h1.object_id
=> 2150233660
irb(main):012:0> h2.object_id
=> 2150205060

26
请注意,这与 #clone 和 #dup 存在相同的深度复制问题。 - forforf
4
@forforf是正确的。如果你不理解深拷贝和浅拷贝,请不要尝试复制数据结构。 - James Moore

7

正如Marshal文档的安全注意事项中所提到的,如果您需要反序列化不受信任的数据,请使用JSON或其他仅能加载简单“原始”类型(如字符串、数组、哈希等)的序列化格式。

以下是在Ruby中使用JSON进行克隆的示例:

请参考下面内容:

require "json"

original = {"John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
cloned = JSON.parse(JSON.generate(original))

# Modify original hash
original["John"] << ' Sandler'
p original 
#=> {"John"=>"Adams Sandler", "Thomas"=>"Jefferson", "Johny"=>"Appleseed"}

# cloned remains intact as it was deep copied
p cloned  
#=> {"John"=>"Adams", "Thomas"=>"Jefferson", "Johny"=>"Appleseed"}

2
大部分情况下这是有效的,但是如果你的键是整数而不是字符串,请小心处理。当你从JSON转换时,键将变成字符串。 - SDJMcHattie

4

我也是Ruby的新手,曾经遇到过复制哈希表的问题。可以使用以下方法进行复制,但我不知道这种方法的速度如何。

copy_of_original_hash = Hash.new.merge(original_hash)

2

请使用Object#clone方法:

h1 = h0.clone

(令人困惑的是,有关clone的文档说要重写initialize_copy,但在Hash中该方法的链接却将您导向replace...)

1

这是一个特殊情况,但如果您要从预定义的哈希开始并复制它,您可以创建一个返回哈希的方法:

def johns 
    {  "John"=>"Adams","Thomas"=>"Jefferson","Johny"=>"Appleseed"}
end

h1 = johns

我遇到的具体情况是,我有一组JSON-schema哈希值,其中一些哈希建立在其他哈希之上。 我最初将它们定义为类变量,并遇到了这个复制问题。

1

克隆速度较慢。为了提高性能,可能应该从空哈希表开始并合并。不涵盖嵌套哈希表的情况...

require 'benchmark'

def bench  Benchmark.bm do |b|    
    test = {'a' => 1, 'b' => 2, 'c' => 3, 4 => 'd'}
    b.report 'clone' do
      1_000_000.times do |i|
        h = test.clone
        h['new'] = 5
      end
    end
    b.report 'merge' do
      1_000_000.times do |i|
        h = {}
        h['new'] = 5
        h.merge! test
      end
    end
    b.report 'inject' do
      1_000_000.times do |i|
        h = test.inject({}) do |n, (k, v)|
          n[k] = v;
          n
        end
        h['new'] = 5
      end
    end
  end
end
  测试项  用户时间   系统时间   总时间       ( 实际时间)
  克隆    1.960000   0.080000   2.040000    (  2.029604)
  合并    1.690000   0.080000   1.770000    (  1.767828)
  注入    3.120000   0.030000   3.150000    (  3.152627)

1
由于标准克隆方法保留了冻结状态,因此如果您希望新对象与原始对象略有不同(如果您喜欢无状态编程),则不适合使用该方法创建基于原始对象的新不可变对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接