Ruby TIPS
クラスを継承するには? メソッドの呼び出しをprivate/protectedで制限するには?
オブジェクト指向言語の特長である「クラスの継承」をRubyで実現する方法を解説。スーパークラスのメソッドの呼び出し制限で、Ruby言語特有の内容についても紹介する。
クラスの継承を利用すれば、プログラムの機能を自然な形で分割してクラス群を構成したり、互換性を損なわずに機能をスムーズに追加・変更したりできるようになる。今回は、Rubyにおけるクラスの継承と、メソッドの呼び出し制限について見ていく。特に、メソッドの呼び出し制限については、他のプログラミング言語と異なるところがあるので、注意が必要となる。
クラスの継承
「TIPS: クラスとそのコンストラクター/アクセサー/メソッドを定義し利用するには?」では、簡単な例として平面上の点を表すPoint
クラスを作成した。今回は、Point
クラスに機能を追加したVector
クラスを作成してみよう。
Vector
クラスは原点(0,0)
から点(x,y)
に向けて引いた矢印のようなものだと思えばいい。Vector
クラスは標準添付のライブラリにもあり、かなり高度な機能を持つが、ここでは、それを極めて単純化したものを学習のために作ってみようというわけだ。
まず、Point
クラスの定義を見てみよう。ここでは、できるだけ簡単な定義にするために、アクセサーとコンストラクターだけを書いてある。ただし、原点から点(x,y)
までの距離を求めるlength
メソッドも含めてある。
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
|
アクセサーを定義して、x
プロパティとy
プロパティで座標を指定できるようにしてある。length
メソッドではピタゴラスの定理によって原点と(x,y)
の距離を計算する。
ここで、単に平面上の点と原点からの距離だけでなく、角度を求めたり、伸縮・回転させたりする必要が生じたとしよう。Point
クラスの定義にそういった機能を追加することもできるが、すでにPoint
クラスを他のプログラムから利用しているのであれば、定義の変更によってトラブルが起こる危険もある。そこで、Point
クラスはできるだけ書き換えずに、新たな機能を追加したVector
クラスを作成すれば、変更にも対応しやすい(もちろん、そういった無計画な増築にも対応できるようにというのが先に立つ理由ではなく、そもそもの設計上の要請から機能を分割するのが本来の姿なのだが)。
ともあれ、このように、あるクラスを基に新たなクラスを作成することを継承と呼ぶ。また、作成元となるクラスをスーパークラスと呼び、新たに作成されたクラスをサブクラスと呼ぶ。ここでは、Point
クラスがスーパークラス、Vector
クラスがサブクラスとなる。
では、Vector
クラスの定義を見てみよう。
class Vector < Point # Pointクラスを継承
def angle
return acos(@x / length) # 角度を返す
end
end
|
クラス名の後に<
を書き、スーパークラスの名前を書けばサブクラスにできる。ここでは、angle
メソッドを定義して、角度を求められるようにしてある。
「クラス名 < スーパークラス名」の形式でクラス定義を書くだけなので、書き方にはそれほど難しい点はない。このようにしてサブクラスを定義すると、特に何も書かなくてもスーパークラスの機能を利用できる。例えば、Vector
クラスには座標を設定するための記述はないが、Point
クラスの機能を受け継いでいるので、x
プロパティやy
プロパティが使える。もちろん、コンストラクターも受け継いでいるし、length
メソッドも使える。さらに、Vector
クラスで新しく定義したangle
メソッドも使えるようになった。angle
メソッドの中で、Point
クラスのlength
メソッドを呼び出していることからもスーパークラスの機能を継承していることが分かるだろう。
では、Vector
クラスを利用して、ベクトルの長さや角度を出力するコードを書いてみよう。
v = Vector.new(3, 4) # Vectorクラスのインスタンスを作成
puts v.length # スーパークラスのlengthメソッドを呼び出す
puts v.angle # このクラスのangleメソッドを呼び出す
|
new
メソッドを使ってVector
クラスのインスタンスを作成する。Vector
クラスの定義にはコンストラクターは書かれていないが、Point
クラスから継承されたコンストラクターが呼び出され、座標が設定される。スーパークラスのlength
メソッドを呼び出して原点との距離を求めることもできるし、Vector
クラスのangle
メソッドを呼び出して角度を求めることもできる。
上記リスト1.1~1.3の全コードが1つのファイルに入力されていることに注意。実行すると以下のようになる。
$ ruby sample001.rb
5.0
0.9272952180016123
$
|
底辺が3、高さが4の直角三角形の斜辺の長さは5である。角度はラジアン単位であることに注意。0.927ラジアン≒53.1°
となる。
【コラム】角度の求め方
リスト1.2やリスト1.3のangle
メソッドでは、以下の図のような三角形のθの値を求める。a/cの値はcosθの値と等しい。つまりcosθの値は分かっている。そこで、cos
関数の逆関数であるacos
関数を使って、cosθの値(=a/c)を基に角度を求める。
※2016/11/04更新(このコラム内の以下を追記して内容を訂正しました。訂正内容については本稿末尾の【更新履歴(お詫びと訂正)】を参照してください): ただし、このacos
関数では正しく角度が求められない場合がある。というのも、y
の値が負の場合にも正の場合と同じ角度が返されてしまうからだ(例えば、v = Vector.new(3, 4)
でベクトルを作成した場合も、v = Vector.new(3, -4)
でベクトルを作成した場合も、同じ値が返される)。これは、角度の計算にy
の符号が反映されないためである。
そこで、反時計回りで角度を求めるには、リスト1.2を以下のようなコードに書き換えるといい。
class Vector < Point # Pointクラスを継承
def angle
t = acos(@x / length)
return @y >= 0 ? t : PI * 2 - t
end
end
|
-π<θ<=π
で値を返すなら、return
文を以下のように書き換えるといい。
return y >= 0 ? t : -t
|
この場合、atan2
関数を使うこともできる(ただし、length
メソッドが使われないので、今回の説明からは外れるが)。
class Vector < Point # Pointクラスを継承
def angle
return atan2(@y, @x)
end
end
|
なお、複素数を使っても同様の結果が得られる。その場合は、以下のようになる。
class Vector < Point # Pointクラスを継承
def angle
return Complex(@x, @y).angle
end
end
|
この場合の返り値は、後者の範囲(-π<θ<=π)
の値となる。
メソッドの呼び出し制限(privateなメソッド)
クラス内のメソッドは、特に何も指定されていなければ、public
なメソッドと見なされる(ただしコンストラクターはprivate
なメソッドとなる)。public
なメソッドは、クラス内からでも、他のクラスからでも利用できる。一方、private
なメソッドはクラス内でのみ利用できるメソッドである。だが、実はprivate
なメソッドはサブクラスからでも利用できる。他のプログラミング言語の経験がある人には意外かもしれないが、実際にそうなっているので、試してみるといい。
public
やprivate
といったキーワードを書くと、それ以降に定義されたメソッドの呼び出し制限が指定できる。以下のように、Point
クラスのlength
メソッドをprivate
にしてみよう。
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メソッドを呼び出す
|
Point
クラスの中で、length
メソッドの定義の前にprivate
を追加した。なお、下から2行目のputs v.length
はエラーになるので、コメントアウトしてある。
実行例は以下の通り。angle
メソッドは正しく結果を表示する。Vector
クラスのangle
メソッドの中で、スーパークラスのlength
メソッドを利用していることに注目してほしい。Point
クラスでprivate
に指定したlength
メソッドがサブクラスであるVector
クラスから呼び出せることが分かる。実行例は以下の通り。
$ ruby sample002.rb
0.9272952180016123
$
|
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)
$
|
サブクラスのインスタンスからはスーパークラスのprivate
なメソッドは呼び出せない。
他のクラスや他のクラスのインスタンスからメソッドを呼び出すにはpublic
である必要がある。サブクラスの定義の中ではスーパークラスのprivate
なメソッドは呼び出せるが、サブクラスのインスタンスからはスーパークラスのprivate
なメソッドを直接呼び出すことはできない。
メソッドの呼び出し制限(protectedなメソッド)
メソッドの呼び出し制限にはpublic
、private
の他に、protected
も指定できる。これも他のプログラミング言語を知っている人には理解しづらい動作なので、丁寧に確認しておこう。
protected
なメソッドは、レシーバーをサブクラスの定義内で指定した場合にも利用できる。レシーバーとは、メソッドが呼び出されるオブジェクトのことである。リスト1.4を以下のように書き換えてみよう。
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メソッドを呼び出す
|
Point
クラスの中で、length
メソッドの定義の前にprotected
を追加した。Vector
クラスのangle
メソッドでは、length
メソッドにレシーバー(self
)を指定している。なお、下から2行目のputs v.length
はエラーになるので、コメントアウトしてある。
リスト1.4のprivate
をprotected
に書き換えただけでは同じ動作にしかならないので違いは分からない。しかし、サブクラスのメソッド定義の中でレシーバーを指定した場合に違いが出る。ここでは自分自身を表すself
を指定してある。リスト1.5を実行すると、スーパークラスのlength
メソッドが正しく呼び出される。実行結果は、実行例1.2と同じだが一応掲載しておく。
$ ruby sample003.rb
0.9272952180016123
$
|
angle
メソッドでは、スーパークラスのlength
メソッドが呼び出されている。length
メソッドがprivate
なメソッドだとレシーバーは指定できないが、protected
なメソッドなら問題なく呼び出せる。
試しに、protected
をprivate
に書き換えて実行してみると、以下のようなエラーが表示される。
$ ruby sample003.rb
sample003.rb:20:in `<main>': private method `length' called for #<Vector:0x007ffbd00260b0 @x=3, @y=4> (NoMethodError)
$
|
private
なメソッドはサブクラスからでも使えるはずなのにエラーになった。一体なぜなのか……。
private
なメソッドの呼び出しにはレシーバーが指定できない。一方、protected
なメソッドであればレシーバーを書いていても書いていなくても正しく呼び出せる。以下のように、サブクラスの中でインスタンスを作成して利用するような場合も同様である。
class Vector < Point # Pointクラスを継承
def angle
temp = Vector.new(@x, @y) # インスタンスを作成
return acos(temp.x / temp.length) # 角度を返す
end
end
|
この例では、作成したインスタンスを参照する変数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
といった書き方にする(メソッド名には小文字と_
を使う。アクセサーにはget
やset
を付けない)べきであるというご指摘もありました。それに関する修正を全体に施しました。これは著者の不勉強にほかなりません。ご指摘くださった皆さまありがとうございました。
※以下では、本稿の前後を合わせて5回分(第11回~第15回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
11. ブロック ― ちょっと便利な繰り返し処理の構文とは?(3)
Rubyに用意されている繰り返し処理の構文のうち「ブロック」を使えば、繰り返し処理をより簡潔に書ける。その基本的な使い方と、自作メソッドでの利用例を解説する。
12. クラスとそのコンストラクター/アクセサー/メソッドを定義し利用するには?
Rubyプログラミングの基本中の基本として、クラスの定義から、そのインスタンスの作成・利用、インスタンスメソッドの定義、変数へのアクセスまでを説明する。
13. 【現在、表示中】≫ クラスを継承するには? メソッドの呼び出しをprivate/protectedで制限するには?
オブジェクト指向言語の特長である「クラスの継承」をRubyで実現する方法を解説。スーパークラスのメソッドの呼び出し制限で、Ruby言語特有の内容についても紹介する。
14. クラスのメソッドをオーバーライドするには?
継承先クラスの新メソッドで元クラスの既存メソッドをオーバーライドして異なる機能に置き換える方法と、新メソッド内から既存メソッドを呼び出すことで既存機能に新機能を追加する方法を説明する。