simanのブログ

ゆるふわプログラマー。競技プログラミングやってます。Ruby好き

RubyのOpenStructソース読み

Rubyの標準ライブラリを読んでみようってことで今回は「OpenStruct」を読むことにしました。

OpenStructとは

OpenStructを使えばJavaScriptみたいにオブジェクトへの要素の追加や削除が簡単に出来るようになります。

require 'ostruct'

pos = OpenStruct.new(x: 10, y: 20)

puts "x = #{pos.x}"
puts "y = #{pos.y}"

pos.z = 30
pos.x = 40

puts "x = #{pos.x}"
puts "z = #{pos.z}"

実行結果

x = 10
y = 20
x = 40
z = 30

ということで早速ostruct.rbを読んでいきたいと思います。(バージョンは2.0.0-p247)


初期化部分

まずは初期化部分。initializeメソッド部分を抜粋

def initialize(hash=nil)
  @table = {}
  if hash
    hash.each_pair do |k, v|
      k = k.to_sym
      @table[k] = v
      new_ostruct_member(k)
    end
  end
end

ここではhash引数が渡されているかどうかをチェックして、何か渡されている場合はHash.each_pairを呼び出してその中身を繰り返し処理しています。

hash.each_pair do |k, v|
  k = k.to_sym
  @table[k] = v
  new_ostruct_member(k)
end

ブロックの中では@tableにキーを追加してnew_ostruct_memberメソッドを呼び出すという処理を行っています。(ちなみに@tableはOpenStructクラス内で使用されているインスタンス変数でHashです)


new_ostruct_member

def new_ostruct_member(name)
  name = name.to_sym
  unless respond_to?(name)
    define_singleton_method(name) { @table[name] }
    define_singleton_method("#{name}=") { |x| modifiable[name] = x }
  end
  name
end

new_ostruct_memberでは、まずrespond_to?メソッドを使用してメソッドが呼び出せるかどうかをチェックして、もし呼び出せない場合はdefine_singleton_methodを使用してメソッドを定義します。定義するメソッドは2つで、1つは値を読み込むためのメソッド。

define_singleton_method(name) { @table[name] }

そして、もう1つが値を書き込むためのメソッドです。

define_singleton_method("#{name}=") { |x| modifiable[name] = x }

ここでmodifiableメソッドが登場します。

def modifiable
  begin
    @modifiable = true
  rescue
    raise TypeError, "can't modify frozen #{self.class}", caller(3)
  end
  @table
end

ここでは「@modifiableに値を代入して、例外が発生しなければ@tableを返す」という処理を行っています。@modifialbeへの値の代入はOpenStructクラスのオブジェクトがfreezeされていた場合、例外が発生します。

例外が発生しない場合は、modifiableメソッドの返り値が@tableなので、@tableに対して[name]=とすることで代入を実現しています。

define_singleton_method("#{name}=") { |x| modifiable[name] = x }

ここまでが初期化の部分です。


要素の追加

オブジェクトに対しての要素の追加はmethod_missingを利用しています。

method_missing

def method_missing(mid, *args) # :nodoc:
  mname = mid.id2name
  len = args.length
  if mname.chomp!('=')
    if len != 1
      raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
    end
    modifiable[new_ostruct_member(mname)] = args[0]
  elsif len == 0
    @table[mid]
  else
    raise NoMethodError, "undefined method `#{mid}' for #{self}", caller(1)
  end
end

method_missingは存在しないメソッドを呼び出した時に呼ばれるメソッドで、上のサンプルの場合は

pos.z = 30

この部分がmethod_missingが呼ばれる部分となります。

method_missingが呼び出されると、呼びだそうとしたメソッド名が「name=」のような形式だった場合に、chomp!メソッドを使用して'='を取り除き、それをnew_ostruct_memberの引数として渡し実行し、新たに要素が追加します。


要素の削除

オブジェクトから要素を削除するためにはdelete_fieldメソッドを使用する必要があります。

def delete_field(name)
  sym = name.to_sym
  singleton_class.__send__(:remove_method, sym, "#{name}=")
  @table.delete sym
end

delete_fieldメソッドでは、remove_methodを使用して、引数で渡されたnameとname=メソッドを削除した後に、deleteメソッドを使用して@tableからも削除しています。。

require 'ostruct'

pos = OpenStruct.new(x: 10, y: 20)

p pos

pos.delete_field(:y)

p pos

実行結果

#<OpenStruct x=10, y=20>
#<OpenStruct x=10>

ここまでで大体の処理は読み終わりました。



始めて標準ライブラリを読んでみましたが、色々知らない構文を見ることが出来てよかったです。次もまた何か別のライブラリを読んでみようかなと思います。