为什么在Ruby中使用符号作为哈希键?

174
很多时候人们在Ruby哈希表中使用符号作为键。相对于使用字符串,这样做有什么优势吗?
例如:
hash[:name]

对比。

hash['name']
4个回答

244

TL;DR:

使用符号不仅在比较时节省时间,还因为它们只存储一次而节省内存。

Ruby Symbols是不可变的(不能被改变),这使得查找某些内容更加容易。

简短回答:

使用符号不仅在比较时节省时间,还因为它们只存储一次而节省内存。

Ruby中的符号基本上是"不可变字符串",这意味着它们不能被改变,并且意味着当在源代码中多次引用相同的符号时,始终将其存储为相同的实体,例如具有相同的对象ID。

  a = 'name'
  a.object_id
=> 557720

  b = 'name'
=> 557740

  'name'.object_id
=> 1373460

  'name'.object_id
=> 1373480          # !! different entity from the one above

# Ruby assumes any string can change at any point in time, 
# therefore treating it as a separate entity

# versus:

  :name.object_id
=> 71068

  :name.object_id
=> 71068

# the symbol :name is a references to the same unique entity

相反的,字符串是可变的,它们可以随时更改。这意味着Ruby需要在其单独的实体中存储源代码中提到的每个字符串,例如,如果您在源代码中多次提到一个字符串“name”,Ruby需要将它们全部存储在单独的String对象中,因为它们可能会在以后更改(这是Ruby字符串的本质)。

如果您将字符串用作哈希键,则Ruby需要评估字符串并查看其内容(并计算哈希函数),并将结果与已存储在哈希表中的键的(哈希)值进行比较。

如果您将符号用作哈希键,则它的固有属性是不可变的,因此Ruby基本上只需比较(哈希函数的)对象ID与已存储在哈希表中的键的(已哈希的)对象ID即可。(速度更快)

缺点: 每个符号都占用Ruby解释器符号表中的一个插槽,而这些插槽永远不会被释放。 符号永远不会被垃圾回收。 因此,当您拥有大量符号(例如自动生成的符号)时,就存在一个特殊情况,您应该评估这如何影响Ruby解释器的大小(例如,如果您以编程方式生成太多符号,则Ruby可能会耗尽内存并崩溃)。

注:

如果您进行字符串比较,Ruby可以通过比较它们的对象ID来比较符号,而无需评估它们。这比比较字符串要快得多,因为字符串需要被评估。

如果您访问哈希表,Ruby总是会应用哈希函数来计算出一个"哈希键",无论您使用什么键。您可以想象一下类似于MD5哈希的东西。然后,Ruby将这些"哈希键"相互比较。
每次在您的代码中使用字符串时,都会创建一个新实例——创建字符串比引用符号慢。
从Ruby 2.1开始,当您使用冻结字符串时,Ruby将使用同一个字符串对象。这避免了创建相同字符串的新副本,并且它们存储在垃圾回收的空间中。
长答案:

https://web.archive.org/web/20180709094450/http://www.reactive.io/tips/2009/01/11/the-difference-between-ruby-symbols-and-strings

http://www.randomhacks.net.s3-website-us-east-1.amazonaws.com/2007/01/20/13-ways-of-looking-at-a-ruby-symbol/

https://www.rubyguides.com/2016/01/ruby-mutability/


6
FYI,Ruby 的下一个版本中将会对符号进行垃圾回收:https://bugs.ruby-lang.org/issues/9634。 - Ajedi32
2
此外,在 Ruby 中,如果将字符串用作哈希键,则会自动冻结字符串。因此,在这种情况下,关于字符串的可变性说法并不完全正确。 - Ajedi32
1
该主题有很深刻的见解。在“长答案”部分中,第一个链接已被删除或迁移。 - Hbksagar
5
符号在 Ruby 2.2 中进行垃圾回收。 - Marc-André Lafortune
2
很棒的回答!说句调侃的话,你的“简短回答”其实已经足够长了。;) - technophyle
显示剩余6条评论

23
原因是效率问题,相比于字符串,Symbol有多个优势:
  1. Symbol是不可变的,所以不需要问“如果键改变会发生什么?”
  2. 字符串在代码中是重复的,并且通常占用更多的内存空间。
  3. 哈希查找必须计算键的哈希值才能进行比较。对于字符串来说,这是O(n),而对于Symbol则是常数时间。
此外,Ruby 1.9引入了一种专门用于具有符号键的哈希的简化语法(例如`h.merge(foo: 42, bar: 6)`),而Ruby 2.0则具有仅适用于符号键的关键字参数
注: 1)您可能会惊讶地发现,Ruby将`String`键与任何其他类型的键处理方式不同。确实如此:
s = "foo"
h = {}
h[s] = "bar"
s.upcase!
h.rehash   # must be called whenever a key changes!
h[s]   # => nil, not "bar"
h.keys
h.keys.first.upcase!  # => TypeError: can't modify frozen string

仅对于字符串键,Ruby将使用冻结副本而不是对象本身。

2)程序中所有:bar的出现都只存储一次字母“b”、“a”和“r”。在Ruby 2.2之前,不断创建从未重用过的新Symbols是一个坏主意,因为它们将永远留在全局Symbol查找表中。Ruby 2.2会进行垃圾回收,所以不用担心。

3)实际上,在Ruby 1.8.x中计算Symbol的哈希值不需要任何时间,因为直接使用对象ID:

:bar.object_id == :bar.hash # => true in Ruby 1.8.7

在 Ruby 1.9.x 中,哈希表(包括 Symbols)会随着会话的变化而发生改变:
:bar.hash # => some number that will be different next time Ruby 1.9 is ran

+1 对于你出色的笔记!我在回答中原本没有提到哈希函数,因为我试图使其更易读 :) - Tilo
@Tilo:确实,这就是我写答案的原因 :-) 我刚刚编辑了我的答案,提到了Ruby 1.9中的特殊语法和Ruby 2.0中承诺的命名参数。 - Marc-André Lafortune
你能解释下哈希查询(Symbol)是常数级别的,而字符串却是O(n)级别的吗? - Asad Moosvi

7

回复:为什么使用符号的优势比字符串更好?

  • 样式:这是Ruby的方式
  • (非常)稍微快一点的值查找,因为哈希一个符号相当于哈希一个整数,而不是哈希一个字符串。

  • 缺点:消耗程序符号表中永远不会释放的一个槽位。


4
提到该符号永远不会被垃圾回收,加1。 - Vortico
1
该符号不会被垃圾回收 - 自 Ruby 2.2+ 起不再适用。 - eudaimonia

0

我对Ruby 2.x中引入的冻结字符串非常感兴趣,希望能有后续报道。

当你处理来自文本输入的大量字符串时(例如通过Rack传递的HTTP参数或负载),在任何地方使用字符串都更容易。

当你处理数十个从不改变的字符串时(如果它们是你的业务“词汇”),我认为将它们冻结可能会有所不同。我还没有进行任何基准测试,但我猜它的性能接近符号。


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