Ruby TIPS

Ruby TIPS

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

2017年4月24日

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

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

 クラスで定義されている演算子の多くは、動作を再定義できる。標準的な機能とは異なる働きをさせたいときや、同じ演算子でも子クラスでは異なる働きをさせたい場合には、演算子を再定義すればよい。今回は簡単な例を使って、演算子を再定義する方法を見ていく。

演算子の再定義

 Rubyでは、文字列はStringクラスのインスタンスである。Stringクラスには+演算子があり、文字列を連結するのに使われる。しかし、普通、単項+演算子や単項-演算子は使えない。また、-演算子も用意されていない。そこで、再定義してもあまり実害のなさそうなStringクラスの単項-演算子と-演算子を再定義することにより、「演算子の再定義」の書き方を見ていこう。

単項演算子の再定義を行う

 演算子を再定義するには、関数の定義と同じような書き方をすればよい。単項演算子を再定義する場合は、defの後にスペースを空けて演算子@と書き、続けて処理の内容を書く。最後に評価された式の値やreturnの後に書かれた式の値が、演算の結果として返される。以下の例は、単項-演算子に「文字列を逆順にしたものを返す」という働きを持たせたものである。

sample001.rb
class String
  def -@            # 単項演算子-の再定義
    self.reverse    # 文字列を逆順にする
  end
end

p -"animation"      # "animation"を逆順にする
リスト1.1 単項-演算子を再定義する

単項演算子の再定義は関数の定義と似ている。最初にdef 演算子@と記述し、以降は一般的な関数の定義と同じように処理を書けばよい。返り値を書けば、それが演算によって返される値となる。

 Stringクラスは組み込みのクラスとして利用できるが、このようにして、既存のクラス名を使ってクラス定義を書けば、クラスに機能を追加できる。ただし、Stringクラスでは、reverseメソッドを使えば逆順にした文字列が得られるので、わざわざ演算子を再定義してその機能を実装する必要はあまりない。ここでは、書き方を知るための例として取り上げた。

 実行例も見てみよう。

ターミナル
$ ruby sample001.rb 
"noitamina"
実行例1.1 再定義された単項-演算子によって文字列を逆順にする

“animation”という文字列の前に単項-演算子が書かれているので、それを逆順にした“noitamina”が返された。

子クラスで演算子を再定義する

 クラスに含まれる演算子を再定義し、機能を変更してしまうと、元の働きを期待して演算子を使った場合に問題が生じることがある。そのような場合はクラスの継承を使って子クラスを作り、子クラスで演算子を再定義するとよい。次に、Stringクラスを継承したMyStringいうクラスを作り、その中で単項-演算子を再定義した例を見てみよう。

sample002.rb
class MyString < String    # Stringクラスを継承したMyStringクラスを作成
  def -@
    self.reverse
  end
end

x = MyString.new("animation")  # MyStringクラスのインスタンスを作成
p -x                           # xを逆順にする
リスト1.2 子クラスで単項-演算子を再定義する

Stringクラスを継承したMyStringクラスを作る。演算子の再定義については全く同じ書き方になっている。MyStringクラスのインスタンスを作成し、文字列を逆順にしてみる。

 結果は実行例1.1と同じなので、省略する。このようにすれば、親クラスの演算子はもともとの働きのままで、再定義した演算子の働きだけが異なる子クラスが作れる。

【コラム】再定義できない演算子の一覧

 以下の表に示した演算子と、+=-=のような自己代入演算子は再定義できない。それらの演算子以外は再定義できる。ただし、Ruby1.8以前では!=演算子と!~演算子は再定義できないことに注意。

演算子 名前・機能
:: クラスやモジュールのネスト
&& 論理積(優先度高)
|| 論理和(優先度高)
?: 条件演算子
.. Rangeオブジェクトの作成(以下)
... Rangeオブジェクトの作成(未満)
= 代入
not 否定(優先度低)
and 論理積(優先度低)
or 論理和(優先度低)
表1.2 再定義できない演算子

再定義できない演算子を優先度の順に並べてある。自己代入演算子とこれらの演算子以外は再定義できる。

二項演算子を再定義する

 a+b+演算子やx-y-演算子のように、左辺と右辺に式が書ける演算子は二項演算子とも呼ばれる。二項演算子を再定義するには、def 演算子(引数)と書き、endまでの間に処理を書く。演算子の左辺に置かれる式はselfに、右辺に置かれる式は()の中の引数に対応する。以下の例は、-演算子に「左辺の文字列から右辺の文字列を取り除く」という働きを持たせたものである。

sample003.rb
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.3 子クラスで-演算子を再定義する

リスト1.2で作成したMyStringクラスの中で-演算子を再定義してみる。MyStringクラスの定義の後半がそのためのコードである。ここでは()の中に書かれているotherという引数が右辺の式に対応する。最後の行では、MyStringクラスのインスタンスxから“ph”を「引く」ことにより、xに含まれる“ph”という文字列を取り除いている。

 Stringクラスのdeleteメソッドは、引数で指定された文字列を削除した文字列を返すメソッドである。この例も、-演算子を再定義してdeleteメソッドと同じ働きを持たせる必要はあまりないが、これで書き方が理解できるだろう。

 実行例は以下の通り。

ターミナル
$ ruby sample003.rb 
"alala"
実行例1.2 再定義された-演算子によって左辺の文字列から右辺の文字列を削除する

変数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.3 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”よりも大きくなければならない。そこで、<=>演算子を再定義して、“.”で区切られた数字の並びを正しく比較できるようにしよう。

 詳しい説明は後回しすることとして、プログラムをざっと眺めておこう。

sample004.rb
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が返される
リスト1.4 <=>演算子を再定義して、章・節・項の番号を比較できるようにする

配列s1と配列s2は、文字列を“.”で区切られた各部分に分け、配列にしたものである。次の配列x1と配列x2は、それぞれ配列s1と配列s2の各要素を整数に変換したものである。最後に、配列(Array)クラスの<=>メソッドを使って各要素を比較する

 では、プログラムの意味や書き方を、図解を交えて見ていこう。

図1.1 再定義した<=>演算子の処理

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]の比較では、最初に102が比較される。この時点で、10の方が大きいことが分かるので、1(左辺が大きい)が返される。

 左辺の要素と右辺の要素が等しい場合は、次の要素を比較する。例えば、[10, 1, 2][10, 1, 1]の比較では、最初の要素は左辺も10、右辺も10と等しいので、次の要素を比較する。次の要素の11も等しいので、さらに次の要素を比較する。最後の要素は左辺の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
実行例1.4 再定義した<=>演算子を使って、章・節・項の番号を比較してみる

これで、章・節・項の番号が正しく比較できるようになった。最初は“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プログラミングでは「等しいかどうか」を調べるための比較はどう行うのか? 比較を行える演算子やメソッドを使って、さまざまな比較を試してみる。

18. 【現在、表示中】≫ 演算子を再定義するには?

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

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

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

20. ファイルから1文字ずつ読み込む(入力する)には?

Rubyでテキストファイルから文字列を読み込むための方法として、ファイルから1文字単位で文字を取得する方法と、ファイル内の全テキスト内容を先頭から1文字ずつループ処理する方法を説明する。

サイトからのお知らせ

Twitterでつぶやこう!