Ruby TIPS

Ruby TIPS

クラスを継承するには? メソッドの呼び出しをprivate/protectedで制限するには?

2016年11月4日

オブジェクト指向言語の特長である「クラスの継承」をRubyで実現する方法を解説。スーパークラスのメソッドの呼び出し制限で、Ruby言語特有の内容についても紹介する。

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

 クラスの継承を利用すれば、プログラムの機能を自然な形で分割してクラス群を構成したり、互換性を損なわずに機能をスムーズに追加・変更したりできるようになる。今回は、Rubyにおけるクラスの継承と、メソッドの呼び出し制限について見ていく。特に、メソッドの呼び出し制限については、他のプログラミング言語と異なるところがあるので、注意が必要となる。

クラスの継承

 「TIPS: クラスとそのコンストラクター/アクセサー/メソッドを定義し利用するには?」では、簡単な例として平面上の点を表すPointクラスを作成した。今回は、Pointクラスに機能を追加したVectorクラスを作成してみよう。

 Vectorクラスは原点(0,0)から点(x,y)に向けて引いた矢印のようなものだと思えばいい。Vectorクラスは標準添付のライブラリにもあり、かなり高度な機能を持つが、ここでは、それを極めて単純化したものを学習のために作ってみようというわけだ。

 まず、Pointクラスの定義を見てみよう。ここでは、できるだけ簡単な定義にするために、アクセサーとコンストラクターだけを書いてある。ただし、原点から点(x,y)までの距離を求めるlengthメソッドも含めてある。

sample001.rb
include(Math)           # sqrtメソッドを使うのに必要
class Point
  attr_accessor :x, :y  # アクセサーの定義

  def initialize(x, y)  # コンストラクター
    @x, @y = x, y
  end

  def length         # lengthメソッド
    return Math.sqrt(@x**2+@y**2)
  end
end
リスト1.1 Pointクラスの定義

アクセサーを定義して、xプロパティとyプロパティで座標を指定できるようにしてある。lengthメソッドではピタゴラスの定理によって原点と(x,y)の距離を計算する。

 ここで、単に平面上の点と原点からの距離だけでなく、角度を求めたり、伸縮・回転させたりする必要が生じたとしよう。Pointクラスの定義にそういった機能を追加することもできるが、すでにPointクラスを他のプログラムから利用しているのであれば、定義の変更によってトラブルが起こる危険もある。そこで、Pointクラスはできるだけ書き換えずに、新たな機能を追加したVectorクラスを作成すれば、変更にも対応しやすい(もちろん、そういった無計画な増築にも対応できるようにというのが先に立つ理由ではなく、そもそもの設計上の要請から機能を分割するのが本来の姿なのだが)。

 ともあれ、このように、あるクラスを基に新たなクラスを作成することを継承と呼ぶ。また、作成元となるクラスをスーパークラスと呼び、新たに作成されたクラスをサブクラスと呼ぶ。ここでは、Pointクラスがスーパークラス、Vectorクラスがサブクラスとなる。

 では、Vectorクラスの定義を見てみよう。

sample001.rb
class Vector < Point               # Pointクラスを継承
  def angle
    return acos(@x / length)  # 角度を返す
  end
end
リスト1.2 Pointクラスを継承したVectorクラスを定義

クラス名の後に<を書き、スーパークラスの名前を書けばサブクラスにできる。ここでは、angleメソッドを定義して、角度を求められるようにしてある。

 「クラス名 < スーパークラス名」の形式でクラス定義を書くだけなので、書き方にはそれほど難しい点はない。このようにしてサブクラスを定義すると、特に何も書かなくてもスーパークラスの機能を利用できる。例えば、Vectorクラスには座標を設定するための記述はないが、Pointクラスの機能を受け継いでいるので、xプロパティやyプロパティが使える。もちろん、コンストラクターも受け継いでいるし、lengthメソッドも使える。さらに、Vectorクラスで新しく定義したangleメソッドも使えるようになった。angleメソッドの中で、Pointクラスのlengthメソッドを呼び出していることからもスーパークラスの機能を継承していることが分かるだろう。

 では、Vectorクラスを利用して、ベクトルの長さや角度を出力するコードを書いてみよう。

sample001.rb
v = Vector.new(3, 4)  # Vectorクラスのインスタンスを作成
puts v.length    # スーパークラスのlengthメソッドを呼び出す
puts v.angle     # このクラスのangleメソッドを呼び出す
リスト1.3 Vectorクラスの利用

newメソッドを使ってVectorクラスのインスタンスを作成する。Vectorクラスの定義にはコンストラクターは書かれていないが、Pointクラスから継承されたコンストラクターが呼び出され、座標が設定される。スーパークラスのlengthメソッドを呼び出して原点との距離を求めることもできるし、Vectorクラスのangleメソッドを呼び出して角度を求めることもできる。

 上記リスト1.1~1.3の全コードが1つのファイルに入力されていることに注意。実行すると以下のようになる。

コンソール
$ ruby sample001.rb 
5.0
0.9272952180016123
$ 
実行例1.1 クラスの継承を利用したプログラムを実行する

底辺が3、高さが4の直角三角形の斜辺の長さは5である。角度はラジアン単位であることに注意。0.927ラジアン53.1°となる。

【コラム】角度の求め方

 リスト1.2やリスト1.3のangleメソッドでは、以下の図のような三角形のθの値を求める。a/cの値はcosθの値と等しい。つまりcosθの値は分かっている。そこで、cos関数の逆関数であるacos関数を使って、cosθの値(=a/c)を基に角度を求める。

図1.1 ベクトルの角度を求める

cos関数は角度からコサインの値を求める関数、acos関数はコサインの値から角度を求める関数である。

※2016/11/04更新(このコラム内の以下を追記して内容を訂正しました。訂正内容については本稿末尾の【更新履歴(お詫びと訂正)】を参照してください): ただし、このacos関数では正しく角度が求められない場合がある。というのも、yの値が負の場合にも正の場合と同じ角度が返されてしまうからだ(例えば、v = Vector.new(3, 4)でベクトルを作成した場合も、v = Vector.new(3, -4)でベクトルを作成した場合も、同じ値が返される)。これは、角度の計算にyの符号が反映されないためである。

 そこで、反時計回りで角度を求めるには、リスト1.2を以下のようなコードに書き換えるといい。

Ruby
class Vector < Point  # Pointクラスを継承
  def angle
    t = acos(@x / length)
    return @y >= 0 ? t : PI * 2 - t
  end
end
反時計回りで角度を求める関数

 -π<θ<=πで値を返すなら、return文を以下のように書き換えるといい。

Ruby
return y >= 0 ? t : -t
「-π<θ<=π」で値を返す場合のreturn文

 この場合、atan2関数を使うこともできる(ただし、lengthメソッドが使われないので、今回の説明からは外れるが)。

Ruby
class Vector < Point  # Pointクラスを継承
  def angle
    return atan2(@y, @x)
  end
end
atan2関数を使った場合の関数

 なお、複素数を使っても同様の結果が得られる。その場合は、以下のようになる。

Ruby
class Vector < Point  # Pointクラスを継承
  def angle
    return Complex(@x, @y).angle
  end
end
複素数を使った場合の関数

 この場合の返り値は、後者の範囲(-π<θ<=π)の値となる。

メソッドの呼び出し制限(privateなメソッド)

 クラス内のメソッドは、特に何も指定されていなければ、publicなメソッドと見なされる(ただしコンストラクターはprivateなメソッドとなる)。publicなメソッドは、クラス内からでも、他のクラスからでも利用できる。一方、privateなメソッドはクラス内でのみ利用できるメソッドである。だが、実はprivateなメソッドはサブクラスからでも利用できる。他のプログラミング言語の経験がある人には意外かもしれないが、実際にそうなっているので、試してみるといい。

 publicprivateといったキーワードを書くと、それ以降に定義されたメソッドの呼び出し制限が指定できる。以下のように、Pointクラスのlengthメソッドをprivateにしてみよう。

sample002.rb
include(Math)           # sqrtメソッドを使うのに必要
class Point
  attr_accessor :x, :y  # アクセサーの定義

  def initialize(x, y)  # コンストラクター
    @x, @y = x, y
  end

  private               # これ以降、privateになる
  def length         # lengthメソッド
    return Math.sqrt(@x**2+@y**2)
  end
end

class Vector < Point    # Pointクラスを継承
  def angle
    return acos(@x / length)   # 角度を返す
  end
end

v = Vector.new(3, 4)  # Vectorクラスのインスタンスを作成
#puts v.length   # スーパークラスのlengthメソッドを呼び出す
puts v.angle     # このクラスのangleメソッドを呼び出す
リスト1.4 privateなメソッドを定義する

Pointクラスの中で、lengthメソッドの定義の前にprivateを追加した。なお、下から2行目のputs v.lengthはエラーになるので、コメントアウトしてある。

 実行例は以下の通り。angleメソッドは正しく結果を表示する。Vectorクラスのangleメソッドの中で、スーパークラスのlengthメソッドを利用していることに注目してほしい。Pointクラスでprivateに指定したlengthメソッドがサブクラスであるVectorクラスから呼び出せることが分かる。実行例は以下の通り。

コンソール
$ ruby sample002.rb 
0.9272952180016123
$ 
実行例1.2 privateなメソッドはサブクラスからも呼び出せる

angleメソッドでは、スーパークラスのlengthメソッドが呼び出されている。lengthメソッドはprivateなメソッドだが、問題なく呼び出せた。

 なお、private :lengthのように、メソッドのシンボルを指定して、特定のメソッドにだけ呼び出し制限を指定することもできる。その場合は、メソッドの定義よりも後ろに記述する必要がある。

 ところで、リスト1.4でコメントアウトした行を実行するとどうなるのだろうか。行頭の#を削除して実行してみると以下のようになる。

コンソール
$ ruby sample002.rb 
sample002.rb:20:in `<main>': private method `length' called for #<Vector:0x007f991019bb20 @x=3, @y=4> (NoMethodError)
$ 
実行例1.3 privateなメソッドをサブクラスのインスタンスから呼び出した

サブクラスのインスタンスからはスーパークラスのprivateなメソッドは呼び出せない。

 他のクラスや他のクラスのインスタンスからメソッドを呼び出すにはpublicである必要がある。サブクラスの定義の中ではスーパークラスのprivateなメソッドは呼び出せるが、サブクラスのインスタンスからはスーパークラスのprivateなメソッドを直接呼び出すことはできない。

メソッドの呼び出し制限(protectedなメソッド)

 メソッドの呼び出し制限にはpublicprivateの他に、protectedも指定できる。これも他のプログラミング言語を知っている人には理解しづらい動作なので、丁寧に確認しておこう。

 protectedなメソッドは、レシーバーをサブクラスの定義内で指定した場合にも利用できる。レシーバーとは、メソッドが呼び出されるオブジェクトのことである。リスト1.4を以下のように書き換えてみよう。

sample003.rb
include(Math)           # sqrtメソッドを使うのに必要
class Point
  attr_accessor :x, :y  # アクセサーの定義

  def initialize(x, y)  # コンストラクター
    @x, @y = x, y
  end

  protected             # これ以降、protectedになる
  def length         # lengthメソッド
    return Math.sqrt(@x**2+@y**2)
  end
end

class Vector < Point    # Pointクラスを継承
  def angle
    return acos(@x / self.length)   # レシーバーを指定。角度を返す
  end
end

v = Vector.new(3, 4)  # Vectorクラスのインスタンスを作成
# puts v.length  # スーパークラスのlengthメソッドを呼び出す
puts v.angle     # このクラスのangleメソッドを呼び出す
リスト1.5 protectedなメソッドを定義する

Pointクラスの中で、lengthメソッドの定義の前にprotectedを追加した。Vectorクラスのangleメソッドでは、lengthメソッドにレシーバー(self)を指定している。なお、下から2行目のputs v.lengthはエラーになるので、コメントアウトしてある。

 リスト1.4のprivateprotectedに書き換えただけでは同じ動作にしかならないので違いは分からない。しかし、サブクラスのメソッド定義の中でレシーバーを指定した場合に違いが出る。ここでは自分自身を表すselfを指定してある。リスト1.5を実行すると、スーパークラスのlengthメソッドが正しく呼び出される。実行結果は、実行例1.2と同じだが一応掲載しておく。

コンソール
$ ruby sample003.rb 
0.9272952180016123
$ 
実行例1.4 protectedなメソッドはレシーバーを指定した場合にも呼び出せる

angleメソッドでは、スーパークラスのlengthメソッドが呼び出されている。lengthメソッドがprivateなメソッドだとレシーバーは指定できないが、protectedなメソッドなら問題なく呼び出せる。

 試しに、protectedprivateに書き換えて実行してみると、以下のようなエラーが表示される。

コンソール
$ ruby sample003.rb 
sample003.rb:20:in `<main>': private method `length' called for #<Vector:0x007ffbd00260b0 @x=3, @y=4> (NoMethodError)
$ 
実行例1.5 privateなメソッドにはレシーバーを指定できない

privateなメソッドはサブクラスからでも使えるはずなのにエラーになった。一体なぜなのか……。

 privateなメソッドの呼び出しにはレシーバーが指定できない。一方、protectedなメソッドであればレシーバーを書いていても書いていなくても正しく呼び出せる。以下のように、サブクラスの中でインスタンスを作成して利用するような場合も同様である。

Ruby
class Vector < Point            # Pointクラスを継承
  def angle
    temp =  Vector.new(@x, @y)  # インスタンスを作成
    return acos(temp.x / temp.length)  # 角度を返す
  end
end
リスト1.6 インスタンスを作成し、レシーバーに指定する

この例では、作成したインスタンスを参照する変数tempがレシーバーとして指定されている。lengthメソッドがprotectedであれば問題なく動くが、privateであればエラーとなる。

 念のため、リスト1.5でコメントアウトした行を実行しようとした場合もエラーになることにも注意しよう。protectedなメソッドは、サブクラスの定義内からは呼び出せるが、サブクラスのインスタンスからはやはり呼び出せない。

 今回はクラスの継承とメソッドの呼び出し制限について見た。演算子やメソッドのオーバーライドなど、触れられなかった機能もまだまだあるが、また後日公開予定のTIPSであらためて紹介することとしたい。

まとめ

 クラスの継承を利用すれば、スーパークラスの機能を受け継いだサブクラスが作れる。サブクラスでは基本的にスーパークラスの機能をそのまま利用できるだけでなく、新たな機能も追加できる。クラスのメソッドは、他のクラスから利用できないようにした方がいい場合もあるので、呼び出し制限が設定できようになっている。ただし、Rubyでは、privateなメソッドであってもサブクラスからであれば利用できる。また、protectedもサブクラスから利用できる。ただし、privateなメソッドではレシーバーが指定できず、protectedなメソッドではレシーバーが指定できるという点が異なる。

処理対象:継承 カテゴリ:文法 > クラス
処理対象:self カテゴリ:文法 > 変数と定数 > 疑似変数
API:Mathモジュール カテゴリ:組み込みライブラリ

【更新履歴(お詫びと訂正)】

2016/11/04

 記事公開後、「acos関数では正しく角度が求められない」というご指摘をいただきました。yの値が負の場合にも正の場合と同じ角度が返されるということです(例えば、v = Vector.new(3, 4)でベクトルを作成した場合も、v = Vector.new(3, -4)でベクトルを作成した場合も、同じ値が返されます)。これは、角度の計算にyの符号が反映されないためです。お詫びして、訂正させていただきます。具体的には「【コラム】角度の求め方」の「図1.1 ベクトルの角度を求める」より下を新たに追記しています。

2016/11/04

 メソッド名についても、getLengthのような書き方ではなく、lengthといった書き方にする(メソッド名には小文字と_を使う。アクセサーにはgetsetを付けない)べきであるというご指摘もありました。それに関する修正を全体に施しました。これは著者の不勉強にほかなりません。ご指摘くださった皆さまありがとうございました。

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

11. ブロック ― ちょっと便利な繰り返し処理の構文とは?(3)

Rubyに用意されている繰り返し処理の構文のうち「ブロック」を使えば、繰り返し処理をより簡潔に書ける。その基本的な使い方と、自作メソッドでの利用例を解説する。

12. クラスとそのコンストラクター/アクセサー/メソッドを定義し利用するには?

Rubyプログラミングの基本中の基本として、クラスの定義から、そのインスタンスの作成・利用、インスタンスメソッドの定義、変数へのアクセスまでを説明する。

13. 【現在、表示中】≫ クラスを継承するには? メソッドの呼び出しをprivate/protectedで制限するには?

オブジェクト指向言語の特長である「クラスの継承」をRubyで実現する方法を解説。スーパークラスのメソッドの呼び出し制限で、Ruby言語特有の内容についても紹介する。

14. クラスのメソッドをオーバーライドするには?

継承先クラスの新メソッドで元クラスの既存メソッドをオーバーライドして異なる機能に置き換える方法と、新メソッド内から既存メソッドを呼び出すことで既存機能に新機能を追加する方法を説明する。

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

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

イベント情報(メディアスポンサーです)

Azure Central の記事内容の紹介

Twitterでつぶやこう!


Build Insider賛同企業・団体

Build Insiderは、以下の企業・団体の支援を受けて活動しています(募集概要)。

ゴールドレベル

  • 日本マイクロソフト株式会社
  • グレープシティ株式会社