s に String か nil が入っているとき,nil を空文字列扱いにして処理したい場合があるよね。
その場合,s.to_s と s || '' とどっちがいいんだろう?というお話。
nil.to_s は空文字列を返すのでどっちでも同じじゃん,と思いがちだけど,微妙な違いがある。文字コードが違うのだ。
p ''.to_s.encoding #=> #<Encoding:UTF-8>
p nil.to_s.encoding #=> #<Encoding:US-ASCII>
たいがい,この違いは気にしなくてもいい。UTF-8 が「ASCII 互換エンコーディング」なので,US-ASCII の文字列との間で比較や結合もできるし,index で互いに検索したり,split や scan の引数に使っても問題ない。
ただ,もし UTF-8 でしか動かない処理に投入するのであれば,s.to_s は使ってはいけないことになる。
実際,そういう目に遭った。UTF-8 と CP932 の両方に対応した文字列処理メソッドで,encoding の値で場合分けしていたところ,想定外だった US-ASCII の空文字が来ちゃって死んだ。
2021-08-25 追記:Ruby 2.7 で nil.to_s は frozen
Ruby 2.7.0 で,nil.to_s にわずかな非互換性が導入された(ことを Ruby 3.0 時代になってから思い出した)。
このバージョンからは nil.to_s が凍結された空文字列を返すようになったのだ。しかも,何回実行しても常に同じオブジェクトが返される。
# Ruby 2.6 の場合
empty_strings = Array.new(3){ nil.to_s }
p empty_strings.first.frozen? # => false
p empty_strings.map{ |s| s.object_id }.uniq.count # => 3
# Ruby 2.7 以降の場合
empty_strings = Array.new(3){ nil.to_s }
p empty_strings.first.frozen? # => true
p empty_strings.map{ |s| s.object_id }.uniq.count # => 1
一方,文字列リテラル "" は,マジックコメントで frozen-string-literal を true にしていない限り,凍結されていない空文字列を返す。そして評価されるたびに新たな String オブジェクトが作られる。
よって,s.to_s と s || "" のどちらがよいかは,s が nil のときに凍結された空文字列が欲しいのかどうかも検討することになる。
2026-03-17 追記:s.to_s がマズいケースを具体的に
以下の文が誤解を招く気がしたので少し補足する。
ただ,もし UTF-8 でしか動かない処理に投入するのであれば,
s.to_sは使ってはいけないことになる。
「UTF-8 でしか動かさない処理」ではない。
「UTF-8 でなければ動かない処理」のことを書いた。
この記事を書いた当時,私が実際につまづいたのは,要するに以下のようなコードだった。
case str.encoding
when Encoding::UTF_8
# なんとかかんとか
when Encoding::CP932
# どうたらこうたら
else
raise "Unsopported encoding"
end
str は基本的に UTF-8 か CP932(=Windows 31J)のどちらかなのだが,あるところで nil だか文字列だか分からないオブジェクトを to_s したものがここに代入されるケースがあり,例外が発生してしまったのであった。
つまり,UTF-8 か CP932 のどちらかでなければ動かないコードになっていた。
筋の悪いやり方だった(私が書いたんだけども)。
UTF-8 も CP932 も US-ASCII とは互換性がある。たとえば以下のコードは例外を出さない:
str_ascii = "a".encode("US-ASCII")
str_utf8 = "あ".encode("UTF-8")
str_cp932 = "あ".encode("CP932")
p str_ascii + str_utf8
p str_utf8 + str_ascii
p str_ascii == str_utf8
p str_utf8.scan(str_ascii)
p str_ascii.scan(str_utf8)
p str_ascii + str_cp932
p str_cp932 + str_ascii
p str_ascii == str_cp932
p str_cp932.scan(str_ascii)
p str_ascii.scan(str_cp932)
だから,US-ASCII な文字列が来ても OK なようにつくるか,あるいはここに至るまでの文字コードをどちらかに倒すよう書くべきであった。
2026-03-17 追記:凍結されていない文字列が欲しいとき
2021-08-25 の追記についても補足する。
凍結されていない文字列が欲しいとき,s.to_s の場合は
+s.to_s
と文字列に対する単項演算子 + を付ければよいし,s || "" の場合は
s || +""
と書けばよい。