4598


ある変数の型をチェックして、そのクラスによって処理を変えて、他のところに渡すような処理を考えていました。その処理自体は一行程度で済むので、どうせなら`case`で書き換えてもいいだろうと思って、書き換えてみたところ、上手く動いてくれません。

具体的には、以下のようなコードです:

[code]

     def type_check_with_if n
       if n.class == String
         "文字列だ！"
       elsif n.class == Hash
         "ハッシュだ！"
       end
     end
    
     def type_check_with_case n
       case n.class
       when String  then "文字列だ！"
       when Hash    then "ハッシュだ！"
       end
     end
    
     test_seed = ["foobar", {:foo => :bar}]
     p test_seed.map {|n| type_check_with_if n }
     # => ["文字列だ！", "ハッシュだ！"]
     p test_seed.map {|n| type_check_with_case n}
     # => [nil, nil]
    
[/code]

本当なら、`type_check_with_case`でも`["文字列だ！",
"ハッシュだ！"]`という出力を期待しているのですが、結果は上記の通り、`nil`の配列です。

もしかして、自分は`case`の仕様を何か勘違いしているのでしょうか? もし、勘違いしていたとするならば、参考情報を含めて教えて頂ければ嬉しいです。


CC BY-SA 3.0
2015-01-15T06:53:23.887
4596
2015-01-15T12:06:28.767
939
question
ruby
caseで変数のクラスを比較しようとすると上手く行かない


まずは正しいコードを紹介します。  
`n.class` の `.class` を消すと期待した通りに動きます。

[code]

    def type_check_with_case n
       case n
       when String  then "文字列だ！"
       when Hash    then "ハッシュだ！"
       end
     end
    
[/code]

以下はその理屈です。

case文そのものの仕組みについてはこちらのサイトで詳しく説明されているのでここでは割愛します。

<http://d.hatena.ne.jp/shibason/20090617/1245231492>

上のサイトでも説明されているとおり、case文では `===` メソッドが使われます。

つまり

[code]

    String === "str"
    => true
    
[/code]

ですが

[code]

    String === "str".class 
    => false
    
[/code]

です。

また、

[code]

    String.class
    => Class
    
[/code]

です。

`Class` クラスの `===` メソッドはどこで定義されているのかというと、親クラスの `Module` クラスで定義されています。

<http://docs.ruby-lang.org/ja/2.2.0/class/Class.html>

<http://docs.ruby-lang.org/ja/2.2.0/class/Module.html#I_--3D--3D--3D>

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

結構ややこしい話になっていますが、case文では `==` ではなく `===` で比較されているということを理解しておいてください。

[code]

    String == "str".class
    => true
    
[/code]

ですが

[code]

    String === "str".class 
    => false
    
[/code]

になるんです。

うまく説明できているかどうかわかりませんが、参考になれば幸いです。

P.S.

僕も今回初めて理屈をじっくり調べてみました。

[code]

       case n
       when String  then "文字列だ！"
       when Hash    then "ハッシュだ！"
       end
    
[/code]

みたいな書き方はイディオムとして当たり前のように使っているので、わざわざ理屈は考えていませんでした。  
僕と同じく大半の人は「それはそういうふうに動くようになっているから」という理解で止まっているんじゃないかな～と思ったりします。

P.S. その2

こういったRubyの言語仕様を疑問に思った場合は、以下の書籍を読んでみると詳しく説明されていたりします。

[プログラミング言語
Ruby](http://www.amazon.co.jp/gp/product/4873113946?ie=UTF8&camp=1207&creative=8411&creativeASIN=4873113946&linkCode=shr&tag=junic05-22&qid=1421319342&sr=8-1&keywords=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E+ruby)

書籍内のバージョンは1.9止まりなので最新のRubyの言語仕様は一部カバーできていませんが、それでも大半の内容は今でも有効なので、リファレンスとして一冊持っておくことをオススメします。


CC BY-SA 3.0
2015-01-15T07:34:39.910
4598
2015-01-15T11:00:21.783
2015-01-15T11:00:21.783
85
85
4596
answer


case は `===` での比較になるため

[code]

    def type_check_with_if n
      if n.class === String
        "文字列だ！"
      elsif n.class === Hash
        "ハッシュだ！"
      end
    end
    
    def type_check_with_case n
      case n.class
      when String  then "文字列だ！"
      when Hash    then "ハッシュだ！"
      end
    end
    
    test_seed = ["foobar", {:foo => :bar}]
    p test_seed.map {|n| type_check_with_if n }
    # => [nil, nil]
    p test_seed.map {|n| type_check_with_case n}
    # => [nil, nil]
    
[/code]

Class の `===` は、そのクラスまたはサブクラスのインスタンスとの比較で true となります。

[code]

    p String === ""  # => true
    
[/code]

そのため、次のように書き換えると意図したように動くと思います。

[code]

    def type_check_with_if n
      if String === n
        "文字列だ！"
      elsif Hash === n
        "ハッシュだ！"
      end
    end
    
    def type_check_with_case n
      case n
      when String  then "文字列だ！"
      when Hash    then "ハッシュだ！"
      end
    end
    
    test_seed = ["foobar", {:foo => :bar}]
    p test_seed.map {|n| type_check_with_if n }
    # => ["文字列だ！", "ハッシュだ！"]
    p test_seed.map {|n| type_check_with_case n}
    # => ["文字列だ！", "ハッシュだ！"]
    
[/code]


CC BY-SA 3.0
2015-01-15T07:40:22.243
4599
2015-01-15T07:40:22.243
2494
4596
answer


`case`は`===`で比較されるので云々は他の回答者がやっていますので別のことを補足します。

まずクラスオブジェクトに対する`===`演算子はほぼ以下のようなメソッドと同値です;

[code]

    class Class
      def ===(obj)
        obj.is_a?(self)
      end
    end
    
[/code]

Rubyにおいて以下の二つは全く同じ式です。というか後者は前者のシンタックスシュガー (読み書きしやすくするための特別な構文) です。

[code]

    String.===(obj)
    String === obj
    
[/code]

ここで何をいいたいのかというと、

`<クラス> == obj`が真になるのは`obj`がクラス`<クラス>`またはその派生クラスのインスタンスであるときである、ということです。

一般にクラスで処理を分岐するときは、それぞれのクラスにメソッドを追加して処理させる (多態を利用する) ように書き換えが可能です。質問にある処理ならば、

[code]

    class String
      def type_check
        "文字列だ！"
      end
    end
    
    class Hash
      def type_check
        "ハッシュだ！"
      end
    end
    
[/code]

のようにです。更にこのような独自のメソッドを定義しなかったとき全てのクラスの基底クラスである`Object`にメソッドを定義して
(Rubyのばあい厳密にはクラス`Object`にも基底クラスがありますが、そこはそれ、お察しください) デフォルトのメソッドが呼ばれるようにもできます。

[code]

    class Object
      def type_check
        "#{self.class}だ！"
      end
    end
    
[/code]

こうしておけば

[code]

    puts "".type_check # => 文字列だ！
    puts Hash.new.type_check # => ハッシュだ！
    puts //.type_check # => Regexpだ！
    
[/code]

などということもできます。

ただ、組み込みクラスにメソッドを追加することはあまりよくない作法であるので、実際にやるときは慎重に行なってください。特に`Object`クラスにメソッドを追加するのは影響範囲がでかいですし、まかり間違って既存のメソッドを上書きしてしまうととても不可解な動作をすることになるでしょう。劇薬と言ってもいいので慎重に行ってください
(大事なことなので2度いいました)。

ですが、自分で定義するクラスであればその恐れはありません。そして`case`を用いてクラスで場合分けををするよりも、この例のように多態を用いて書く方が、オブジェクト指向において一般にはよい作法と言われているようです
(議論はあると思います)。というか多態ってそもそもこういうことをやりたいがために存在しているのでしょうし。


CC BY-SA 3.0
2015-01-15T11:49:38.333
4614
2015-01-15T12:06:28.767
2015-01-15T12:06:28.767
3413
3413
4596
answer