【Ruby 3.0 Advent Calendar 2020】OpenStruct で既存のメソッドを呼び出せるようになった【8日目】
Ruby 3.0 Advent Calendar 2020 8日目の記事になります。
昨日は Rubyで型チェック!動かして理解するRBS入門 〜サンプルコードでわかる!Ruby 3.0の主な新機能と変更点 Part 1〜 です。
今日は OpenStruct
について書きます。
OpenStruct について
OpenStruct
を使うと簡単な構造体を定義することができます。
require "ostruct" homu = OpenStruct.new(name: "homu", age: 14) # .new に渡したプロパティにアクセスできる pp homu.name # => "homu" pp homu.age # => 14 # 新しい要素を追加することもできる homu.id = 1 pp homu.id # => 1
こんな感じで任意のデータ構造を定義して値を参照したり追加したりすることができます。
Object クラスにメソッドが追加されると互換性の問題があった
この OpenStruct
なんですが Ruby 2.7 では Object
などで定義されているメソッド名と同じ名前だと値を参照する事ができません。
require "ostruct" # 各バージョン pp RUBY_VERSION # => "2.7.2" pp OpenStruct::VERSION # => "0.2.0" # Object#class や Object#hash と同じ名前で定義する obj = OpenStruct.new(class: "class", hash: "hash") # .new で定義した値は返ってこない… p obj.class # => OpenStruct p obj.hash # => 4198709777848673391
これは互換性の問題があり、例えば Ruby 2.5 で then
という名前で要素を定義していたとします。
require "ostruct" pp RUBY_VERSION # => "2.5.6" # then という名前で定義する obj = OpenStruct.new(then: "then") # .new で定義した値が返ってくる pp obj.then # => "then"
Ruby 2.5 では then
は有効な名前なのですが Ruby 2.6 で Object#then
という新しいメソッドが追加されました。
なので先程のようなコードは Ruby 2.5 では意図する動作をしていましたが Ruby 2.6 では壊れてしまいます。
require "ostruct" pp RUBY_VERSION # => "2.6.6" # then という名前で定義する obj = OpenStruct.new(then: "then") # Object#then が返ってくる… pp obj.then # => #<Enumerator: ...>
このように Object
クラスに新しいメソッドを追加する場合に OpenStruct
に対して互換性の問題がありました。
OpenStruct で既存のメソッド名で呼び出せるようになった
Ruby 3.0(正確にいうと最新の ostruct
gem なのですが)からは Object
のメソッドと同名でも定義できるようになりました。
require "ostruct" # 各バージョン pp RUBY_VERSION # => "3.0.0" pp OpenStruct::VERSION # => "0.3.1" # Object のメソッドと同名で定義できる obj = OpenStruct.new(class: "class", hash: "hash", then: "then") p obj.class # => "class" p obj.hash # => "hash" p obj.then # => "then"
これにより先程のような互換性の問題がなくなりました。
また、 OpenStruct
は gem なので gem install ostruct
すれば Ruby 3.0 未満でもこの機能を使うことができます。
# Ruby 2.7 でもこの機能を使える require "ostruct" # 各バージョン pp RUBY_VERSION # => "2.7.2" pp OpenStruct::VERSION # => "0.3.1" # Object のメソッドと同名で定義できる obj = OpenStruct.new(class: "class", hash: "hash", then: "then") pp obj.class # => "class" pp obj.hash # => "hash" pp obj.then # => "then"
これはべんり。
Object のメソッドを呼び出すには…?
Object
のメソッドを明示的に呼び出す場合は呼び出したいメソッド名に !
を付けて呼び出します。
require "ostruct" # 各バージョン pp RUBY_VERSION # => "3.0.0" pp OpenStruct::VERSION # => "0.3.1" # Object のメソッドと同名で定義できる obj = OpenStruct.new(class: "class", hash: "hash", then: "then") # これは各要素を返す pp obj.class # => "class" pp obj.hash # => "hash" pp obj.then # => "then" # これは Object のメソッドを呼びだす pp obj.class! # => OpenStruct pp obj.hash! # => 1305732522429585457 pp obj.then! # => #<Enumerator: ...>
また、該当する要素がない場合も Object
のメソッドを呼び出します。
require "ostruct" # 各バージョン pp RUBY_VERSION # => "3.0.0" pp OpenStruct::VERSION # => "0.3.1" obj = OpenStruct.new(class: "class", hash: "hash", then: "then") # 存在しない場合は Object のメソッドを呼び出す pp obj.to_s # => "#<OpenStruct class=\"class\", hash=\"hash\", then=\"then\">" # 要素を追加した場合はその要素を返す obj.to_s = "to_s" pp obj.to_s # => "to_s" pp obj.to_s! # => "#<OpenStruct class=\"class\", hash=\"hash\", then=\"then\", to_s=\"to_s\">"
うーむ、ちょっと複雑ですね…。
ちなみに !
付き名で定義した場合は Object
のメソッドを優先します。
require "ostruct" # 各バージョン pp RUBY_VERSION # => "3.0.0" pp OpenStruct::VERSION # => "0.3.1" obj = OpenStruct.new(class!: "class") # 保持してる要素ではなくて Object#class を返す pp obj.class! # => OpenStruct # OpenStruct#[] でアクセスできる pp obj[:class!]
むずかしい…。