为什么在 Ruby 中构建字符串时,铲子操作符(< <)优先于 plus-equals (+ =) ?

我在研究 Ruby Koans。

关于字符串中的 test_the_shovel_operator_modifies_the_original_string Koan 包括以下评论:

Ruby 程序员倾向于使用铲子操作符(< <)而不是加号 等于运算符(+ =)。为什么?

我猜这和速度有关,但我不明白引擎盖下的动作会让铲子操作员更快。

有人能解释一下这个偏好背后的细节吗?

35954 次浏览

因为它更快/不创建不需要运行的字符串 <-> 垃圾收集器的副本。

证据:

a = 'foo'
a.object_id #=> 2154889340
a << 'bar'
a.object_id #=> 2154889340
a += 'quux'
a.object_id #=> 2154742560

因此,<<更改原始字符串而不是创建新字符串。这样做的原因是,在 ruby 中,a += ba = a + b(其他 <op>=操作符也是如此)的语法简写,a = a + b是赋值。另一方面,<<concat()的别名,它改变接收器的位置。

表现证明:

#!/usr/bin/env ruby


require 'benchmark'


Benchmark.bmbm do |x|
x.report('+= :') do
s = ""
10000.times { s += "something " }
end
x.report('<< :') do
s = ""
10000.times { s << "something " }
end
end


# Rehearsal ----------------------------------------
# += :   0.450000   0.010000   0.460000 (  0.465936)
# << :   0.010000   0.000000   0.010000 (  0.009451)
# ------------------------------- total: 0.470000sec
#
#            user     system      total        real
# += :   0.270000   0.010000   0.280000 (  0.277945)
# << :   0.000000   0.000000   0.000000 (  0.003043)

虽然不能直接回答您的问题,但是为什么 完全翻转的垃圾箱一直是我最喜欢的 Ruby 文章之一。它还包含关于垃圾收集的字符串的一些信息。

一个正在学习 Ruby 作为他的第一门编程语言的朋友在浏览 Ruby Koans 系列的 String in Ruby 时问了我同样的问题。我用下面的类比向他解释;

你有一杯半满的水,你需要重新装满你的杯子。

第一种方法是拿一个新的杯子,从水龙头中取出一半的水灌入,然后用第二个半满的杯子重新装满你的饮用水。你每次需要倒酒的时候都这样。

第二种方法是你拿起你的半满的杯子,直接从水龙头里加满水。

在一天结束的时候,如果你每次需要重新装满杯子的时候都选择一个新的杯子,你就会有更多的杯子需要清洗。

同样的情况也适用于铲工和正等操作工。另外,平等操作者每次需要重新装满玻璃杯的时候都会选择一个新的“玻璃杯”,而铲子操作者只是拿着同样的玻璃杯重新装满。在一天结束时,更多的’玻璃’收集为加平等运算符。

这是一个古老的问题,但我只是偶然发现它,我不完全满意现有的答案。铲子 < < 比连接 + = 快有很多优点,但也有语义上的考虑。

接受的答案显示 < < 在适当的位置修改现有对象,而 + = 创建一个新对象。因此,您需要考虑是希望对字符串的所有引用都反映新值,还是希望仅保留现有引用并创建一个本地使用的新字符串值。如果需要所有引用来反映更新后的值,则需要使用 < < 。如果希望不使用其他引用,那么需要使用 + = 。

一个非常常见的情况是,只有一个对字符串的引用。在这种情况下,语义差异并不重要,因为它的速度,所以自然而然地更喜欢 < < 。

虽然大多数答案涵盖 +=是较慢的,因为它创建一个新的副本,这是重要的是要记住,+=<< 不是可互换!您希望在不同的情况下使用每种方法。

使用 <<还将改变指向 b的任何变量。在这里,我们也突变 a时,我们可能不想要。

2.3.1 :001 > a = "hello"
=> "hello"
2.3.1 :002 > b = a
=> "hello"
2.3.1 :003 > b << " world"
=> "hello world"
2.3.1 :004 > a
=> "hello world"

因为 +=创建了一个新的副本,所以它也保留了所有指向它的变量。

2.3.1 :001 > a = "hello"
=> "hello"
2.3.1 :002 > b = a
=> "hello"
2.3.1 :003 > b += " world"
=> "hello world"
2.3.1 :004 > a
=> "hello"

理解这种区别可以在处理循环时为您省去很多麻烦!