6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby: OpenStruct のキーは to_sym される

Last updated at Posted at 2015-01-03

はじめに

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の)メソッド名にならないかのようなキーがあったとしても、上のように何とかなります。

6
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?