Rubyの文字列とシンボルの違いをキッチリ説明できる人になりたい

  • 83
    いいね
  • 6
    コメント

Rubyには、文字列とシンボルという似て非なるものがある。
それぞれの使いドコロだったり、内部での扱われ方だったりはなんとなく分かってるつもりだったけど、いざ違いを説明しろと言われたら言葉に詰まるなーと思って整理してみる。

困ったときはリファレンスだ!ということで、Ruby2.3.0のシンボルのページを見てみたところ、ちょうどいい解説があったのでそれを参考に文字列との違いを見ていく。
http://docs.ruby-lang.org/ja/2.3.0/class/Symbol.html

シンボルとは何なのか?

文字列はまぁ文字列だけど、シンボルってどんなものだろう?
リファレンスによると、

Rubyの内部実装では、メソッド名や変数名、定数名、クラス名など の`名前'を整数で管理しています。これは名前を直接文字列として処理するよりも 速度面で有利だからです。そしてその整数をRubyのコード上で表現したものがシンボルです。

シンボルは、ソース上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在です。

名前を管理するという役割上、シンボルと文字列は一対一に対応します。 また、文字列と違い、immutable (変更不可)であり、同値ならば必ず同一です。

ふむふむ。要するに、文字列の皮を被った数値なのかー。
1つずつ検証してみよう。

メソッドや変数、定数、クラスを作るとシンボルができる?

Rubyの内部実装では、メソッド名や変数名、定数名、クラス名など の`名前'を整数で管理しています。

class HogeClass
end

def hoge_method
end

hoge_var = 0

HogeConst = 0

p Symbol.all_symbols
# -> [(既にいろいろシンボルができてる!), :HogeClass, :hoge_method, :hoge_var, :HogeConst, ...]

できてた!

文字列の比較よりシンボルの比較のほうが速い?

名前を直接文字列として処理するよりも 速度面で有利だからです。

require 'benchmark'

Benchmark.bm 10 do |r|
  str = "0123456789abcdef"
  str_hash = {
    "0123456789abcdef" => 1,
  }
  r.report "String" do
    100_000.times do
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
      str_hash[str]
    end
  end

  sym = :"0123456789abcdef"
  sym_hash = {
    "0123456789abcdef": 1,
  }
  r.report "Symbol" do
    100_000.times do
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
      sym_hash[sym]
    end
  end
end
                 user     system      total        real
String       0.080000   0.000000   0.080000 (  0.079430)
Symbol       0.030000   0.000000   0.030000 (  0.029106)

やってることは極端だけど…まぁ試すまでもなく文字列より数値の比較のほうが速いか。

シンボルはどこで書いても同じオブジェクト?

シンボルと文字列は一対一に対応します。 また、文字列と違い、immutable (変更不可)であり、同値ならば必ず同一です。

str1 = "hoge"
str2 = "hoge"
p str1.equal?(str2) #=> false
p str1.object_id    #=> 70332245439420
p str2.object_id    #=> 70332245439400

sym1 = :hoge
sym2 = :hoge
p sym1.equal?(sym2) #=> true
p sym1.object_id    #=> 901148
p sym2.object_id    #=> 901148

たしかに。
ということは何度書いてもオブジェクト数が増えなくて省メモリ。

before_obj_info = ObjectSpace.each_object.each_with_object(Hash.new 0) {|o, h|
  h[o.class]+=1
}

str1 = "hoge"
str2 = "hoge"
str3 = "hoge"
sym1 = :hoge
sym2 = :hoge
sym3 = :hoge

after_obj_info = ObjectSpace.each_object.each_with_object(Hash.new 0) {|o, h|
  h[o.class]+=1
}
p after_obj_info[String] - before_obj_info[String] #=> 3
p after_obj_info[Symbol] - before_obj_info[Symbol] #=> 0

なんでシンボルの数が増えてないんだろうと思って調べてみたら、最初の時点で:hogeができてた。オブジェクトが共通するということで、シンボルは最初に作られるのかなー。

ちなみに、Ruby2.1以前は、シンボルに対してGCが走らなかったらしい。シンボルの数だけ、実行中ずっと残り続けるオブジェクトができてしまうので、もし使う機会があれば注意が必要。

で、なんて説明しよう?

ここまでで、文字列とシンボルの違いが見えてきた。さて、なんて説明しよう?

文字列

その名の通り文字の列。文字列自体がデータであるときに使う。
たとえば氏名とか住所とかメッセージとか。

シンボル

文字列の皮を被った数値。コード上は文字列で見えてるけど内部では数値として扱われる。比較、検索に関して速度面で有利なので、主にデータの名前に使うと吉。
たとえばハッシュのキーだったり、メソッドに渡すクラス名/メソッド名/変数名/定数名だったりとか。ステータス名なんかも、文字列自体がデータではないのでシンボルを使うとよさげ。

補足としては、
- クラス名/メソッド名/変数名/定数名は定義すると勝手にシンボルを作って検索に備えている。
- シンボルの名前と数値は一対一対応なので、何回書いてもオブジェクト数が増えずメモリ的におトク。
- シンボルの名前はimmutable (変更不可)。でも、だからといって変更不可の文字列として使うのはちょっと違う。その場合はString#freezeでも使うといい。