かずおの開発ブログ(主にRuby)

日々の開発のことを色々書きます。

== と ===の違いについて

今回はRubyにおける==と===の違いについて調べました。 ===メソッドはwhen case文の中で内部的に使われているので、違いをわかっていないと思わぬところではまります。

先に結論から言っておくと

  • ===メソッドが拡張されていないクラス

    • ==と同じ
  • ===メソッドが拡張されているクラス

    • それぞれ定義された挙動

ということです。常に違う動きをするわけではないというのが、初めわかってなかったんですが、

これを理解するとスッと理解できました。

===メソッドが拡張されていないクラス

まずは===メソッドが拡張されていないクラスについて考えます。これは上記の通り==メソッドと同じ動きをするので、 例えば拡張されていないStringクラスでは

"hoge" == "hoge"
=> true
"hoge" === "hoge"
=> true

といったように==と===で同じ挙動を示します。

=== メソッドが拡張されているクラス

===メソッドが拡張されているクラスには以下の4つがあります。

  • Range
  • Regexp
  • Proc
  • Module, Class

1つずつどのような挙動の違いがあるのか確認していきましょう。

Range

Rangeの===は引数が自身の範囲内に含まれるなら真を返します。

(1..5) == 3 #これは同値性を判定しているのでfalse
=> false
(1..5) === 3 #拡張された===が呼び出される
=> true

ここで注意しないといけないのは

3 === (1..5)
=> false

左右を入れ替えるとfalseになるということです。これは3(Numeric)の===を呼び出しているため、拡張されていない===メソッドが呼び出されて同値性を判定した結果falseになるということです。このように===拡張されたクラスを扱う時は左右を入れ替えるだけで真偽値が変わってしまうので注意が必要です。

ちなみにRangeの実装は以下のようになっています(注: C言語)

/*
 *  call-seq:
 *     rng === obj       ->  true or false
 *
 *  Returns <code>true</code> if +obj+ is an element of the range,
 *  <code>false</code> otherwise.  Conveniently, <code>===</code> is the
 *  comparison operator used by <code>case</code> statements.
 *
 *     case 79
 *     when 1..50   then   print "low\n"
 *     when 51..75  then   print "medium\n"
 *     when 76..100 then   print "high\n"
 *     end
 *
 *  <em>produces:</em>
 *
 *     high
 */

static VALUE
range_eqq(VALUE range, VALUE val)
{
    return rb_funcall(range, rb_intern("include?"), 1, val);
}

Regexp

Regexpは引数の文字列がマッチした場合は真を返します。

/k/ == 'kazuya'
=> false
/k/ === 'kazuya'
=> true

実装は以下のようになっています。(cは辛いですね。。。)

/*
 *  call-seq:
 *     rxp === str   -> true or false
 *
 *  Case Equality---Used in case statements.
 *
 *     a = "HELLO"
 *     case a
 *     when /^[a-z]*$/; print "Lower case\n"
 *     when /^[A-Z]*$/; print "Upper case\n"
 *     else;            print "Mixed case\n"
 *     end
 *     #=> "Upper case"
 *
 *  Following a regular expression literal with the #=== operator allows you to
 *  compare against a String.
 *
 * /^[a-z]*$/ === "HELLO" #=> false
 * /^[A-Z]*$/ === "HELLO" #=> true
 */

VALUE
rb_reg_eqq(VALUE re, VALUE str)
{
    long start;

    str = reg_operand(str, FALSE);
    if (NIL_P(str)) {
    rb_backref_set(Qnil);
    return Qfalse;
    }
    start = rb_reg_search(re, str, 0, 0);
    if (start < 0) {
    return Qfalse;
    }
    return Qtrue;
}

Proc

Procついては一番トリッキーです。なんとblockの呼び出しができます。 Proc#===(arg) -> () argにブロックの引数を入れることができます。

proc = Proc.new{|a, b| puts a; puts b}
proc.call(1,2)
1
2
=> nil
proc === [1, 2]
1
2
=> nil

クエリ:Proc#=== | るりまサーチ なんでこんな実装になっているかというと、冒頭でも書きましたがwhen case文で使うためですね。 こんな感じでProcそのものの同値判定ではなくProcの戻り値で判定を行うことができます

proc = Proc.new{"hello"}
greeting = "hello"
case greeting
  when proc #ここでproc===が呼ばれる
    puts "this is hello"
end
=> "this is hello"

こちらも実装を確認してみましょう。 case文で使うための実装ですよということが書いてあります。

/*  Document-method: ===
 *
 *  call-seq:
 *     proc === obj   -> result_of_proc
 *
 *  Invokes the block with +obj+ as the proc's parameter like Proc#call.  It
 *  is to allow a proc object to be a target of +when+ clause in a case
 *  statement.
 */

/* CHECKME: are the argument checking semantics correct? */

/*
 *  call-seq:
 *     prc.call(params,...)   -> obj
 *     prc[params,...]        -> obj
 *     prc.(params,...)       -> obj
 *
 *  Invokes the block, setting the block's parameters to the values in
 *  <i>params</i> using something close to method calling semantics.
 *  Generates a warning if multiple values are passed to a proc that
 *  expects just one (previously this silently converted the parameters
 *  to an array).  Note that <code>prc.()</code> invokes
 *  <code>prc.call()</code> with the parameters given.  It's a syntax sugar to
 *  hide "call".
 *
 *  Returns the value of the last expression evaluated in the block. See
 *  also Proc#yield.
 *
 *     a_proc = Proc.new { |scalar, *values| values.collect { |value| value*scalar } }
 *     a_proc.call(9, 1, 2, 3)   #=> [9, 18, 27]
 *     a_proc[9, 1, 2, 3]        #=> [9, 18, 27]
 *     a_proc.(9, 1, 2, 3)       #=> [9, 18, 27]
 *
 *  For procs created using <code>lambda</code> or <code>->()</code> an error
 *  is generated if the wrong number of parameters are passed to a Proc with
 *  multiple parameters.  For procs created using <code>Proc.new</code> or
 *  <code>Kernel.proc</code>, extra parameters are silently discarded.
 *
 *     a_proc = lambda {|a,b| a}
 *     a_proc.call(1,2,3)
 *
 *  <em>produces:</em>
 *
 *     prog.rb:4:in `block in <main>': wrong number of arguments (given 3, expected 2) (ArgumentError)
 *         from prog.rb:5:in `call'
 *         from prog.rb:5:in `<main>'
 *
 */
#if 0
static VALUE
proc_call(int argc, VALUE *argv, VALUE procval)
{
    VALUE vret;
    const rb_block_t *blockptr = 0;
    const rb_iseq_t *iseq;
    rb_proc_t *proc;
    VALUE passed_procval;
    GetProcPtr(procval, proc);

    iseq = proc->block.iseq;
    if (RUBY_VM_IFUNC_P(iseq) || iseq->body->param.flags.has_block) {
   if (rb_block_given_p()) {
       rb_proc_t *passed_proc;
       passed_procval = rb_block_proc();
       GetProcPtr(passed_procval, passed_proc);
       blockptr = &passed_proc->block;
   }
    }

    vret = rb_vm_invoke_proc(GET_THREAD(), proc, argc, argv, blockptr);
    RB_GC_GUARD(procval);
    RB_GC_GUARD(passed_procval);
    return vret;
}
#endif

#if SIZEOF_LONG > SIZEOF_INT
static inline int
check_argc(long argc)
{
    if (argc > INT_MAX || argc < 0) {
    rb_raise(rb_eArgError, "too many arguments (%lu)",
         (unsigned long)argc);
    }
    return (int)argc;
}
#else
#define check_argc(argc) (argc)
#endif

VALUE
rb_proc_call(VALUE self, VALUE args)
{
    VALUE vret;
    rb_proc_t *proc;
    GetProcPtr(self, proc);
    vret = rb_vm_invoke_proc(GET_THREAD(), proc, check_argc(RARRAY_LEN(args)), RARRAY_CONST_PTR(args), 0);
    RB_GC_GUARD(self);
    RB_GC_GUARD(args);
    return vret;
}

VALUE
rb_proc_call_with_block(VALUE self, int argc, const VALUE *argv, VALUE pass_procval)
{
    VALUE vret;
    rb_proc_t *proc;
    rb_block_t *block = 0;
    GetProcPtr(self, proc);

    if (!NIL_P(pass_procval)) {
    rb_proc_t *pass_proc;
    GetProcPtr(pass_procval, pass_proc);
    block = &pass_proc->block;
    }

    vret = rb_vm_invoke_proc(GET_THREAD(), proc, argc, argv, block);
    RB_GC_GUARD(self);
    RB_GC_GUARD(pass_procval);
    return vret;
}

Module, Class

Module, Classは引数が自身または自身のサブクラスのインスタンスであれば真を返します

"hello" === String
 => false
String === "hello"
 => true

こちらも実装を確認しておきます

/*
 *  call-seq:
 *     mod === obj    -> true or false
 *
 *  Case Equality---Returns <code>true</code> if <i>obj</i> is an
 *  instance of <i>mod</i> or and instance of one of <i>mod</i>'s descendants.
 *  Of limited use for modules, but can be used in <code>case</code> statements
 *  to classify objects by class.
 */

static VALUE
rb_mod_eqq(VALUE mod, VALUE arg)
{
    return rb_obj_is_kind_of(arg, mod);
}

...

VALUE
rb_obj_is_kind_of(VALUE obj, VALUE c)
{
    VALUE cl = CLASS_OF(obj);

    c = class_or_module_required(c);
    return class_search_ancestor(cl, RCLASS_ORIGIN(c)) ? Qtrue : Qfalse;
}

って感じです。when caseの内部でしか基本使われないとはいえ理解しておいて損はないかと思います!