Ruby TIPS
演算子を再定義するには?
Rubyではクラスの二項演算子や単項プラス/マイナス演算子を定義(もしくは再定義)できる。その方法を基礎から説明し、実用的な使い方の例を示す。
クラスで定義されている演算子の多くは、動作を再定義できる。標準的な機能とは異なる働きをさせたいときや、同じ演算子でも子クラスでは異なる働きをさせたい場合には、演算子を再定義すればよい。今回は簡単な例を使って、演算子を再定義する方法を見ていく。
演算子の再定義
Rubyでは、文字列はString
クラスのインスタンスである。String
クラスには+
演算子があり、文字列を連結するのに使われる。しかし、普通、単項+
演算子や単項-
演算子は使えない。また、-
演算子も用意されていない。そこで、再定義してもあまり実害のなさそうなString
クラスの単項-
演算子と-
演算子を再定義することにより、「演算子の再定義」の書き方を見ていこう。
単項演算子の再定義を行う
演算子を再定義するには、関数の定義と同じような書き方をすればよい。単項演算子を再定義する場合は、def
の後にスペースを空けて演算子@
と書き、続けて処理の内容を書く。最後に評価された式の値やreturn
の後に書かれた式の値が、演算の結果として返される。以下の例は、単項-
演算子に「文字列を逆順にしたものを返す」という働きを持たせたものである。
class String
def -@ # 単項演算子-の再定義
self.reverse # 文字列を逆順にする
end
end
p -"animation" # "animation"を逆順にする
|
単項演算子の再定義は関数の定義と似ている。最初にdef 演算子@
と記述し、以降は一般的な関数の定義と同じように処理を書けばよい。返り値を書けば、それが演算によって返される値となる。
String
クラスは組み込みのクラスとして利用できるが、このようにして、既存のクラス名を使ってクラス定義を書けば、クラスに機能を追加できる。ただし、String
クラスでは、reverse
メソッドを使えば逆順にした文字列が得られるので、わざわざ演算子を再定義してその機能を実装する必要はあまりない。ここでは、書き方を知るための例として取り上げた。
実行例も見てみよう。
$ ruby sample001.rb
"noitamina"
|
“animation”という文字列の前に単項-
演算子が書かれているので、それを逆順にした“noitamina”が返された。
子クラスで演算子を再定義する
クラスに含まれる演算子を再定義し、機能を変更してしまうと、元の働きを期待して演算子を使った場合に問題が生じることがある。そのような場合はクラスの継承を使って子クラスを作り、子クラスで演算子を再定義するとよい。次に、String
クラスを継承したMyString
いうクラスを作り、その中で単項-
演算子を再定義した例を見てみよう。
class MyString < String # Stringクラスを継承したMyStringクラスを作成
def -@
self.reverse
end
end
x = MyString.new("animation") # MyStringクラスのインスタンスを作成
p -x # xを逆順にする
|
String
クラスを継承したMyString
クラスを作る。演算子の再定義については全く同じ書き方になっている。MyString
クラスのインスタンスを作成し、文字列を逆順にしてみる。
結果は実行例1.1と同じなので、省略する。このようにすれば、親クラスの演算子はもともとの働きのままで、再定義した演算子の働きだけが異なる子クラスが作れる。
【コラム】再定義できない演算子の一覧
以下の表に示した演算子と、+=
や-=
のような自己代入演算子は再定義できない。それらの演算子以外は再定義できる。ただし、Ruby1.8以前では!=
演算子と!~
演算子は再定義できないことに注意。
演算子 | 名前・機能 |
---|---|
:: | クラスやモジュールのネスト |
&& | 論理積(優先度高) |
|| | 論理和(優先度高) |
?: | 条件演算子 |
.. | Rangeオブジェクトの作成(以下) |
... | Rangeオブジェクトの作成(未満) |
= | 代入 |
not | 否定(優先度低) |
and | 論理積(優先度低) |
or | 論理和(優先度低) |
再定義できない演算子を優先度の順に並べてある。自己代入演算子とこれらの演算子以外は再定義できる。
二項演算子を再定義する
a+b
の+
演算子やx-y
の-
演算子のように、左辺と右辺に式が書ける演算子は二項演算子とも呼ばれる。二項演算子を再定義するには、def 演算子(引数)
と書き、end
までの間に処理を書く。演算子の左辺に置かれる式はself
に、右辺に置かれる式は()
の中の引数に対応する。以下の例は、-
演算子に「左辺の文字列から右辺の文字列を取り除く」という働きを持たせたものである。
class MyString < String
def -@
self.reverse
end
def -(other) # -演算子の再定義
self.delete(other) # otherで指定された文字列を削除する
end
end
x = MyString.new("alphalpha")
p x - "ph" # xから“ph”を削除する
|
リスト1.2で作成したMyString
クラスの中で-
演算子を再定義してみる。MyString
クラスの定義の後半がそのためのコードである。ここでは()
の中に書かれているother
という引数が右辺の式に対応する。最後の行では、MyString
クラスのインスタンスx
から“ph”を「引く」ことにより、x
に含まれる“ph”という文字列を取り除いている。
String
クラスのdelete
メソッドは、引数で指定された文字列を削除した文字列を返すメソッドである。この例も、-
演算子を再定義してdelete
メソッドと同じ働きを持たせる必要はあまりないが、これで書き方が理解できるだろう。
実行例は以下の通り。
$ ruby sample003.rb
"alala"
|
変数x
は“alphalpha”という文字列を参照する。この文字列から“ph”を削除すると、“alala”となる。
以上、演算子を再定義する方法を簡単な例で見てきたが、書き方が分かったところで、少し実用的な例も見ておこう。
章・節・項の番号を比較する
さまざまなクラスで定義されている<=>
演算子は大小比較に使われる便利な演算子である。形が空飛ぶ円盤に似ているので、UFO演算子や宇宙船演算子などと呼ばれることもある。この演算子を使うと、左辺が右辺より小さいときには-1、等しいときには0、左辺が右辺より大きいときには1が返される。まずは、この演算子の働きを知るために、irb(interactive ruby)で動作を確認してみよう。
$ irb # irbを起動する
> 1 <=> 2 # 1は2より小さい
=> -1
> 1 <=> 1 # 1と1は等しい
=> 0
> 2 <=> 1 # 1は2より大きい
=> 1
> "10" <=> "2" # "10"は"2"より小さい(文字列の比較)
=> -1
> # [Ctrl+]+Dキーを押してirbを終了する
|
<=>
演算子の動作を確認する<=>
演算子は、左辺が小さければ-1を、等しければ0を、左辺が大きければ1を返す。数字を表す文字列を比較するときには、数値の大小ではなく文字列の大小で比較されるので、“10”は“2”よりも小さくなることに注意。この場合、先頭の文字から順に比較するので、まず“10”の最初の文字の“1”と“2”が比較され、“10”の方が小さいという結果になる。
例えば、10章1節2項を“10.1.2”と表記することがある。この値は数値として表現できないので、文字列として取り扱う。しかし、文字列として取り扱うと、“10.1.2”は2章1節を表す“2.1”よりも小さくなってしまう。本来の目的から考えると、“10.1.2”は“2.1”よりも大きくなければならない。そこで、<=>
演算子を再定義して、“.”で区切られた数字の並びを正しく比較できるようにしよう。
詳しい説明は後回しすることとして、プログラムをざっと眺めておこう。
class ChapterString < String
def <=>(other)
s1 = self.split(".") # “.”を区切りとして文字列の各部分を配列にする
s2 = other.split(".") # “.”を区切りとして文字列の各部分を配列にする
x1 = []; x2 = [] # 空の配列を作る
s1.each{|v| x1 << v.to_i} # 各要素を整数化して配列x1に追加していく
s2.each{|v| x2 << v.to_i} # 各要素を整数化して配列x2に追加していく
x1 <=> x2 # x1とx2を比較した結果を返す
end
end
x = ChapterString.new("10.1.2")
p x <=> "2.1" # 1が返される
p x <=> "10.1" # これも1が返される
p x <=> "10.1.2" # 等しいので0が返される
p x <=> "11.3.1" # -1が返される
|
<=>
演算子を再定義して、章・節・項の番号を比較できるようにする配列s1
と配列s2
は、文字列を“.”で区切られた各部分に分け、配列にしたものである。次の配列x1
と配列x2
は、それぞれ配列s1
と配列s2
の各要素を整数に変換したものである。最後に、配列(Array
)クラスの<=>
メソッドを使って各要素を比較する
では、プログラムの意味や書き方を、図解を交えて見ていこう。
split
メソッドを使って、“.”で区切られた文字列を部分文字列の配列に変換する。次に、each
メソッドにブロックを渡し、全ての要素をto_i
メソッドで整数化し、配列x1
や配列x2
に追加する。配列(Array)クラスの<=>
メソッドは、各要素を先頭から順に比較し、大小の判定をした結果を返す。
再定義された<=>
演算子を使って、結局のところ何がやりたいかというと、“10.1.2”や“10.1”のような文字列に含まれる数字を先頭から順に取り出して、数値として比較していきたい、ということである。
そこで、まず、章・節・項に当たる数字を取り出すために、split
メソッドを使って文字列を分割する。split
メソッドは、引数として指定した区切り文字で文字列を区切り、それぞれの部分文字列を配列として返してくれる。例えば、"10.1.2".split(".")
は["10", "1", "2"]
という配列を返す。
次に、配列の各要素を順に数値として比較したいので、to_i
メソッドを使って整数化しておく必要がある。繰り返し処理を使ってもいいが、each
メソッドにブロックを渡せば処理が簡単に記述できる。プログラム中の<<
演算子は、配列に要素を追加するための演算子である。例えば、["10", "1", "2"]
は[10, 1, 2]
に変換される。なお、ここでは、プログラムを単純にするため、文字列が数値として解釈できない場合に関しては特に対処をしていない(その場合は0
に変換される)。
数値の配列が作成できれば、後は先頭から順に比較していけばいい。このとき、繰り返し処理を使って各要素を比較しなくても、配列(Array
クラス)の<=>
演算子を利用すれば、そのまま結果が返せる。つまり、左辺が小さければ-1が、等しければ0が、左辺が大きければ1が返される。
例えば、[10, 1, 2]
と[2, 1]
の比較では、最初に10
と2
が比較される。この時点で、10
の方が大きいことが分かるので、1(左辺が大きい)が返される。
左辺の要素と右辺の要素が等しい場合は、次の要素を比較する。例えば、[10, 1, 2]
と[10, 1, 1]
の比較では、最初の要素は左辺も10
、右辺も10
と等しいので、次の要素を比較する。次の要素の1
と1
も等しいので、さらに次の要素を比較する。最後の要素は左辺の2
と、右辺の1
である。ここで左辺が大きいことが分かったので、1が返される。
また、要素の数が異なり、共通する部分が等しい場合には、要素の数が多い方が大きいと見なされる。例えば、[10, 1, 2]
と[10, 1]
の比較では、[10, 1]
までは同じで、右辺の要素が少ない。この場合は、左辺が大きいとみなされ、1が返される。なお、[10, 1, -1]
と[10, 1]
のように、余計な要素が負の値であっても、要素の多い方が大きいものと見なされることに注意が必要である。
実行例も示しておこう。リスト1.4の中のコメントと同じ結果になる。
$ ruby sample004.rb
1
1
0
-1
|
<=>
演算子を使って、章・節・項の番号を比較してみるこれで、章・節・項の番号が正しく比較できるようになった。最初は“10.1.2”の方が“2.1”より大きいので、1が返される。次も、“10.1.2”の方が“10.1”より大きいので、1が返される。等しい場合は当然のことながら0が返される。最後は“10.1.2”の方が“11.3.1”よりも小さいので-1が返される。
このような例は、バージョン番号の比較にもそのまま使える。また、連番の付いたファイル名の大小比較にも応用できる。例えば、“file10”と“file2”を文字列として比較すると、“file2”の方が大きくなってしまうが、“file10”の方を大きいと見なすような演算子を持つクラスが作成できるだろう。
まとめ
単項演算子を再定義するには、まずdef 演算子@
と書く。一方、二項演算子を再定義する場合はdef 演算子(引数)
と書く。後はend
までの間にメソッドの定義と同じ書き方で処理を記述すればよい。演算子を再定義すれば、そのクラスに最もふさわしい働きをさせることができるようになる。
API:Arrayクラス|Stringクラス カテゴリ:組み込みライブラリ
API:to_iメソッド カテゴリ:Stringクラス
※以下では、本稿の前後を合わせて5回分(第16回~第20回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
16. RSSを扱うには? ― 標準rssライブラリ利用して天気予報を表示する
Rubyに標準搭載されているrssライブラリを使って、Webサイトで提供されているRSS/Atomフィードを処理する方法を説明する。例として天気予報情報のRSSフィードを使う。
17. 数値/文字列/配列/範囲式/正規表現の比較を行うには?
Rubyプログラミングでは「等しいかどうか」を調べるための比較はどう行うのか? 比較を行える演算子やメソッドを使って、さまざまな比較を試してみる。
19. ファイルから文字列を読み込む(入力する)には?(基本編)
テキストファイルから文字列を読み込むための基礎を解説。ファイル操作をブロックで記述する方法や、ファイルを開く際に「テキスト読み出し専用モード」でアクセスしたり文字コードを指定したりする方法、BOM付きファイルを処理する方法を説明する。
20. ファイルから1文字ずつ読み込む(入力する)には?
Rubyでテキストファイルから文字列を読み込むための方法として、ファイルから1文字単位で文字を取得する方法と、ファイル内の全テキスト内容を先頭から1文字ずつループ処理する方法を説明する。