29
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby で s が nil か String か分らぬとき s.to_s と s || '' はどっちがいい?

29
Last updated at Posted at 2015-02-05

s に String か nil が入っているとき,nil を空文字列扱いにして処理したい場合があるよね。

その場合,s.to_ss || '' とどっちがいいんだろう?というお話。

nil.to_s は空文字列を返すのでどっちでも同じじゃん,と思いがちだけど,微妙な違いがある。文字コードが違うのだ。

p ''.to_s.encoding #=> #<Encoding:UTF-8>
p nil.to_s.encoding #=> #<Encoding:US-ASCII>

たいがい,この違いは気にしなくてもいい。UTF-8 が「ASCII 互換エンコーディング」なので,US-ASCII の文字列との間で比較や結合もできるし,index で互いに検索したり,splitscan の引数に使っても問題ない。

ただ,もし 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-literaltrue にしていない限り,凍結されていない空文字列を返す。そして評価されるたびに新たな String オブジェクトが作られる。

よって,s.to_ss || "" のどちらがよいかは,snil のときに凍結された空文字列が欲しいのかどうかも検討することになる。

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 || +""

と書けばよい。

29
25
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?