57

I recently read a nice post on using StringIO in Ruby. What the author doesn't mention, though, is that StringIO is just an "I." There's no "O." You can't do this, for example:

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

Ruby really needs a StringBuffer just like the one Java has. StringBuffers serve two important purposes. First, they let you test the output half of what Ruby's StringIO does. Second, they are useful for building up long strings from small parts -- something that Joel reminds us over and over again is otherwise very very slow.

Is there a good replacement?

It's true that Strings in Ruby are mutable, but that doesn't mean we should always rely on that functionality. If stuff is large, the performance and memory requirements of this, for example, is really bad.

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

The "correct" way to do this in Java is:

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

Though my Java is a bit rusty.

Ajedi32
  • 45,670
  • 22
  • 127
  • 172
James A. Rosen
  • 64,193
  • 61
  • 179
  • 261
  • "Mega Maid?" Never heard of her. Never really believed in StringBuffers either, but I always used them for fear of someone seeing my code. But really, does that stuff ever add up? – Dan Rosenstark May 06 '09 at 01:53
  • 3
    Probably a 'SpaceBalls' reference. – Stephen Eilert Aug 30 '10 at 16:59
  • 2
    Mega maid has been deleted as collateral damage from getting rid of profanity. – Andrew Grimm May 09 '11 at 03:52
  • Your string joining example is not equivalent to the Java code. As you mention, Ruby strings are mutable, so in Ruby you just do: `stuff.inject('') { |res, s| res << s.to_s }`. You can safely rely on Ruby strings being mutable, it's not going to change as it would break every single Ruby application in existence. – Theo May 09 '11 at 05:01
  • 1
    I really don't understand why StringIO doesn't have a to_s method. It's a class that manage a string, so if you want that string you have to specifically ask for it. It should have a to_s method since is the ruby convention, but it doesn't. (Someone can correct me if I'm wrong) – hcarreras Jul 16 '14 at 16:10
  • @Theo In Ruby 3, string literals would be immutable. However we can still use mutable `String.new` or [`+''`](http://ruby-doc.org/core-2.3.1/String.html#method-i-2B-40). – Franklin Yu Aug 14 '16 at 05:45

5 Answers5

124

I looked at the ruby documentation for StringIO, and it looks like what you want is StringIO#string, not StringIO#to_s

Thus, change your code to:

s = StringIO.new
s << 'foo'
s << 'bar'
s.string
Stefan
  • 109,145
  • 14
  • 143
  • 218
Mike Stone
  • 44,224
  • 30
  • 113
  • 140
36

Like other IO-type objects in Ruby, when you write to an IO, the character pointer advances.

>> s = StringIO.new
=> #<StringIO:0x3659d4>
>> s << 'foo'
=> #<StringIO:0x3659d4>
>> s << 'bar'
=> #<StringIO:0x3659d4>
>> s.pos
=> 6
>> s.rewind
=> 0
>> s.read
=> "foobar"
Colin Curtin
  • 2,093
  • 15
  • 17
26

I did some benchmarks and the fastest approach is using the String#<< method. Using StringIO is a little bit slower.

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)

Concatenating strings using the String#+ method is the slowest approach by many orders of magnitude:

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)

So I think the right answer is that the equivalent to Java's StringBuffer is simply using String#<< in Ruby.

sokkyoku
  • 2,161
  • 1
  • 20
  • 22
jmanrubia
  • 1,865
  • 20
  • 13
  • 1
    What version of ruby was used for this benchmark, please? – Jared Beck Mar 17 '14 at 03:03
  • 1
    Wow. So this should be the right answer as String is fastest. Tested on ruby 2.1.5 and same results. – Nikkolasg Dec 17 '14 at 18:05
  • and what happens when ruby makes strings immutable? Such micro-optimizations are a hell at the end. – akostadinov Dec 15 '16 at 16:39
  • What happens if you want to add some characters to very long string? I think `StringIO` then will be faster – nothing-special-here Feb 12 '17 at 22:01
  • The string concatenation solution won't work with frozen string literals, but `StringIO` works. – KARASZI István Mar 15 '20 at 09:00
  • Interesting to see how much faster ruby and computers together have become in the past years. On ruby 2.7.5 i got these results in order: 0.856717 (4.6x faster), 1.002285 (5.3x), 0.553703 (2.5x), 0.001409 (3.9x). I'm on a MBP 2017. – psmith Apr 25 '22 at 02:00
12

Your example works in Ruby - I just tried it.

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"

Unless I'm missing the reason you're using to_s - that just outputs the object id.

palmsey
  • 5,812
  • 3
  • 37
  • 41
3

Well, a StringBuffer is not quite as necessary in Ruby, mainly because Strings in Ruby are mutable... thus you can build up a string by modifying the existing string instead of constructing new strings with each concat.

As a note, you can also use special string syntax where you can build a string which references other variables within the string, which makes for very readable string construction. Consider:

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

These strings can also contain expressions, not just variables... such as:

str = "The count will be: #{count + 1}"
count = count + 1
Mike Stone
  • 44,224
  • 30
  • 113
  • 140
  • 1
    That's certainly true, and it's great for short interpolations. It's lousy for building long strings like HTML pages, though. See http://en.wikipedia.org/wiki/Schlemiel_the_painter%27s_Algorithm – James A. Rosen Jun 09 '09 at 12:15
  • 1
    Sure, but for building HTML pages, why not use something that's build for that function, like HAML or ERB? – Earl Jenkins Nov 02 '11 at 22:23
  • 1
    re: "Schlemiel the Painter's algorithm" If you are using StringIO as in the above, Joel Spolsky's criticism does not apply. StringIO has a seek pointer just like a file. There is no need to recompute the end of the string each time. And for longer strings you can use %{ } or a library like the ones Earl has suggested. – CJ. Dec 04 '13 at 05:38