Ruby中的字符串拼接

403

我正在寻找一种更优雅的方法来在Ruby中连接字符串。

我有以下代码行:

source = "#{ROOT_DIR}/" << project << "/App.config"

有没有更好的方法来做这件事?

此外,<<和+之间有什么区别?


3
这个问题 https://dev59.com/KW445IYBdhLWcg3w0dZD 与此高度相关。 - esengineer
这是更高效的字符串拼接方式。 - Taimoor Changaiz
16个回答

624
你可以用以下几种方法实现:
  1. 就像你展示的那样使用<<,但这不是通常的方式。
  2. 使用字符串插值

    source = "#{ROOT_DIR}/#{project}/App.config"
    
  3. 使用+运算符

  4. source = "#{ROOT_DIR}/" + project + "/App.config"
    

根据我的观察(虽然没有测量),第二种方法似乎在内存/速度方面更有效率。无论哪种方法,当ROOT_DIR为nil时都会抛出未初始化常量错误。

处理路径名时,您可能希望使用File.join来避免弄乱路径名分隔符。

最终,这是一个品味问题。


8
我对 Ruby 不是很熟悉。但是一般情况下,当您连接大量字符串时,通过将字符串附加到数组中,最后再原子地组合字符串,通常可以提高性能。那么 << 可能会有用吗? - PEZ
1
你必须添加内存并将较长的字符串复制到其中。<<与+或多或少相同,只是您可以使用单个字符<<。 - Keltia
10
不要使用<<来操作数组元素,改用Array#join方法,它更快。 - Grant Hutchins

107

+ 运算符是正常的连接选择,可能是连接字符串最快的方式。

+<< 之间的区别在于,<< 改变其左侧对象,而 + 不会。

irb(main):001:0> s = 'a'
=> "a"
irb(main):002:0> s + 'b'
=> "ab"
irb(main):003:0> s
=> "a"
irb(main):004:0> s << 'b'
=> "ab"
irb(main):005:0> s
=> "ab"

38
使用加号操作符连接字符串绝对不是最快的方法。每次使用加号操作符时,它都会创建一个新的副本,而使用 << 操作符则可以就地连接,在性能上更加优秀。 - Evil Trout
6
对于大多数情况,插值、+<< 的效果都差不多。如果你处理很多字符串或者非常大的字符串,那么可能会注意到它们之间的差异。我对它们表现得如此相似感到惊讶。 - Matt Burke
9
您的jruby测试结果可能因初始JVM负载而导致插值的结果偏差。如果您对每个解释器运行测试套件多次(在同一进程中 - 因此将所有内容都包装在5.times do ... end块中),则可以获得更准确的结果。我的测试表明,在所有Ruby解释器中,插值是最快的方法。我本来以为<<会是最快的,但这就是我们进行基准测试的原因。 - womble
我对 Ruby 不太熟悉,想知道变异是在栈上还是堆上执行?如果在堆上执行,即使是变异操作,似乎应该更快,但可能仍涉及某种形式的malloc。如果没有它,我期望缓冲区溢出。使用堆栈可能非常快,但结果值可能被放置在堆上,需要一个malloc操作。最终,即使变量引用使其看起来像是原地变异,我也预期内存指针是一个新的地址。那么,真的有区别吗? - Robin Coe

81

如果你只是要将路径连接起来,你可以使用Ruby自带的File.join方法。

source = File.join(ROOT_DIR, project, 'App.config')

5
这似乎是正确的方法,因为Ruby会负责在具有不同路径分隔符的系统上创建正确的字符串。 - PEZ

32

来自http://greyblake.com/blog/2012/09/02/ruby-perfomance-tricks/

使用<<,也称为concat,比使用+=更高效,因为后者会创建一个临时对象,并用新对象覆盖第一个对象。

require 'benchmark'

N = 1000
BASIC_LENGTH = 10

5.times do |factor|
  length = BASIC_LENGTH * (10 ** factor)
  puts "_" * 60 + "\nLENGTH: #{length}"

  Benchmark.bm(10, '+= VS <<') do |x|
    concat_report = x.report("+=")  do
      str1 = ""
      str2 = "s" * length
      N.times { str1 += str2 }
    end

    modify_report = x.report("<<")  do
      str1 = "s"
      str2 = "s" * length
      N.times { str1 << str2 }
    end

    [concat_report / modify_report]
  end
end

输出:

____________________________________________________________
LENGTH: 10
                 user     system      total        real
+=           0.000000   0.000000   0.000000 (  0.004671)
<<           0.000000   0.000000   0.000000 (  0.000176)
+= VS <<          NaN        NaN        NaN ( 26.508796)
____________________________________________________________
LENGTH: 100
                 user     system      total        real
+=           0.020000   0.000000   0.020000 (  0.022995)
<<           0.000000   0.000000   0.000000 (  0.000226)
+= VS <<          Inf        NaN        NaN (101.845829)
____________________________________________________________
LENGTH: 1000
                 user     system      total        real
+=           0.270000   0.120000   0.390000 (  0.390888)
<<           0.000000   0.000000   0.000000 (  0.001730)
+= VS <<          Inf        Inf        NaN (225.920077)
____________________________________________________________
LENGTH: 10000
                 user     system      total        real
+=           3.660000   1.570000   5.230000 (  5.233861)
<<           0.000000   0.010000   0.010000 (  0.015099)
+= VS <<          Inf 157.000000        NaN (346.629692)
____________________________________________________________
LENGTH: 100000
                 user     system      total        real
+=          31.270000  16.990000  48.260000 ( 48.328511)
<<           0.050000   0.050000   0.100000 (  0.105993)
+= VS <<   625.400000 339.800000        NaN (455.961373)

11

既然这是一个路径,我可能会使用数组和连接:

source = [ROOT_DIR, project, 'App.config'] * '/'

10

这是另一个灵感来自于此代码片段的基准测试。它比较了动态和预定义字符串的拼接(+)、追加(<<)和插值(#{})。

require 'benchmark'

# we will need the CAPTION and FORMAT constants:
include Benchmark

count = 100_000


puts "Dynamic strings"

Benchmark.benchmark(CAPTION, 7, FORMAT) do |bm|
  bm.report("concat") { count.times { 11.to_s +  '/' +  12.to_s } }
  bm.report("append") { count.times { 11.to_s << '/' << 12.to_s } }
  bm.report("interp") { count.times { "#{11}/#{12}" } }
end


puts "\nPredefined strings"

s11 = "11"
s12 = "12"
Benchmark.benchmark(CAPTION, 7, FORMAT) do |bm|
  bm.report("concat") { count.times { s11 +  '/' +  s12 } }
  bm.report("append") { count.times { s11 << '/' << s12 } }
  bm.report("interp") { count.times { "#{s11}/#{s12}"   } }
end

输出:

Dynamic strings
              user     system      total        real
concat    0.050000   0.000000   0.050000 (  0.047770)
append    0.040000   0.000000   0.040000 (  0.042724)
interp    0.050000   0.000000   0.050000 (  0.051736)

Predefined strings
              user     system      total        real
concat    0.030000   0.000000   0.030000 (  0.024888)
append    0.020000   0.000000   0.020000 (  0.023373)
interp    3.160000   0.160000   3.320000 (  3.311253)

结论:磁共振成像中的插值是比较复杂的。

由于字符串现在开始是不可变的,我希望能看到一个新的基准测试。 - Bibek Shrestha

7

我更喜欢使用Pathname:

require 'pathname' # pathname is in stdlib
Pathname(ROOT_DIR) + project + 'App.config'

关于<<+的区别,来自Ruby文档: +:返回一个新的字符串,其中包含将other_str连接到str后面的结果。 <<:将给定对象连接到str。如果对象是介于0和255之间的Fixnum,则在连接之前将其转换为字符。
因此,它们的区别在于第一个操作数的处理方式(<<会直接更改原字符串,+会返回新字符串,因此更占用内存),以及如果第一个操作数是Fixnum时的处理方式(<<会添加与该数字代码相等的字符,+会引发错误)。

2
我刚刚发现调用 Pathname 上的 '+' 可能是危险的,因为如果参数是绝对路径,则接收方路径会被忽略:Pathname('/home/foo') + '/etc/passwd' # => #<Pathname:/etc/passwd>。根据 rubydoc 的示例设计,这是有意而为之的。看来使用 File.join 更加安全。 - Kelvin
如果您想返回一个字符串对象,还需要调用 (Pathname(ROOT_DIR) + project + 'App.config').to_s - lacostenycoder

6
让我向您展示一下我的经验。
我有一个查询,返回了32k条记录,对于每条记录,我调用一个方法将数据库记录格式化为字符串,然后将其连接成一个字符串。最终这个字符串会变成一个磁盘上的文件。
我的问题是当记录达到24k时,连接字符串的过程变得很慢。我使用的是常规的“+”操作符。
当我改用“<<”操作符时,就像魔法一样,速度非常快。
所以,我想起了我的旧时光——大约是1998年——当我使用Java并使用“+”来连接字符串时,我改用了StringBuffer(现在我们Java开发人员已经有了StringBuilder)。
我相信,在Ruby世界中,“+”/“<<” 的处理过程与 Java世界中“+”/StringBuilder.append的处理过程是相同的。
前者重新分配整个对象在内存中,而后者只是指向一个新地址。

5
你说连接字符串?那么如何使用 #concat 方法呢?
a = 'foo'
a.object_id #=> some number
a.concat 'bar' #=> foobar
a.object_id #=> same as before -- string a remains the same object

公正地说,concat作为<<的别名。


7
其他人没有提到的一种将字符串粘合在一起的方法是简单地并置它们:"foo" "bar" 'baz" #=> "foobarabaz" - Boris Stitnicky
注意:这里应该使用双引号而不是单引号,和其他的一样。很棒的方法! - Joshua Pinter

5

以下是更多的方法:

"String1" + "String2"

"#{String1} #{String2}"

String1<<String2

And so on ...


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