Edited at

Ruby: OpenStruct のキーは to_sym される

More than 3 years have passed since last update.


はじめに

Ruby の標準添付ライブラリに OpenStruct があります。

組み込みクラス Struct (扱い方がC言語の構造体に近い感じ)は使いにくい場面が多いので、私は OpenStruct をその代わりにすることが多いです。


OpenStruct の特徴

特徴は「attr on write」(書き込んだ時に属性が定義される; もともと Python にあった機能らしい)です。

Ruby のオブジェクトには外部から直接アクセスできる変数/属性(他言語でいう public 可視性の変数/属性)といったものはありませんが、値を書き込んだ時に setter/getter (アクセサメソッド)が作られるイメージです。


sample1.rb

require 'ostruct'

st1 = OpenStruct.new
st1.foo = 'hello' # この時 foo/foo= が作られる
st1.bar = 10 # この時 bar/bar= が作られる

p st1.foo #=> "hello" # 値を取得
p st1.bar #=> 10 # 値を取得
p st1.baz #=> nil # 定義されてないものは nil



sample2.rb

require 'ostruct'

# new の引数にハッシュを渡し、値を設定することもできる
st2 = OpenStruct.new(foo: 'hello', bar: 10)

p st2.foo #=> "hello" # 値を取得
p st2.bar #=> 10 # 値を取得



[],[]=によるアクセス

上では setter/getter で値にアクセスしましたが、Hash のように [],[]=でも値にアクセスできます。

扱い方の雰囲気は、JavaScript のオブジェクトのプロパティに似ていると思います。


sample3.rb

require 'ostruct'

st3 = OpenStruct.new

st3[:foo] = 'hello' # `OpenStruct#[]=` で値を設定
p st3[:foo] #=> "hello" # `OpenStruct#[]` で値を取得
p st3.foo #=> "hello" # getter で値を取得

st3.bar = 10 # setter で値を設定
p st3[:bar] #=> 10 # `OpenStruct#[]` メソッドで値を取得
p st3.bar #=> 10 # getter で値を取得



[],[]=のキー

ところで、Hash のキーには Symbol を使うことが多いと思いますが、Symbol 以外も使えます。


sample4.rb

hash = {}

hash[:key] = 'hello' # キーは Symbol
hash['key'] = 'goodbye' # キーは String

p hash[:key] #=> "hello"
p hash['key'] #=> "goodbye"


Hash のキーは eql? メソッドで同じか否かの判定を行います。

なので、:key'key' は区別されます。

p :key.eql? 'key'   #=> false

これに対し、OpenStruct はキーを to_sym します。

(あるいは、キーとして与えられたオブジェクトを to_sym したもの(Symbol)をキーとして扱う)


sample5.rb

require 'ostruct'

p 'key'.to_sym #=> :key # 'key' を to_sym すると :key

st5 = OpenStruct.new
st5[:key] = 'hello'
st5[:key] #=> "hello"
st5['key'] #=> "hello"

st5['key'] = 'goodbye'
st5[:key] #=> "goodbye"
st5['key'] #=> "goodbye"

st5.foo = 100
st5[:foo] #=> 100
st5['foo'] #=> 100

# 適当なオブジェクトでも to_sym で :foo を返すようにすれば...
obj = Object.new
def obj.to_sym
:foo
end
p obj.to_sym #=> :foo

# ... :foo の値にアクセスできるようになる
st5[obj] #=> 100


OpenStruct では to_sym できないオブジェクトをキーにすると例外が発生します。


sample6.rb

require 'ostruct'

hash = {}
hash[1] = 100
hash[1] #=> 100

st6 = OpenStruct.new
st6[1] = 100 #!! NoMethodError: undefined method `to_sym' for 1:Fixnum



ARGV.getopts や YAML と一緒に使ってみる

:key'key' をキーとして同一視するなど OpenStruct にはクセがあります。

ですが、私はARGV.getopts や YAML (or JSON)と一緒に使ったりします。

ARGV.getopts も YAML も、キーが文字列なので Symbol にならんかな、といったものです。

(YAML の場合は、書き方次第でSymbolをキーにできますが、私は可搬性に抵触するなどの理由で結局文字列をキーにすることが多いです)

以下は例です。


sample7_before.rb

opts = ARGV.getopts 'abc:'

p opts['a']
p opts['b']
:


OpenStruct を使った場合


sample7_after.rb

require 'ostruct'

opts = OpenStruct.new(ARGV.getopts 'abc:')

p opts.a # opts[:a] でも同じ
p opts.b # opts[:b] でも同じ
:


YAML 読み込みの例です。


sample.yml

---

# YAML サンプル
foo: hello, world
bar: have fun


sample8_before.rb

require 'yaml'

hash = YAML.load open 'sample.yml'
p hash['foo'] #=> "hello, world"
p hash['bar'] #=> "have fun"


OpenStruct を使った場合


sample8_after.rb

require 'yaml'

require 'ostruct'

st8 = OpenStruct.new(YAML.load open 'sample.yml')
p st8.foo #=> "hello, world" # p st[:foo] でも同じ
p st8.bar #=> "have fun" # p st[:bar] でも同じ


文字列をキーに[]でアクセスするのが、私には居心地が悪いと感じるので、それが OpenStruct で解消されます。

ただし、Hash と異なり OpenStruct は eachkeys などが使えないのでケースバイケースで使い分ける必要があります。

(※2015/01/05 追記) OpenStruct には each_pair はあります。コメント欄を参照ください。


おわりに

本稿の動作確認は以下の環境で行っています。


  • Ruby 2.1.5 p273

  • Ubuntu Linux 14.04


ちょっと、追記

OpenStruct では to_sym するものをキーにするので文字列をキーにできます。

空文字列('') や 空白を含む文字列('a b') や数字だけの文字列('1') もキーにできます。


sample9.rb

require 'ostruct'

st9 = OpenStruct.new('' => :foo, 'a b' => :bar, '1' => :baz)
p st9[''] #=> :foo
p st9['a b'] #=> :bar
p st9['1'] #=> :baz


この時 setter/getter はどう呼び出すのでしょう?

無論、まっとうな方法では Syntax Error になり呼び出せません。

ただし、send メソッドを使う、method メソッドで Method オブジェクトとして取り出して call する、等の方法で呼び出すことはできます。


sample9_2.rb

# ...上の続き (getter の場合)

st9.send '' #=> :foo
st9.send 'a b' #=> :bar
st9.send '1' #=> :baz

st9.method('').() #=> :foo # st9.method('').call と書いても同じ
st9.method('a b').() #=> :bar # st9.method('a b').call と書いても同じ
st9.method('1').() #=> :baz # st9.method('1').call と書いても同じ



sample9_3.rb

# ...上の続き (setter の場合)

st9.send '=', 10
st9.send 'a b=', 20
st9.send '1=', 30

p st9[''] #=> 10
p st9['a b'] #=> 20
p st9['1'] #=> 30

st9.method('=').(40) # st9.method('=').call(40) と書いても同じ
st9.method('a b=').(50) # st9.method('a b=').call(50) と書いても同じ
st9.method('1=').(60) # st9.method('1=').call(60) と書いても同じ

p st9[''] #=> 40
p st9['a b'] #=> 50
p st9['1'] #=> 60


わざわざこのようなことをすることも無いと思いますが、YAML 内容から OpenStruct オブジェクトを作った時など、(setter/getterの)メソッド名にならないかのようなキーがあったとしても、上のように何とかなります。