ネットから HTML を取得し、 Nokogiri でパースして取れた項目をテキトーに Hash に入れて、それを配列に入れておく、ようなことをしょっちゅうしています。
Hash にしておくと当然ながら項目は Hash[:title] のように Hash#[] で取得することになります。メソッドじゃないです。ちょっと面倒だと思いつつ使っていました。
最近購入した
- 作者: Peter J. Jones,arton,長尾高弘
- 出版社/メーカー: 翔泳社
- 発売日: 2015/01/09
- メディア: 大型本
- この商品を含むブログ (13件) を見る
作り方(るりまを参考に)
dog = Struct.new("Dog", :name, :age) #=> Struct::Dog fred = dog.new("fred", 5) #=> #<struct Struct::Dog name="fred", age=5> Dog2 = Struct.new(:name, :age) #=> Dog2 fred2 = Dog2.new("fred", 5) #=> #<struct Dog2 name="fred", age=5> fred.class.ancestors #=> [Struct::Dog, Struct, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject] fred2.class.ancestors #=> [Dog2, Struct, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject] # PP::ObjectMixin は pry で組み込まれています。
Struct の二種類の作り方でできるインスタンスは見かけ上似ているけど違うことが分かりました。
参考:RubyのStructクラス - A_few_resources
なおこの二番目の方法は
名前のないクラスは、最初に名前を求める際に代入されている定数名を検索し、見つかった定数名をクラス名とします。
singleton method Class.new
という匿名クラスの性質と同じ動作でクラス名が決定しています。
作り方2:クラスにメソッドを生やす
Struct.new のサブクラスを使ったり、 Struct.new にブロックを渡すと、メソッド定義が行えます。
class Dog3 < Struct.new(:name, :age) #サブクラス def say "Bow!" end end #=> nil fred3 = Dog3.new("fred", 5) #=> #<struct Dog3 name="fred", age=5> [fred3.name, fred3.age, fred3.say] #=> ["fred", 5, "Bow!"] fred3.class.ancestors #=> [Dog3, #<Class:0x007fd67ccf3a00>, Struct, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject] Dog4 = Struct.new(:name, :age) do #ブロック渡し def say "Bow!" end end #=> Dog4 fred4 = Dog4.new("fred", 5) #=> #<struct Dog4 name="fred", age=5> [fred4.name, fred4.age, fred4.say] #=> ["fred", 5, "Bow!"] fred4.class.ancestors => [Dog4, Struct, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject]
継承関係を見ていると、四つ目のほうが良い感じがします。この書き方をスタンダードにしたいです。
ただし、ブロックなのでクラスのコンテキストが切り替わらず、例えばクラス内で定数を定義しても後で扱えないです(参考:RubyのStructのイディオムとアンドキュメンテッドな機能 - yarbの日記)。
またブロックでの再オープンは出来ないようです(定数再定義だと怒られたあとに再定義される)。でもクラス定義での再オープンはフツーに出来ますので、その手順でやりましょう。
Dog4 = Struct.new(:name, :age) do def eat "Yummy!" end end (pry): warning: already initialized constant Dog4 (pry): warning: previous definition of Dog4 was here #=> Dog4 Dog4.new.respond_to?(:eat) #=> true Dog4.new.respond_to?(:say) #=> false
クラス定義の再オープンはこんな感じ。
class Dog4 def eat "Yummy!" end end #=> :eat fred4.eat #=> "Yummy!"
無名クラスだと使いづらいので、ちゃんと名付けてあげましょう。
使い方
Dog5 = Struct.new(:name, :age) do def say "Bow!" end def eat "Yummy!" end end #=> Dog5 fred5 = Dog5.new("fred", 5) #=> #<struct Dog5 name="fred", age=5>
fred5.name # メソッドとしてアクセス #=> "fred" fred5.age #=> 5 fred5[0] # 配列的なアクセス(メンバのインデックス) #=> "fred" fred5[1] #=> 5 fred5[2] # 存在しないインデックスだと IndexError # IndexError: offset 2 too large for struct(size:2) fred5.size #=> 2
fred5["name"] # ハッシュ的なアクセス(文字列メンバ) #=> "fred" fred5[:name] # ハッシュ的なアクセス(シンボルメンバ) #=> "fred" fred5.age = 7 # setter #=> 7 fred5.age # getter #=> 7
fred5.to_a # 全要素の配列返し #=> ["fred", 7] fred5.values #=> ["fred", 7] fred5.keys # keys メソッドはない #NoMethodError: undefined method `keys' for #<struct Dog5 name="fred", age=5> fred5.members # 代わりに members メソッドを使う #=> [:name, :age] Dog5.members # クラスメソッド members でも得られる #=> [:name, :age]
るりまでの用語を読んでいると、ハッシュでの「キー・値」に相当する用語は Struct だと「メンバ・値」になるようです。
fred5.find { |item| item % 5 == 0 } # Enumerable のメソッドも使える #=> 5 fred5.map { |item| item } #=> ["fred", 7] fred5.each_pair { |m, v| puts "#{m} => #{v}" } name => fred age => 5 => #<struct Dog5 name="fred", age=5>
fred5.say # ブロックで設定したメソッド #=> "Bow!" fred5.eat #=> "Yummy!" fred5.height # 設定してないメソッドでは当然ながら NoMethodError #NoMethodError: undefined method `height' for #<struct Dog5 name="fred", age=7> fred5["height"] # ハッシュ的アクセスで存在しないキーの場合は NameError #NameError: no member 'height' in struct fred5["say"] # メソッドをハッシュ的アクセスすることは(当然)出来ない #NameError: no member 'say' in struct
参考1:ちゃぱてぃ商店IT部 @ ウィキ - ruby/サンプル/Ruby1.8 1.9 構造体のようなクラスを簡単に作るクラス「Struct」
参考2:
JavaScriptとかを見ていると、もうなんでもハッシュでいいんじゃないのという気もするわけだけど、ハッシュだと、
- 属性定義が動的すぎる
- 属性名のスペルミスに気付きづらい
ということ。逆に、実行時になるまで属性の数や種類が分からないときはハッシュのほうがいい。
RubyのStructのイディオムとアンドキュメンテッドな機能 - yarbの日記
参考3:
Struct vs. OpenStruct vs. Hash
Struct
- データを格納するオブジェクトが必要で、且つコードを書く時点で必要なメンバが分かっている
- ハッシュ的にも使いたい
- メンバが少ないクラスを簡単に作りたい
- メンバ名を間違えたときにエラーが出るようにしたい
OpenStruct
(略)
参考4:
ポイント1: ハッシュは動的、構造体は静的
ハッシュは存在しない要素を参照したり、要素をあとから追加できます。一方、構造体は初めに定義した要素しか扱えません。…実行時まで属性名が決まらないような場合は、ハッシュの方がきれいに対応できます。
ポイント2: 等価の基準が違う
構造体は、たとえ内容が全く同じでも、元になった構造体自体が違う場合、等価になりません。
Person = Struct.new(:name, :age) person = Person.new("Taro", 16) Robot = Struct.new(:name, :age) robot = Robot.new("Taro", 16) p person == robot # => falseポイント3: to_a の挙動が違う
ポイント4: 構造体はインスタンス生成に属性名がいらない
ポイント1の違いが一番大きいでしょう。参考リンク先では、例えば「文章中に出てくる単語をキー、その出現回数をバリューにしたいとき」のような動的な処理のときはハッシュがよいだろう、と結論付けられています。私もそう思います。
Ruby におけるハッシュ (Hash) と構造体 (Struct) の使い分け - Qiita
newとinitializeと
Dog = Struct.new(:name, :age) do def initialize(name, age) puts "I'm #{name}, #{age} years old. Bow!" end end fred = Dog.new("fred", 5) I'm fred, 5 years old. Bow! #=> #<struct Dog name=nil, age=nil>
Dog に initialize メソッドがあると Struct(で作ったクラス) の initialize メソッドが使われないので、 Struct のメソッドが使えません。
fred.name
#=> nil
なので super を使いましょう。
Dog = Struct.new(:name, :age) do def initialize(name, age) puts "I'm #{name}, #{age} years old. Bow!" super end end fred = Dog.new("fred", 5) I'm fred, 5 years old. Bow! #=> #<struct Dog name="fred", age=5> fred.name #=> "fred"
参考:Tips - Structクラスで簡単にクラスをつくろう | アルミナ解析室
なお、 Struct.new で作られたクラスの引数の数はチェックされず、少なければ nil が入り、多ければ切り捨てられます。引数の数までチェックするには initialize を入れておけばいいでしょう。
Dog2 = Struct.new(:name, :age) do def initialize(name, age) super end end fred = Dog2.new # ArgumentError: wrong number of arguments (0 for 2) fred = Dog2.new("fred", 5) #=> #<struct Dog2 name="fred", age=5>
その他、「本当に Struct でいいのかい?」を考えるヒントになるリンク↓
Struct inheritance is overused - The Pug Automatic
同値性を変えてみる
生まれた場所が同じ場合に「同地」(…くだらん^^;)であるとします。
Dog = Struct.new(:name, :age, :birth_place) do def same_birth_place?(other) birth_place == other.birth_place end end fred = Dog.new("fred", 4, "osaka") #=> #<struct Dog name="fred", age=4, birth_place="osaka"> john = Dog.new("john", 3, "osaka") #=> #<struct Dog name="john", age=3, birth_place="osaka"> fred.same_birth_place?(john) #=> true
このときに「同地であれば同値」、つまり fred.eql?(john) #=> true
になるようにしてみましょう。そのためには Dog#eql?
メソッドを作ると共に Dog#hash
メソッドも作る必要があります。
instance method Object#hash
オブジェクトのハッシュ値を返します。Hash クラスでオブジェク トを格納するのに用いられています。
メソッド hash は Object#eql? と組み合わせて Hash クラスで利用されます。その際
A.eql?(B) ならば A.hash == B.hash
の関係を必ず満たしていなければいけません。eql? を再定義した時には必ずこちらも合わせて再定義してください。
instance method Object#hash
では二つを作ります。上で見たようにクラスを再オープンすればいいですね。
class Dog def eql?(other) same_birth_place?(other) end def hash code = 17 code = 37 * code + birth_place.hash code end end
この「汎用ハッシュレシピ」は「プログラミング言語Ruby」p.233に載っていました。
- 作者: まつもとゆきひろ,David Flanagan,卜部昌平(監訳),長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2009/01/26
- メディア: 大型本
- 購入: 21人 クリック: 356回
- この商品を含むブログ (129件) を見る
EFFECTIVE JAVA 第2版 (The Java Series)
- 作者: Joshua Bloch,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2014/03/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (12件) を見る
この hash
メソッドの定義をもうちょっと一般化しておきましょう。
class Dog def hash %i[birth_place].inject(17) do |code, member| 37 * code + __send__(member).hash end end end
inject メソッドの前の配列に比較するためのメンバをいれておけばいいです。
もしくは、比較するためのメンバを %i[birth_place]
のように具体的に挙げるのではなく、逆に比較しないためのメンバ %i[name age]
を全メンバ Dog#members
から取り去るようにしてもよいでしょう。
class Dog def hash (members - %i[name age]).inject(17) do |code, member| 37 * code + __send__(member).hash end end end
何を以て比較したいか、何を外して比較したいか、視点の違いで選びましょう。
さて。
fred.eql?(john) #=> true fred.hash #=> 2080544753624017431 john.hash #=> 2080544753624017431 fred.hash == john.hash #=> true
となりました。
Array#-
で要素を削除する判断は hash
メソッドでの返値によるので、
[fred] - [john]
#=> []
という性質が Dog クラスに加わりました!
同じく、ハッシュキーの同値性は hash
メソッドでの返値で判断しますので
{ fred => 2 }[fred] #=> 2 { fred => 2 }[john] #=> 2
にもなります。
eql?
メソッドと hash
メソッドをいじって同値をいじる方法は Struct オブジェクトに限定されず、どんなクラスのオブジェクトでも使えます。ただし両方のメソッドを変更しないと効果が現れない点が重要です。