mikanmarusanのブログ

テクノロジーとかダイビングとか

[Ruby] シンボルとハッシュ

TL;DR

  • シンボルの特徴
    • 同じ内容のシンボルはかならず同一のオブジェクト
    • ソースコード上では文字列のように見え、内部では整数として扱われる
  • キーがシンボルの場合、 {a => b}{a: b} と書き換えることができる

概要

単に自分が Ruby の newbie なのかもしれないが、Railsでシンボルを使ってハッシュを表現するときにどのように記述すべきか迷うことがある。Hashのキーに文字列を使うかシンボルを使うかとか、「=>」(ハッシュロケット、ファットアローなどともいう)を使うか使わないかなどである。

 {"key" => "value"} 
 {:key => "value"} 
 {:key => :value}
 {key:  "value"}
 {key: :value}

がどう違うかを説明できるようになって平昌オリンピックを迎えたい。
以下、特に指定がなければ Ruby 2.5.0(2017/12/28現在 最新版)での話とする。

シンボルとは「名前という概念を表すオブジェクト」

シンボル(Symbol)は正しくは Symbolクラスのオブジェクト。シンボルオブジェクトは、:symbol のようなリテラルで表現することができる。

マニュアルを要約すると、

  • ソースコード上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在
    • Rubyの内部実装では、メソッド名や変数名、定数名、クラス名など の名前を 整数 で管理していて、その整数をRubyのコード上で文字列のように表現したもの
  • 同じ内容のシンボルはかならず同一のオブジェクトになる

ということである。一言で言うと「名前という概念を表す整数オブジェクト」というところか。

同じ内容のシンボルはかならず同一のオブジェクト(同一のobject_id)

マニュアルによれば、「同じ内容のシンボルはかならず同一のオブジェクト」 ということである。これを実際に確認してみる。文字列の場合と比較するため、同じ内容のシンボルの場合と同じ内容の文字列の場合でそれぞれ比較する。

sym1 = :symbol
sym2 = :symbol

str1 = "string"
str2 = "string"

p sym1.object_id
p sym2.object_id

p str1.object_id
p str2.object_id
[実行結果]
# 同じ内容のシンボルは同一のオブジェクトになる(=object_idが同一)
384488
384488
# 同じ内容の文字列は異なるオブジェクトになる(=object_idが異なる)
70167279378560
70167279378540

この結果から、シンボルは、プログラム上では文字列にように表現しているが、内部では整数(object_id)として扱われていると言える。ここがマニュアルでいう ソース上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在 につながる。だからメモリ効率もよいし、(整数比較となるので)比較が高速になるということなのだろう。

シンボルのシンタックスシュガー

シンボルオブジェクトは、以下のようなリテラルでも取得することができる。以下は全て :symbol というシンボルになり全て同じ値になる(記法の違いのことをシンタックスシュガーと呼ぶ)。

p :symbol
p :"symbol"
p "symbol".to_sym
p %s(symbol) # %記法
[実行結果]
:symbol
:symbol
:symbol
:symbol

シンボルを使うとよいケース

「同じ内容のシンボルはかならず同一のオブジェクト」 という特徴を利用すると、複数あるとよろしくないものを作るときに向いている。例えば、ハッシュのキーとしてシンボルを使うことでキーに一意性を持たせることができる。

また、ソースコード上では文字列のように見え、内部では整数として扱われる」 という特徴を利用すると、C言語の列挙型(enum)のように、プログラムからは名前でアクセスする整数リスト(値は別に何であっても構わない)の使い方が想像できる。

Railsでは、ActiveRecord::Enum を使って列挙型を表現することができる。

class Membership < ActiveRecord::Base
  enum gender: [:female, :male, :other, :rather_not_say]
end

ハッシュアローを使うとき、使わないとき

マニュアルのHashクラスを見るとこう書かれている。

{a => b, ... } # aはキー、bは値となる
{s: b , ... } # { :s => b, ... } と同じ。キーがシンボルの場合の省略した書き方

ということは、下記の2つは記法の違いなので、ハッシュとしては同一となる。

 {:key => "value"} 
 {key:  "value"}

また、下記も記法の違いで同一である。

 {:key => :value}
 {key:  :value} 

念のため下記のサンプルコードで確かめてみる。

h1 = {"key" => "value"}
h2 = {:key => "value"}
h3 = {:key => :value}
h4 = {key:  "value"}
h5 = {key: :value}

p h1 === h2 # false
p h1 === h3 # false
p h1 === h4 # false
p h1 === h5 # false
p h2 === h3 # false
p h2 === h4 # true
p h2 === h5 # false
p h3 === h4 # false
p h3 === h5 # true
p h4 === h5 # false

{:key => "value"}{key: "value"} 、そして {:key => :value}{key: :value} が同じであることが確認できた。

まとめると、キーがシンボルの場合に限り、=> (ハッシュロケット)記法以外に、 : 記法(シンボルを表す前コロンは書かず、キーと値の間にコロンを置く)で表現できる。いいかえると、キーがシンボルの場合 {a => b}{a: b} と書き換えることができる ということだ。

Hashのキーがシンボルでも文字列でもアクセスできる

rubyのハッシュは、下記のサンプルの通り、キーがシンボルか文字列かを判別する。

hash = {key: "value"}
hash[:key]  #=> "value"
hash["key"] #=> nil

hash = {"key" => "value"}
hash[:key]  #=> nil
hash["key"] #=> "value"

だけど、Rails の Controller において リクエスト情報(クエリパラメータやPOSTで送られてくるデータ)が格納される params については、シンボルと文字列のどちらを指定しても同じ値が取得できる。

class ExamplesController < ApplicationController
  def index
    p params[:action]  # => 'index'
    p params['action'] # => 'index'
  end
end

これは、前回のエントリとど同様、Railsのコア拡張(ActiveSupport::CoreExtenstions::Hash)に含まれる、Hash#with_indifferent_access効果である。
中身としては比較的単純で、キーがシンボルだったら文字列に変換するようにしてあるだけだ。

参考までに、キーがシンボルでも文字列でもアクセスできる、Hashクラスを継承したHash2クラスの疑似コードを書く。

# Hash2クラス
class Hash2 < Hash
  def convert_key(key)
    key.kind_of?(Symbol) ? key.to_s : key
  end

  def [](key)
    super(convert_key(key))
  end

  def initialize(constructor = {})
    if constructor.respond_to?(:to_hash)
      super()
      update(constructor)

      hash = constructor.to_hash
      self.default = hash.default if hash.default
      self.default_proc = hash.default_proc if hash.default_proc
    else
      super(constructor)
    end
  end
end
# Hash2を使ったテスト
h = Hash2.new("key" => "value")
p h["key"]  # => 'value'
p h[:key] # => 'value'

TL;DR(再掲)

  • シンボルの特徴
    • 同じ内容のシンボルはかならず同一のオブジェクト
    • ソースコード上では文字列のように見え、内部では整数として扱われる
  • キーがシンボルの場合、 {a => b}{a: b} と書き換えることができる
  • 2017年も終わり、良いお年を!