はじめに
Ruby の標準添付ライブラリに OpenStruct があります。
組み込みクラス Struct (扱い方がC言語の構造体に近い感じ)は使いにくい場面が多いので、私は OpenStruct をその代わりにすることが多いです。
OpenStruct の特徴
特徴は「attr on write」(書き込んだ時に属性が定義される; もともと Python にあった機能らしい)です。
Ruby のオブジェクトには外部から直接アクセスできる変数/属性(他言語でいう public 可視性の変数/属性)といったものはありませんが、値を書き込んだ時に setter/getter (アクセサメソッド)が作られるイメージです。
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
require 'ostruct'
# new の引数にハッシュを渡し、値を設定することもできる
st2 = OpenStruct.new(foo: 'hello', bar: 10)
p st2.foo #=> "hello" # 値を取得
p st2.bar #=> 10 # 値を取得
[]
,[]=
によるアクセス
上では setter/getter で値にアクセスしましたが、Hash のように []
,[]=
でも値にアクセスできます。
扱い方の雰囲気は、JavaScript のオブジェクトのプロパティに似ていると思います。
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 以外も使えます。
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)をキーとして扱う)
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 できないオブジェクトをキーにすると例外が発生します。
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をキーにできますが、私は可搬性に抵触するなどの理由で結局文字列をキーにすることが多いです)
以下は例です。
opts = ARGV.getopts 'abc:'
p opts['a']
p opts['b']
:
OpenStruct を使った場合
require 'ostruct'
opts = OpenStruct.new(ARGV.getopts 'abc:')
p opts.a # opts[:a] でも同じ
p opts.b # opts[:b] でも同じ
:
YAML 読み込みの例です。
---
# YAML サンプル
foo: hello, world
bar: have fun
require 'yaml'
hash = YAML.load open 'sample.yml'
p hash['foo'] #=> "hello, world"
p hash['bar'] #=> "have fun"
OpenStruct を使った場合
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 は each
や keys
などが使えないのでケースバイケースで使い分ける必要があります。
(※2015/01/05 追記) OpenStruct には each_pair
はあります。コメント欄を参照ください。
おわりに
本稿の動作確認は以下の環境で行っています。
- Ruby 2.1.5 p273
- Ubuntu Linux 14.04
ちょっと、追記
OpenStruct では to_sym するものをキーにするので文字列をキーにできます。
空文字列(''
) や 空白を含む文字列('a b'
) や数字だけの文字列('1'
) もキーにできます。
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 する、等の方法で呼び出すことはできます。
# ...上の続き (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 と書いても同じ
# ...上の続き (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の)メソッド名にならないかのようなキーがあったとしても、上のように何とかなります。