見出し画像

Ruby シンボルの正体

Rubyを始めてからぼんやりしていていまいち実体が掴めていなかったシンボルについて現時点での理解をまとめてみます。

改めてリファレンスマニュアルをみると、以下のように書いてあります。

実装
Rubyの内部実装では、メソッド名や変数名、定数名、クラス名などの`名前'を整数で管理しています。これは名前を直接文字列として処理するよりも速度面で有利だからです。そしてその整数をRubyのコード上で表現したものがシンボルです。シンボルは、ソース上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在です。名前を管理するという役割上、シンボルと文字列は一対一に対応します。また、文字列と違い、immutable (変更不可)であり、同値ならば必ず同一です。

Ruby 2.7.0 リファレンスマニュアルより

つまり一言でいうとシンボルとは、オブジェクトの名前を整数で表現したもの、くらいでしょうか。その整数を使ってオブジェクトを参照する、と。

例えばある大学に同姓同名の鈴木一郎さんが二人いたとしても学籍番号で区別することができます。この学籍番号のようなものはRubyではobject_idと呼ばれています。

# Ruby
student1 = "ichiro suzuki"
student2 = "ichiro suzuki"

student1 == student2
=> true # 名前は一緒

student1.object_id
=> 70297613453420
student2.object_id
=> 70297605031860
 
student1.equal?(student2)
=> false  # 名前(文字列)は一緒でも別人(別オブジェクト)!

名前(文字列)が同じでも変数名が違ったら別オブジェクトです。ちなみにこれは文字列オブジェクトの場合の話で整数オブジェクトでは違った挙動になります。

student1_score = 755
student2_score = 755

student1_score.object_id
=> 1511
student2_score.object_id
=> 1511

stdent1_score.equal?(student2_score)
=> true  # 数値が同じなら同じオブジェクト!

整数オブジェクトではobject_idは同じです。数値が同じなら名前(変数名)がなんであろうと同じオブジェクトなんですね。

で、ここからがややこしくなるんですがinternメソッドを使ってそれぞれのシンボルを確かめてみます。

instance method String#intern
  intern -> Symbol
  to_sym -> Symbol
    文字列に対応するシンボル値 Symbol を返します。

Ruby 2.7.0 リファレンスマニュアルより
student1.intern
=> :'ichiro suzuki'

student2.intern
=> :'ichiro suzuki'

同じシンボルになります。
さらにこのシンボルのobject_idを確認すると・・・。

student1.intern.object_id
=> 70297605145300

student1.intern.object_id
=> 70297605145300

同じシンボルだから当然同じobject_idになります。

ええと、結局どういうことなの?と混乱してきましたが、要は整数のようにシンボルが同じだった場合は内部処理的には同じオブジェクトですよ、ということです。上に出てきた整数のような動きをシンボルはするんですね。これがリファレンスで言うところの、

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

なわけです。

で、結局シンボルは何が嬉しいの?ということですが、プロを目指す人のためのRuby入門にはシンボルの特徴が以下のようにあげられています。

・表面上は文字列っぽいので、プログラマにとって理解しやすい
・内部的には整数なので、コンピュータは高速に値を比較できる
・同じシンボルは同じオブジェクトであるため、メモリの使用効率が良い
・イミュータブルなので、勝手に値が変えられる心配がない

ほうほう。
というわけでどれくらい高速なのか2つめの特徴を検証してみました。

# trial_1
require 'benchmark'

city1 = :tokyo
city2 = :kyoto

5.times do
 result = Benchmark.realtime do
   10_000_000.times do
     city1 == city2
   end
 end
 puts "time #{result}s"
end 

city1とcity2をシンボルで定義して、それらが同じかどうか1000万回比較します。これを5回繰り返してそれぞれの処理時間を計測します。

# trial_2
require 'benchmark'

city1 = "tokyo"
city2 = "kyoto"

5.times do
 result = Benchmark.realtime do
   10_000_000.times do
     city1 == city2
   end
 end
 puts "time #{result}s"
end 

次はシンボルではなく文字列で定義して同じことをします。

実行結果はこちら。

# trial_1
=> time 0.7339030000002822s
=> time 0.7668919999996433s
=> time 0.7497819999989588s
=> time 0.7571870000010676s
=> time 0.7535530000004655s

# trial_2
=> time 0.9273950000006153s
=> time 0.9732490000005782s
=> time 0.9535189999987779s
=> time 0.9482060000009369s
=> time 0.9594880000004196s

シンボルの比較は 0.73-0.76s なのに対して文字列の比較は 0.92-0.97s ほどとなりました。そこそこ違いが出た印象です。


実際に自分でコードを書いて実感すると理解が進みますね。


サポートいただけましたら椅子から転げ落ちて喜びます。