Ruby TIPS

Ruby TIPS

数値/文字列/配列/範囲式/正規表現の比較を行うには?

2017年3月16日

Rubyプログラミングでは「等しいかどうか」を調べるための比較はどう行うのか? 比較を行える演算子やメソッドを使って、さまざまな比較を試してみる。

ローグ・インターナショナル 羽山 博
  • このエントリーをはてなブックマークに追加

 プログラムの中で「等しいかどうか」を調べることは多い。ただし、日常の感覚とは違って、さまざまな種類の「等しい」があることに注意が必要だ。今回は、==演算子や===演算子、eql?メソッド、equal?メソッドを取り上げ、それらの働きを確認していく。

比較のための演算子やメソッド

 Rubyでは、数値や文字列の大小を比較するために、さまざまな演算子やメソッドが利用できる。今回は、等しいかどうかの比較に絞って、簡単な実験を行ってみたい。値が等しいのか、同一のオブジェクトであるかなどによって結果が異なってくるので、それぞれの機能をきちんと確認しておこう。日常的な「等しい」という漠然とした感覚で捉えていると、思わぬ落とし穴に陥ることになりかねない。

 等しいかどうかを比較するための演算子やメソッドには、以下のようなものがある

  • == 値が等しい
  • === case式の中で使われたり、範囲に含まれるかどうかを調べたりするのに使う
  • eql? 同一のクラスであり、==で比較した結果が等しい(オブジェクトのハッシュ値が等しい)
  • equal? 同一のオブジェクトである(object_idが等しい)

 これらの動作を1つ1つ確かめてみようというわけである。なお、equal?以外は、最適な比較ができるように各クラスで再定義されているのが普通である。従って、「等しいかどうか評価する」という機能は同じだが、クラスによって動作が異なっている。===の働きは==と似ているが、範囲や正規表現などを利用するときに注意すべきポイントがあるので、後半で触れることとする。では、数値の比較から見ていこう

数値の比較を行う

 整数はFixnumクラスのインスタンスであり、浮動小数点数はFloatクラスのインスタンスである。数値を==演算子で比較するときには型変換が行われるので、値が等しければtrueが返される。ただし、浮動小数点数では誤差が発生することがあるので、値が等しいと思っていてもそうならないことがある。当然のことながら、FixnumクラスとFloatクラスはクラスが異なるので、eql?メソッドやequal?メソッドで比較するとfalseが返される。

 今回は簡単な実験を数多く行うので、Rubyの対話型環境であるirbInteractive Ruby)を利用しよう。irbでは式の値がすぐに表示されるので、ちょっとした動作確認や実験が簡単にできる。以下、$をシェルのプロンプト、>をirbのプロンプトとし、適宜、空行を入れて見やすくしておく。

ターミナル
$ irb
> x,y=1,1.0
=> [1, 1.0]

> x.class
Fixnum          # xはFixnumクラス
> y.class
Float           # yはFloatクラス

> x==y          # xとyは値が等しいか?
=> true
> x.eql?(y)     # xとyは同じクラスで同じ値か?
=> false
> x.equal?(y)   # xとyは同じオブジェクトか?
=> false

> 0.1*10*3==3   # 左辺は3.0、右辺は3
=> true
> 0.1*3*10==3   # 左辺は3.0000000000000004、右辺は3
=> false
実行例1.1 さまざまな方法で数値を比較する

前半部分のいくつかの比較は、常識的な感覚で理解できる結果だろう。後半部分では誤差が発生する場合とそうでない場合があるので、注意が必要だ。0.1*101.0となるが、0.1*30.30000000000000004になる。

 なお、多重代入を使って、複数の変数に値を代入するときには、x,y=1,1のように変数をカンマで区切って左辺に書く。x=1, y=1と書きたくなるかもしれないが、それでは期待した通りにはならない。変数y1が代入された後、変数x1,yが代入されるので、変数xは配列[1,1]を参照することになる。

 念のため、ハッシュ値とobject_idの値も確認しておこう(実行例1.2)。実行例1.1のように異なる値が代入されているといずれの値も異なるが、同じ値が代入されていると、ハッシュ値もobject_idの値も同じになる。

ターミナル
> x,y=1,1                # 同じ値を代入する
=> [1, 1]

> x.hash
=> 1683040744889400978   # xのハッシュ値を表示してみる
> y.hash
=> 1683040744889400978   # yのハッシュ値を表示してみる

> x.object_id            # xのobject_idを表示してみる
=> 3
> y.object_id            # yのobject_idを表示してみる
=> 3
実行例1.2 数値のハッシュ値やobject_idを表示する

ハッシュ値とは、オブジェクトの値を格納したり検索したりするのに使われる値である。
object_idは、オブジェクトを一意に識別するための値である。
数値(=FixnumクラスやFloatクラスのインスタンス)の場合、値が同じであればハッシュ値も同じであり(このコードでは1683040744889400978)、object_idも同じである(このコードでは3)。object_idは毎回異なるので、このコード例と同じ値にはならない(特にハッシュ値は異なる結果になる)ことに注意してほしい。

 irbではキーを押せば、以前実行した式をさかのぼって表示できるので、似たような式を入力するときに便利である。irbを終了させるには、プロンプトの直後でCtrlDキーを押すか、exitコマンドまたはquitコマンドを入力するよい。

文字列の比較を行う

 続いて文字列の比較を行ってみよう。同じ値の文字列であれば、==演算子でもeql?メソッドでもtrueが返される。しかし、equal?メソッドで比較した場合、同じ文字列を参照しているかどうかによって結果が異なる。数値でも文字列でも、値が等しいかどうかを調べるには==演算子を使うので、問題になることはないかもしれないが、equal?メソッドを使うときは要注意である。

ターミナル
$ irb
> a,b="hoge","hoge"
=> ["hoge", "hoge"]

> a==b          # aとbは値が等しい
=> true
> a.eql?(b)     # aとbは同じクラスで同じ値か?
=> true
> a.equal?(b)   # aとbは同じオブジェクトか?
=> false

> a.object_id   # aのobject_idを表示してみる
=> 70240432335320
> b.object_id   # bのobject_idを表示してみる
=> 70240432335300

> a.hash        # aのハッシュ値を表示してみる
=> -1806074491870963302
> b.hash        # bのハッシュ値を表示してみる
=> -1806074491870963302
実行例1.3 さまざまな方法で文字列を比較する

同一のオブジェクトであるかどうかということは、object_idが等しいかどうかということである。後半部分で変数aと変数bobject_id7024043233532070240432335300で異なっていることを確認しておこう。また、eql?メソッドの働きを確認するために、ハッシュ値も表示してみた。

 実行例1.3を見ると、変数aと変数bobject_idが異なるので、異なるオブジェクトである(厳密には、異なるオブジェクトを参照している)が、ハッシュ値が同じなので、参照先の文字列は同じ値であるということが分かる。なお、object_idもハッシュ値も、必ずしも決まった値が与えられるわけではなく、毎回異なる値が与えられる。

 続いて、変数aと変数bが同じオブジェクトを参照するようにして、比較を行ってみる。

ターミナル
> a=# bの参照をaに代入する(同じ文字列を指す)
=> "hoge"
> a==b         # aとbは値が等しい
=> true
> a.eql?(b)    # aとbは同じクラスで同じ値か?
=> true
> a.equal?(b)  # aとbは同じオブジェクトか?
=> true

> a.object_id   # aのobject_idを表示してみる
=> 70240432335300
> b.object_id   # bのobject_idを表示してみる
=> 70240432335300
実行例1.4 変数が同じ文字列を参照するようにして比較してみる

今度は、equal?メソッドの結果がtrueになった。後半部分で変数aと変数boject_id7024043233530070240432335300で同じになっていることを確認しておこう。

【コラム】文字列のエンコーディングと比較

 文字列の値が等しいかどうかは、バイト列が等しいかどうかで調べられるので、エンコーディングによって結果が異なることがある。例えば、半角英数字はUTF-8でもShift_JISでも同じコードだが、UTF-16では異なる。全角文字はUTF-8とShift_JISでは異なるコードである。以下の例は、デフォルトのエンコーディングがUTF-8の場合の簡単な例である。

ターミナル
$ irb
> a,b="hoge","hoge"
=> ["hoge", "hoge"]
> a.encode!("Shift_JIS")  # aのエンコーディングをShift_JISに変更
=> "hoge"
> a==b   # aとbは値が等しい
=> true

> a.encode!("UTF-16LE")  # aのエンコーディングをUTF-16LEに変更
=> "hoge"
> a==b   # aとbは値が等しいか(見た目では同じに見えるが……)
=> false

> c,d="日本語","日本語"
=> ["日本語", "日本語"]
> c.encode!("Shift_JIS")  # cのエンコーディングをShift_JISに変更
=> "\x{93FA}\x{967B}\x{8CEA}"
> c==# cとdは値が等しい
=> false

> c.encoding  # cのエンコーディングを表示してみる
=> #<Encoding:Shift_JIS>
> d.encoding  # dのエンコーディングを表示してみる
=> #<Encoding:UTF-8>
実行例1.3 エンコーディングの異なる文字列を比較する

日本語入力で文字化けする場合は、最初のirbコマンドをirb --noreadlineに変えて実行してみてほしい。
==演算子やeql?メソッドの評価はバイト列が等しいかどうかによって決められる。見た目が同じであっても、エンコーディングが異なるとバイト列が異なることがあるので、値が等しくなるとは限らない。

配列の比較を行う

 配列の比較も文字列の比較と似ている。まず、同じ値を持つ、異なる配列を作成した場合の例を見てみよう。結果はだいたい想像できると思うが、equal?メソッドで比較した場合だけfalseとなる。以下の例では、変数xで参照される配列を単に配列xと呼び、変数yで参照される配列を単に配列yと呼ぶことにする。

ターミナル
$ irb
> x=[0,1]
=> [0, 1]
> y=[0,1]
=> [0, 1]

> x==y         # xとyは値が等しい
=> true
> x.eql?(y)    # xとyは同じクラスで同じ値か?
=> true
> x.equal?(y)  # xとyは同じオブジェクトか?
=> false

> x[0]=2       # 配列xの要素を変更
=> 2
> y    # 配列yの要素を表示。配列xと配列yは別のものなので値は変わらない
=> [0, 1]
実行例1.4 同じ値を持つ、異なる配列を比較する

 配列xと配列yは、同じ値を持つが、異なるオブジェクトである。実行例の最後で、配列xの要素を変更しているが、もちろん配列yの要素は変わらない。

 次に参照を代入した場合である。この場合は、同じオブジェクトを参照するので、object_idも等しくなる。

ターミナル
> x=[0,1]
=> [0, 1]
> y=x          # xの参照をyに代入
=> [0, 1]

> x==y         # xとyは値が等しい
=> true
> x.eql?(y)    # xとyは同じクラスで同じ値か?
=> true
> x.equal?(y)  # xとyは同じオブジェクトか?
=> true

> x[0]=2       # 配列xの要素を変更
=> 2
> y    # 配列yの要素を表示。変数xと変数yは同じ配列を参照しているので値が変わる
=> [2, 1]
実行例1.5 同じ配列を参照する2つの変数を使って比較する

変数xと変数yは同じ配列を参照するので、equal?メソッドの結果もtrueとなる。配列xの要素を変更すると、配列yの要素も変わる。

範囲式の比較を行う

 範囲式で===演算子を利用すれば、値が範囲内にあるかどうかが調べられる。数値や文字列では、==演算子と===演算子の働きに違いはないが、範囲式では==と働きが異なるので注意が必要。なお、===演算子は左辺をレシーバーとするので、左辺と右辺を入れ替えると結果が異なることにも注意が必要だ。

ターミナル
$ irb
> (0..10)===5  # 0以上10以下の範囲に5が含まれる
=> true
> (0..10)==5   # 0以上10以下の範囲と5は等しい
=> false

> 5===(0..10)  # 5と0以上10以下の範囲は等しい
=> false
実行例1.6 ===演算子は左辺をレシーバーとして評価が行われる

===演算子の左辺と右辺を入れ替えると、オブジェクトによっては結果が異なることに注意。

 ===演算子は各クラスで再定義されているので、そのクラスの働きに合った機能を持つ。===演算子では左辺がレシーバーとなるので、左辺に範囲式を書けば、Rangeクラスの===演算子が呼び出される。従って範囲内にあるかどうかが正しく判定される。一方、左辺に整数を書けばFixnumクラスの===演算子が呼び出される。この場合は、整数と等しいかどうかを調べるので、右辺に範囲式を書くと正しく判定されない。

 ===演算子の機能はcase式のwhen節に書かれた式を評価するときにも使われる。when節に書かれた式がレシーバーとなるので、以下の2つの式は同じ動作となる。

ターミナル
$ irb
> case 5
> when 0..10   # 0..10がレシーバーになる
> p "範囲内"
> else
* p "範囲外"
> end
"範囲内"
実行例1.7 case式ではwhen節に書かれた式をレシーバーとした===演算子の評価が行われる

日本語入力で文字化けする場合は、最初のirbコマンドをirb --noreadlineに変えて実行してみてほしい。この例では、whenの後に書かれた0..10がレシーバーとして扱われるので、実行例1.6の1行目と同様の演算((0..10)===5)が行われることが分かる。なお、プロンプトの*は「行が継続している」という意味である。

正規表現の比較を行う

 文字列が正規表現に一致するかどうかを調べるには、一般には=~演算子を利用するので、==演算子や===演算子を使うことはあまりないかもしれない。ただし、上で見たように、case式の中で正規表現が使われることがあるので、===演算子の働きについては確認しておいた方がいいだろう。

 =~演算子は、正規表現に一致する文字列の位置を返す(一致しなければnilを返す)が、===演算子は一致するかどうかを返す(一致すればtrueを返し、一致しなければfalseを返す)。以下の例で、正規表現/c.e/は「cで始まり、任意の1文字が続き、eで終わる文字列」を表す。

ターミナル
$ irb
> /c.e/=~"abcde"   # /c.e/が、"abcde"のどの位置に含まれているか
=> 2  # 2文字目から一致する(先頭が0)

> /c.e/==="abcde"  # /c.e/に"abcde"は一致する
=> true
> /c.e/=="abcde"   # /c.e/と"abcde"は等しい
=> false
> "abcde"===/c.e/  # "abcde"と/c.e/は等しい
=> false
実行例1.8 正規表現では、===演算子は正規表現に一致するかどうかが評価される

===演算子は左辺をレシーバーとするので、左辺に正規表現を書いた場合はRegexpクラスの===演算子が呼び出される。従って、正規表現に右辺の文字列が一致しているかどうかが正しく判定される。一方、左辺に文字列を書くと、Stringクラスの===演算子が呼び出される。この場合、文字列と正規表現が等しいかどうかを調べるので、正しく判定されない。

 case式の中でwhen節の後に正規表現を書いた場合も実行例1.4で見たのと同様、when節の後に書かれた正規表現をレシーバーとして===演算子の評価が行われる。念のため、このコードも見ておこう。

ターミナル
$ irb
> case "abcde"
> when /c.e/    # /c.e/がレシーバーになる
> p "一致"
> else
* p "不一致"
> end
"一致"
実行例1.9 case式のwhen節の後に正規表現を書いた例

この例では、whenの後に書かれた/c.e/がレシーバーとして扱われるので、実行例1.8で===演算子を使った場合と同様の演算が行われる。

まとめ

 ==演算子は値が等しいかどうかを調べるのに使う。===演算子は左辺をレシーバーとして等しいかどうかを調べるのに使う。

 また、case式を使った条件分岐では、===演算子を使った比較が行われる。その場合、when節の後に書かれた式がレシーバーとなる。

 eql?メソッドは同じクラスであり、同じ値であるか(ハッシュ値が等しいかどうか)という評価を行う。

 equal?メソッドは同じオブジェクトであるかどうか(object_idが等しいかどうか)という評価を行う。同じ値を持つ場合でも、異なるオブジェクトを参照している場合と、同じオブジェクトを参照している場合があり、それによってeql?メソッドの結果とequal?メソッドの結果が異なってくる。

 equal?メソッド以外は、各クラスで適切な比較ができるように、演算子やメソッドが再定義されていることがあるので、詳細な動作を知るには各クラスのドキュメントを参照しよう(例えばRangeクラスなら==演算子===演算子eql?メソッドのリンク先にドキュメントがある)。

処理対象:==演算子|===演算子|eq?メソッド|equal?メソッド カテゴリ:文法 > 演算子式 > 比較
処理対象:=~演算子 カテゴリ:正規表現
API:eq?メソッド|equal?メソッド|object_id カテゴリ:Objectクラス
API:Fixnumクラス|Floatクラス|Rangeクラス|Regexpクラス|Stringクラス カテゴリ:組み込みライブラリ

※以下では、本稿の前後を合わせて5回分(第15回~第19回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

15. 範囲式や正規表現を使うには? ― 穴埋め問題と株価診断プログラムを作る

範囲式は条件式の中で使うとちょっと面白い動作をする。範囲式を使って簡単な穴埋め問題や株価診断プログラムを作ってみよう。

16. RSSを扱うには? ― 標準rssライブラリ利用して天気予報を表示する

Rubyに標準搭載されているrssライブラリを使って、Webサイトで提供されているRSS/Atomフィードを処理する方法を説明する。例として天気予報情報のRSSフィードを使う。

17. 【現在、表示中】≫ 数値/文字列/配列/範囲式/正規表現の比較を行うには?

Rubyプログラミングでは「等しいかどうか」を調べるための比較はどう行うのか? 比較を行える演算子やメソッドを使って、さまざまな比較を試してみる。

18. 演算子を再定義するには?

Rubyではクラスの二項演算子や単項プラス/マイナス演算子を定義(もしくは再定義)できる。その方法を基礎から説明し、実用的な使い方の例を示す。

19. ファイルから文字列を読み込む(入力する)には?(基本編)

テキストファイルから文字列を読み込むための基礎を解説。ファイル操作をブロックで記述する方法や、ファイルを開く際に「テキスト読み出し専用モード」でアクセスしたり文字コードを指定したりする方法、BOM付きファイルを処理する方法を説明する。

サイトからのお知らせ

Twitterでつぶやこう!