かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Ruby Class を case 式で判定しようとしてハマったメモ。

Ruby は深く触ってないので初学者マンです。 Rails の複数のクラスで共通して使われている Decorator 内で case 式でクラスを判定して返す値を変えていた部分で self.class.name の文字列で判定するのではなく self.class で判定すればいいよ。とアドバイスをもらったのですが上手く動作させられなかったのでメモ

case 式だと .class だと上手く比較できない

class A
end
 
def class_type_check a
  p "a.class is #{a.class}"
  # => a.class is A"

  case a.class.name
  when 'A' then p "A"
  when 'B' then p "B"
  end
  # => "A"

  case a.class
  when A then p "A"
  when B then p "B"
  end
  # => 
end

class_type_check_with_case A.new

class_type_check に渡されるクラスオブジェクトaa.classA と表示されます。
クラス名を文字列にした a.class.name で比較する case 式では当然 when "A" が該当となります。
しかし、a.class で比較した case 式では a.classA であるにも関わらず when A の部分が該当となっていませんでした🤔

a.class ではなくa だと上手く動作しました。

class A
end
 
def class_type_check a
  case a
  when A then p "A"
  when B then p "B"
  end
  # => "A"
end

class_type_check_with_case A.new

なーぜー? 🤔🤔🤔
a.class で判定する場合 a.class はAクラスのインスタンスで A そのものではないからオブジェクトIDとかが違って同一オブジェクトではないとかなのでしょうか???

case 式は === で判定している

case は一つの式に対する一致判定による分岐を行います。when 節で指定された値と最初の式を評価した結果とを演算子 === を用いて 比較して、一致する場合には when 節の本体を評価します。
つまり、

case 式0
when 式1, 式2
  stmt1
when 式3, 式4
  stmt2
else
  stmt3
end
は以下の if 式とほぼ等価です。
tmp = 式0
if 式1 === tmp or 式2 === tmp
  stmt1
elsif 式3 === tmp or 式4 === _tmp
  stmt2
else
  stmt3
end

ref. 制御構造 (Ruby 2.6.0)

. (はてなブログ、引用の間に空白以外の何か挟まないと引用を連続して書けないのか…

case式は===演算子を使ったif〜elsif〜end式と等価な処理を行うように実装されている
基本的にこの===演算子は、サブクラスで再定義されていない限り、単にObject#==を呼び出すだけ
ref. Rubyのcase式と===演算子について - しばそんノート

Rubyの case 式は if whenの条件 === case の比較する値 と等価で、 === JavaScriptとかと違ってより厳密に型とかチェックする演算子ではなく、左辺のオブジェクトで定義されている === を呼び出すというものという事っぽい。

上の例を if文に置き換えてみるとこんな感じ

a = A.new
p a.class
# => A
if A === a.class
  p "A"
else
  p false
end
# => false

class クラス=== で判定する結果が返されるということっぽい。
class クラスの === は親クラスの Module クラスで定義されている。

self === obj -> bool
指定された objself かそのサブクラスのインスタンスであるとき真を返します。 また、objself をインクルードしたクラスかそのサブクラスのインスタンスである場合にも 真を返します。上記のいずれでもない場合に false を返します。
言い替えると obj.kind_of?(self)true の場合、 true を返します。
ref. class Module self === obj -> bool

a.class == A なら true だけど、 === の場合は a.class.kind_of?(A)という判定になる。 a.classAは Aクラスそのもの(self)ではないし、a.classAは Aのサブクラスのインスタンスでもないから false ということっぽい。

なので case a としている場合は

a = A.new
if A === a
  p "A"
else
  p false
end
# => "A"

a は Aクラスのインスタンスだから true ということになるっぽい。 (ちゃんと理解している訳ではない

case a.class が当てはまる例

a.classA は Class クラスのオブジェクトなので、case a.class は Class クラスより親のクラスであれば true になる 。

class A
end

a = A.new
# XXX === a.class は a.class.kind_of?(XXX) と等価なので
a.class.kind_of?(Class)
# => true
a.class.kind_of?(Module)
# => true
a.class.kind_of?(Object)
# => true

👇

def class_type_check a
  case n.class
  when A then p "A"
  when B then p "B"
  when String then p "String"
  when Class then p "Class"
  when Module then p "Module"
  end
  # => "Class"
end

class_type_check A.new

完 全 理 解 !

ポエム

JavaScriptの不思議な挙動試してみるのとか好きだったし、こういう言語の挙動みたいな部分面白いからまたちゃんとRuby再入門しようかな〜って気持ちになってきた。(不思議な挙動を抑制されるTypeScriptを枷のように感じてるし、今の所まだ Rails の良さを感じられるに至ってい。disるつもりはなくってnot for meって感じなのだ。こんなんだから僕は稼げるエンジニアにはなれないのだろう…


[参考]

改訂2版 パーフェクトRuby

改訂2版 パーフェクトRuby