为什么Ruby没有真正的StringBuffer或StringIO?

57

最近我读了一篇很好的文章,介绍了在Ruby中使用StringIO。但作者没有提到的是,StringIO只是一个"I",没有"O"。例如,你无法这样做:

s = StringIO.new
s << 'foo'
s << 'bar'
s.to_s
# => should be "foo\nbar"
# => really is ''`

Ruby确实需要一个类似Java的StringBuffer。StringBuffer有两个重要用途,第一,它可以像Ruby的StringIO那样测试输出一半。第二,它们有助于从小部分组建长字符串——这是Joel多次提醒我们否则非常慢的事情。

有没有好的替代品?

确实,Ruby中的字符串是可变的,但这并不意味着我们应该总是依赖这种功能。如果stuff很大,例如这样做的性能和内存需求真的很糟糕。

result = stuff.map(&:to_s).join(' ')

在Java中做这件事的“正确”方法是:

result = StringBuffer.new("")
for(String s : stuff) {
  result.append(s);
}

虽然我的Java有点生疏。


“Mega Maid?”从未听说过。我也从未真正相信StringBuffers,但我总是使用它们,因为害怕有人看到我的代码。但实际上,这些东西真的有用吗? - Dan Rosenstark
3
可能是对电影《星际大丑闻》的引用。 - Stephen Eilert
2
Mega Maid因为清除不良语言而被误删。 - Andrew Grimm
你的字符串连接示例与Java代码不等价。正如你所提到的,Ruby字符串是可变的,因此在Ruby中,你只需要执行以下操作:stuff.inject('') { |res, s| res << s.to_s }。你可以放心地依赖于Ruby字符串是可变的,它不会改变,因为这会破坏每一个现有的Ruby应用程序。 - Theo
1
我真的不明白为什么StringIO没有to_s方法。它是一个管理字符串的类,所以如果你想要那个字符串,你必须明确地要求它。 它应该有一个to_s方法,因为这是Ruby的约定,但它没有。(如果我错了,有人可以纠正我) - hcarreras
@Theo 在 Ruby 3 中,字符串字面量将是不可变的。但是我们仍然可以使用可变的 String.new+'' - Franklin Yu
5个回答

126

我查看了关于StringIO的Ruby文档,看起来你需要的是StringIO#string,而不是StringIO#to_s

因此,请将您的代码更改为:

s = StringIO.new
s << 'foo'
s << 'bar'
s.string

37

像 Ruby 中的其他 IO 类型对象一样,当您向一个 IO 写入时,字符指针会前进。

>> s = StringIO.new
=> #<StringIO:0x3659d4>
>> s << 'foo'
=> #<StringIO:0x3659d4>
>> s << 'bar'
=> #<StringIO:0x3659d4>
>> s.pos
=> 6
>> s.rewind
=> 0
>> s.read
=> "foobar"

我并不是真的需要这个,因为有StringIO#read,但我总是喜欢知道多种做法。+1 - James A. Rosen
啊,我的意思是 StringIO#string - James A. Rosen

26

我进行了一些基准测试,最快的方法是使用String#<<方法。使用StringIO稍微慢一些。

s = ""; Benchmark.measure{5000000.times{s << "some string"}}
=>   3.620000   0.100000   3.720000 (  3.970463)

>> s = StringIO.new; Benchmark.measure{5000000.times{s << "some string"}}
=>   4.730000   0.120000   4.850000 (  5.329215)

使用String#+方法拼接字符串是最慢的方法,速度要慢很多个数量级:

s = ""; Benchmark.measure{10000.times{s = s + "some string"}}
=>   0.700000   0.560000   1.260000 (  1.420272)

s = ""; Benchmark.measure{10000.times{s << "some string"}}
=>   0.000000   0.000000   0.000000 (  0.005639)

所以我认为,Ruby中与Java的StringBuffer等价的方法就是使用String#<<


1
请问这个基准测试使用的是哪个版本的 Ruby? - Jared Beck
1
哇,那么字符串应该是最快的答案。在 Ruby 2.1.5 上测试过,结果相同。 - Nikkolasg
当Ruby使字符串不可变时会发生什么?这些微小的优化最终会成为一种折磨。 - akostadinov
如果您想向非常长的字符串添加一些字符会发生什么?我认为使用StringIO会更快。 - nothing-special-here
字符串连接方案无法与冻结字符串文字一起使用,但是 StringIO 可以正常工作。 - KARASZI István
有趣的是看到 Ruby 和计算机在过去几年中变得更快了。在 Ruby 2.7.5 上,我按顺序得到了以下结果:0.856717(4.6倍速度更快),1.002285(5.3倍),0.553703(2.5倍),0.001409(3.9倍)。我使用的是2017年版的 MBP。 - psmith

13

你的例子在Ruby中可行 - 我刚试过了。

irb(main):001:0> require 'stringio'
=> true
irb(main):002:0> s = StringIO.new
=> #<StringIO:0x2ced9a0>
irb(main):003:0> s << 'foo'
=> #<StringIO:0x2ced9a0>
irb(main):004:0> s << 'bar'
=> #<StringIO:0x2ced9a0>
irb(main):005:0> s.string
=> "foobar"

除非我没理解你使用 to_s 的原因 - 否则它只会输出对象的 ID。


3

在 Ruby 中,String 是可变的,因此不像 Java 那样必须使用 StringBuffer。你可以通过修改现有的字符串来构建一个字符串,而不是每次都用 concat 构造新的字符串。

值得一提的是,你还可以使用特殊的字符串语法来构建一个字符串,该语法可以引用字符串中的其他变量,使字符串构造更加易读。例如:

first = "Mike"
last = "Stone"
name = "#{first} #{last}"

这些字符串不仅可以包含变量,还可以包含表达式,例如:

str = "The count will be: #{count + 1}"
count = count + 1

1
这确实是真的,对于短插值非常好用。但是对于构建像HTML页面这样的长字符串来说就不太好了。请参见http://en.wikipedia.org/wiki/Schlemiel_the_painter%27s_Algorithm。 - James A. Rosen
1
当然可以,但是对于构建HTML页面,为什么不使用专门用于此功能的东西,比如HAML或ERB呢? - Earl Jenkins
1
关于“Schlemiel the Painter's algorithm”的问题,如果您像上面那样使用StringIO,则Joel Spolsky的批评不适用。 StringIO具有与文件一样的寻址指针。 每次无需重新计算字符串的结尾。 对于较长的字符串,您可以使用%{ }或Earl建议的库。 - CJ.

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