🐙

[Misc #20509] Array#==(other) は other の定義によって挙動が変わるのでそれを明文化したいというチケット

2024/08/08に公開

[Misc #20509] Document importance of #to_ary and #to_hash for Array#== and Hash#==

  • Array#==(other) では other の定義によって挙動が変わるのでそれを明文化したいというチケット
  • どう挙動が変わるのかというと other.to_aryother.== が定義されている場合に依存します
  • どういうことかというと [1, 2, 3] == other のときに『 other.to_ary メソッドが定義されていれば other == [1, 2, 3] を呼び出す』という挙動になります
  • 例えば other.== が定義されているだけではそのメソッドは特に呼び出されません
class X
  # このメソッドは呼び出されない
  def ==(other)
    pp "X#==(#{other})"
    [1, 2, 3] == other
  end
end

x = X.new
pp [1, 2, 3] == x
# => false
  • しかし other.to_ary が定義されている場合は other.==([1, 2, 3]) メソッドが呼び出されて、その結果が返ってきます
class X
  # to_ary メソッドが定義されていればこのメソッドが呼び出されてその結果が変えてくる
  def ==(other)
    pp "X#==(#{other})"
    [1, 2, 3] == other
  end

  # このメソッドは実際には呼び出されない
  def to_ary(other)
    pp "X#to_ary(#{other})"
    []
  end
end

x = X.new
pp [1, 2, 3] == x
# => true

__END__
output:
"X#==([1, 2, 3])"
true
  • ただし #to_ary は『メソッドが定義されているかどうかの判定』に使われているだけなので実際にメソッドは呼び出されません
  • 逆にいうと #to_ary が定義されているだけで Array との比較は行われないことになります
class X
  def to_ary
    [1, 2, 3]
  end
end

x = X.new

# [1, 2, 3] == x は #to_ary を内部で呼び出すとかはない
pp [1, 2, 3] == x          # => false
pp [1, 2, 3] == x.to_ary   # => true
  • #to_ary を定義した上で Array#==(other) でも動くようにする場合は以下のように定義する必要があります
class X
  # to_ary の結果と比較するような演算子を定義する
  def ==(other)
    to_ary == other
  end

  def to_ary
    [1, 2, 3]
  end
end

x = X.new

pp [1, 2, 3] == x          # => true
pp [1, 2, 3] == x.to_ary   # => true
  • この挙動は Array#== だけではなくて Hash#== も同様の挙動になっています
  • なぜこういう挙動になっているのかの背景は以下のコメントに詳しく書いてあります
    • https://bugs.ruby-lang.org/issues/20509#note-3
    • CRuby の実装的に対象のオブジェクトが『配列かどうか』を内部で判定するときに『 #to_ary が定義されていれば配列オブジェクトとして扱う』みたいな感じになっているらしい
      • いわゆるダックタイピング的な呼び出しを行うための機構
      • なので #to_ary の結果は関係ない
GitHubで編集を提案

Discussion