Ruby TIPS
数値/文字列/配列/範囲式/正規表現の比較を行うには?
Rubyプログラミングでは「等しいかどうか」を調べるための比較はどう行うのか? 比較を行える演算子やメソッドを使って、さまざまな比較を試してみる。
プログラムの中で「等しいかどうか」を調べることは多い。ただし、日常の感覚とは違って、さまざまな種類の「等しい」があることに注意が必要だ。今回は、==
演算子や===
演算子、eql?
メソッド、equal?
メソッドを取り上げ、それらの働きを確認していく。
比較のための演算子やメソッド
Rubyでは、数値や文字列の大小を比較するために、さまざまな演算子やメソッドが利用できる。今回は、等しいかどうかの比較に絞って、簡単な実験を行ってみたい。値が等しいのか、同一のオブジェクトであるかなどによって結果が異なってくるので、それぞれの機能をきちんと確認しておこう。日常的な「等しい」という漠然とした感覚で捉えていると、思わぬ落とし穴に陥ることになりかねない。
等しいかどうかを比較するための演算子やメソッドには、以下のようなものがある
==
: 値が等しい===
:case
式の中で使われたり、範囲に含まれるかどうかを調べたりするのに使うeql?
: 同一のクラスであり、==
で比較した結果が等しい(オブジェクトのハッシュ値が等しい)equal?
: 同一のオブジェクトである(object_id
が等しい)
これらの動作を1つ1つ確かめてみようというわけである。なお、equal?
以外は、最適な比較ができるように各クラスで再定義されているのが普通である。従って、「等しいかどうか評価する」という機能は同じだが、クラスによって動作が異なっている。===
の働きは==
と似ているが、範囲や正規表現などを利用するときに注意すべきポイントがあるので、後半で触れることとする。では、数値の比較から見ていこう
数値の比較を行う
整数はFixnum
クラスのインスタンスであり、浮動小数点数はFloat
クラスのインスタンスである。数値を==
演算子で比較するときには型変換が行われるので、値が等しければtrueが返される。ただし、浮動小数点数では誤差が発生することがあるので、値が等しいと思っていてもそうならないことがある。当然のことながら、Fixnum
クラスとFloat
クラスはクラスが異なるので、eql?
メソッドやequal?
メソッドで比較するとfalseが返される。
今回は簡単な実験を数多く行うので、Rubyの対話型環境であるirb(Interactive 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
|
前半部分のいくつかの比較は、常識的な感覚で理解できる結果だろう。後半部分では誤差が発生する場合とそうでない場合があるので、注意が必要だ。0.1*10
は1.0となるが、0.1*3
は0.30000000000000004になる。
なお、多重代入を使って、複数の変数に値を代入するときには、x,y=1,1
のように変数をカンマで区切って左辺に書く。x=1, y=1
と書きたくなるかもしれないが、それでは期待した通りにはならない。変数y
に1が代入された後、変数x
に1,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
|
ハッシュ値とは、オブジェクトの値を格納したり検索したりするのに使われる値である。
object_id
は、オブジェクトを一意に識別するための値である。
数値(=Fixnum
クラスやFloat
クラスのインスタンス)の場合、値が同じであればハッシュ値も同じであり(このコードでは1683040744889400978)、object_id
も同じである(このコードでは3)。object_id
は毎回異なるので、このコード例と同じ値にはならない(特にハッシュ値は異なる結果になる)ことに注意してほしい。
irbでは↑キーを押せば、以前実行した式をさかのぼって表示できるので、似たような式を入力するときに便利である。irbを終了させるには、プロンプトの直後でCtrl+Dキーを押すか、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
|
同一のオブジェクトであるかどうかということは、object_id
が等しいかどうかということである。後半部分で変数aと
変数b
のobject_id
が70240432335320と70240432335300で異なっていることを確認しておこう。また、eql?
メソッドの働きを確認するために、ハッシュ値も表示してみた。
実行例1.3を見ると、変数a
と変数b
はobject_id
が異なるので、異なるオブジェクトである(厳密には、異なるオブジェクトを参照している)が、ハッシュ値が同じなので、参照先の文字列は同じ値であるということが分かる。なお、object_id
もハッシュ値も、必ずしも決まった値が与えられるわけではなく、毎回異なる値が与えられる。
続いて、変数a
と変数b
が同じオブジェクトを参照するようにして、比較を行ってみる。
> a=b # 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
|
今度は、equal?
メソッドの結果がtrueになった。後半部分で変数a
と変数b
のoject_id
が70240432335300と70240432335300で同じになっていることを確認しておこう。
【コラム】文字列のエンコーディングと比較
文字列の値が等しいかどうかは、バイト列が等しいかどうかで調べられるので、エンコーディングによって結果が異なることがある。例えば、半角英数字は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==d # cとdは値が等しいか
=> false
> c.encoding # cのエンコーディングを表示してみる
=> #<Encoding:Shift_JIS>
> d.encoding # dのエンコーディングを表示してみる
=> #<Encoding:UTF-8>
|
※日本語入力で文字化けする場合は、最初の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]
|
配列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]
|
変数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
|
===
演算子は左辺をレシーバーとして評価が行われる===
演算子の左辺と右辺を入れ替えると、オブジェクトによっては結果が異なることに注意。
===
演算子は各クラスで再定義されているので、そのクラスの働きに合った機能を持つ。===
演算子では左辺がレシーバーとなるので、左辺に範囲式を書けば、Range
クラスの===
演算子が呼び出される。従って範囲内にあるかどうかが正しく判定される。一方、左辺に整数を書けばFixnum
クラスの===
演算子が呼び出される。この場合は、整数と等しいかどうかを調べるので、右辺に範囲式を書くと正しく判定されない。
===
演算子の機能はcase
式のwhen
節に書かれた式を評価するときにも使われる。when
節に書かれた式がレシーバーとなるので、以下の2つの式は同じ動作となる。
$ irb
> case 5
> when 0..10 # 0..10がレシーバーになる
> p "範囲内"
> else
* p "範囲外"
> end
"範囲内"
|
===
演算子の評価が行われる※日本語入力で文字化けする場合は、最初の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
|
===
演算子は正規表現に一致するかどうかが評価される===
演算子は左辺をレシーバーとするので、左辺に正規表現を書いた場合はRegexp
クラスの===
演算子が呼び出される。従って、正規表現に右辺の文字列が一致しているかどうかが正しく判定される。一方、左辺に文字列を書くと、String
クラスの===
演算子が呼び出される。この場合、文字列と正規表現が等しいかどうかを調べるので、正しく判定されない。
case
式の中でwhen
節の後に正規表現を書いた場合も実行例1.4で見たのと同様、when
節の後に書かれた正規表現をレシーバーとして===
演算子の評価が行われる。念のため、このコードも見ておこう。
$ irb
> case "abcde"
> when /c.e/ # /c.e/がレシーバーになる
> p "一致"
> else
* p "不一致"
> end
"一致"
|
この例では、when
の後に書かれた/c.e/がレシーバーとして扱われるので、実行例1.8で===
演算子を使った場合と同様の演算が行われる。
まとめ
==
演算子は値が等しいかどうかを調べるのに使う。===
演算子は左辺をレシーバーとして等しいかどうかを調べるのに使う。
また、case
式を使った条件分岐では、===
演算子を使った比較が行われる。その場合、when
節の後に書かれた式がレシーバーとなる。
eql?
メソッドは同じクラスであり、同じ値であるか(ハッシュ値が等しいかどうか)という評価を行う。
equal?
メソッドは同じオブジェクトであるかどうか(object_id
が等しいかどうか)という評価を行う。同じ値を持つ場合でも、異なるオブジェクトを参照している場合と、同じオブジェクトを参照している場合があり、それによってeql?
メソッドの結果とequal?
メソッドの結果が異なってくる。
equal?
メソッド以外は、各クラスで適切な比較ができるように、演算子やメソッドが再定義されていることがあるので、詳細な動作を知るには各クラスのドキュメントを参照しよう(例えばRange
クラスなら==
演算子/===
演算子/eql?
メソッドのリンク先にドキュメントがある)。
処理対象:=~演算子 カテゴリ:正規表現
API:eq?メソッド|equal?メソッド|object_id カテゴリ:Objectクラス
API:Fixnumクラス|Floatクラス|Rangeクラス|Regexpクラス|Stringクラス カテゴリ:組み込みライブラリ
※以下では、本稿の前後を合わせて5回分(第15回~第19回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
16. RSSを扱うには? ― 標準rssライブラリ利用して天気予報を表示する
Rubyに標準搭載されているrssライブラリを使って、Webサイトで提供されているRSS/Atomフィードを処理する方法を説明する。例として天気予報情報のRSSフィードを使う。
17. 【現在、表示中】≫ 数値/文字列/配列/範囲式/正規表現の比較を行うには?
Rubyプログラミングでは「等しいかどうか」を調べるための比較はどう行うのか? 比較を行える演算子やメソッドを使って、さまざまな比較を試してみる。
19. ファイルから文字列を読み込む(入力する)には?(基本編)
テキストファイルから文字列を読み込むための基礎を解説。ファイル操作をブロックで記述する方法や、ファイルを開く際に「テキスト読み出し専用モード」でアクセスしたり文字コードを指定したりする方法、BOM付きファイルを処理する方法を説明する。