>> 連載の目次は こちら!
■ 概要
- ようやくクラスの話まで到達 (;´ρ`)
- 長くなりそうなので数回に分けて解説する
- 1回目は、基本的なクラスの定義方法と継承について整理してみる
- モジュールの件もどこかに入ってくる予定
■ クラスの基本的な定義と利用
class Player
# クラスの定数
# @see https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#const
#
# 名前の先頭が大文字だと定数として扱われる。通常は全部大文字にするはず。
# クラス(およびサブクラス)の中、およびクラスの外からもアクセスできる
# クラスの外からは クラス名::定数名 でアクセスする
MIND = "love and peace!"
# クラス変数
# @see https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#class
#
# 変数名の先頭に @@(アットマークふたつ)を付ける
# クラスで一意に値を保持する ==> インスタンス間で値が共有される。
# クラス(およびサブクラス)の中からアクセスできる
# メソッドを用意しないとクラスの外からはアクセスできない(後述。アクセス制御について次回解説予定)
@@music_fund = 0
# 初期化メソッド initialize
# @see https://docs.ruby-lang.org/ja/latest/method/Object/i/initialize.html
#
# いわゆるコンストラクタに当たるクラスメソッド。
# インスタンス生成時の new で実行される。
def initialize(yen)
# インスタンス変数
# @see https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#instance
#
# 変数名の先頭に @(アットマークひとつ)を付ける
# インスタンスごとに個別に値を保持する
# クラス(およびサブクラス)のインスタンスメソッドおよびinitializeメソッドからアクセスできる
# 他の多くの言語のようにメソッドの外で宣言せず、initializeメソッドまたは任意のインスタンスメソッドの中で初期化する。
# ※メソッドの外で宣言すると違う種類の変数になっちゃう(後述のクラスインスタンス変数)
# アクセサなどのメソッドを用意しないとクラスの外からはアクセスできない(後述。アクセス制御について次回解説予定)
@pocket_money = yen
@parent_only = 123
end
# インスタンスメソッド
# @see https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#method
#
# クラスのインスタンスをレシーバとして実行されるメソッド
# インスタンスメソッドへのアクセス可能範囲は、アクセス制御(後述。次回解説予定)による
def play
puts "listen to me!"
end
def thanks(to)
# クラスの定数へのクラス内からのアクセスは、定数名そのままでOK
puts "%s thanks! %s" % [to, MIND]
end
def donate(yen)
# インスタンスメソッドからクラス変数へアクセスしてみる
@@music_fund += yen;
end
# クラスメソッド
# @see https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#class_method
#
# クラスそのものをレシーバとして実行されるメソッド
# 意味的には、クラスの特異メソッドに該当するらしい(特異クラスについては次回以降に解説予定)
# なので定義の記述方法はほかにもある(後述)
def self.fund_total
# クラスメソッドからクラス変数へアクセスしてみる
puts @@music_fund.to_s + "円"
# クラスメソッドからインスタンス変数へアクセス...はできない。nilが返る
# p @pocket_money
end
def save_pocket_money(yen)
# インスタンスメソッドからインスタンス変数へアクセスしてみる
@pocket_money += yen
end
def pocket_money_total
# 同上
puts @pocket_money.to_s + "円"
end
# インスタンス変数/クラス変数/定数へのアクセス確認
def dump_vars
p @pocket_money, @parent_only, @@music_fund, MIND
end
end
# クラスのインスタンス化(オブジェクトの生成)
player1 = Player.new(100) # おこづかい手持ち100円の演奏家
player2 = Player.new(90) # おこづかい手持ち90円の演奏家
# クラス名の取得
puts player1.class # Player
# インスタンスメソッドの実行
player1.play # listen to me!
# クラス変数にインスタンスメソッドを通してアクセスしてみる
player1.donate(130) # 共有の音楽基金(インスタンス間で共有するクラス変数)に130円寄付
player2.donate(250) # 共有の音楽基金(インスタンス間で共有するクラス変数)に250円寄付
# クラス変数にクラスメソッドを通してアクセスしてみる
Player.fund_total # 380円 別インスタンスからの寄付金が合計されている(クラス変数の値が共有されていることがわかる)
# インスタンス変数にインスタンスメソッドを通してアクセスしてみる
player1.save_pocket_money(450) # 路上で演奏して450円稼いだのでお財布へ
player2.save_pocket_money(550) # 路上で演奏して550円稼いだのでお財布へ
player1.pocket_money_total # 550円 個人のお金(インスタンス変数)は550円になった
player2.pocket_money_total # 640円 個人のお金(インスタンス変数)は640円になった
# インスタンス変数/クラス変数/定数へのアクセス確認
player1.dump_vars
# 550 インスタンス変数にアクセスできている
# 123 同上
# 380 クラス変数にアクセスできている
# "love and peace!" 定数にアクセスできている
# クラスの定数に、クラス外からアクセス
puts Player::MIND # love and peace! クラスの定数へは :: でアクセスする
■ クラスの継承
# ここでは 先程の Player のサブクラス Singer を定義してみる
class Singer < Player
# サブクラス側でも独自に定数とクラス変数を定義してみる
VOICE = "miracle!"
@@vocal_insurance = 0
# サブクラス側で初期化メソッド initialize をオーバーライドしてみる
# 省略した場合は親クラスの initialize が継承される
def initialize(yen, microphone)
# super で親クラスの initialize を実行できる
# 引数を省略すると、サブクラスのinitializeで受け取った引数がそのまま親クラスに渡される
# この例だと、親クラスの引数は1つなので、super だけだと引数を2つ渡そうとしてエラーになる
super(yen)
# サブクラス側でも独自にインスタンス変数を定義してみる
@microphone = microphone
end
# playメソッドをSinger風にオーバーライドしてみる
def play
super # 親クラスの同名のメソッドを実行。super(xxx)で引数も渡せる
puts "La La La ♫"
thanks("everybody") # 親クラスから普通に継承したメソッドを実行
end
# サブクラス側で独自にメソッドを定義してみる
# (親クラス側で用意したインスタンス変数/クラス変数/定数にサブクラスからアクセスできるか確認してみる)
def dump_parent_vars
p @pocket_money # newやメソッドでサブクラス側から値を渡す想定のインスタンス変数
p @parent_only # 親クラス側でしか初期化していない想定のインスタンス変数
p @@music_fund # 親クラスで保持するクラス変数
p MIND # 親クラスの定数
end
# サブクラス側のインスタンス変数/クラス変数/定数へのアクセス確認
def dump_child_vars
# まあ普通にアクセスできるはず
p @microphone, @@vocal_insurance, VOICE
end
end
# サブクラスのインスタンス化(オブジェクトの生成)
singer1 = Singer.new(30, "FOSTEX") # おこづかい手持ち30円のシンガー。マイクはFOSTEX
singer2 = Singer.new(50, "BEHRINGER") # おこづかい手持ち50円のシンガー。マイクはBEHRINGER
# オーバーライドされたインスタンスメソッドの実行
singer1.play
# listen to me!
# La La La ♫
# everybody thanks! love and peace!
# 親クラス名の取得
puts Singer.superclass # Player
puts Player.superclass # Object
# 親クラスで用意されたクラス変数に、インスタンスメソッドを通してアクセスする
singer1.donate(100) # 共有の音楽基金(インスタンス間で共有するクラス変数)に100円寄付
singer2.donate(200) # 共有の音楽基金(インスタンス間で共有するクラス変数)に200円寄付
# 親クラスで用意されたクラス変数に、クラスメソッドを通してアクセスする
Singer.fund_total # 300円
# 音楽基金は合計で300円になっている。クラス変数の値がインスタンス間で共有されていることがわかる
# また、親クラスのクラスメソッドも継承されていることがわかる
# 親クラスで用意されたインスタンス変数に、インスタンスメソッドを通してアクセスする
singer1.save_pocket_money(600) # 路上で歌って600円稼いだのでお財布へ
singer2.save_pocket_money(750) # 路上で歌って750円稼いだのでお財布へ
singer1.pocket_money_total # 個人のお金(インスタンス変数)は630円になった
singer2.pocket_money_total # 個人のお金(インスタンス変数)は800円になった
# 親クラスで用意されたインスタンス変数/クラス変数/定数にサブクラスからアクセスできるか確認
singer1.dump_parent_vars
# 630 親クラス側で用意されたインスタンス変数にサブクラスからアクセスできている。値はサブクラス側からセットした金額が反映されている
# 123 親クラス側で用意されたインスタンス変数にサブクラスからアクセスできている。値は親クラスで初期化した値が見えている
# 300 親クラスで保持しているクラス変数にアクセスできている。
# "love and peace!" 親クラスの定数にアクセスできている
# サブクラス自身のインスタンス変数/クラス変数/定数へのアクセス確認
singer2.dump_child_vars
# "BEHRINGER" まあ、
# 0 普通に、
# "miracle" アクセスできてます!
# クラスの定数に、クラス外からアクセス
puts Player::MIND # love and peace! クラスの定数へは :: でアクセスする
puts Singer::MIND # love and peace! クラスの定数はサブクラスからも同様にアクセスできる
■ クラスメソッドの定義方法いろいろ
- 前述の通り、Rubyにおけるクラスメソッドは、クラスの特異メソッドに該当する
- なので、定義方法はいろいろある。ここで整理しておく
- 特異クラスについては次回以降に解説予定
class MyTest
# 方法1 クラス内で self に対して追加する
# いちばんシンプルかな
def self.my_class_method_1
puts __method__
end
# 方法2 クラス内で、クラスに対して追加する
# 個別にこれをやると、クラス名が変わったときめんどいな
def MyTest.my_class_method_2
puts __method__
end
# 方法3 クラス内で self を引いて特異クラスを定義してそこで定義する
# まとめて定義できるな
class << self
def my_class_method_3_1
puts __method__
end
def my_class_method_3_2
puts __method__
end
end
# 方法4 クラス内で、クラスを引いて特異クラスを定義してそこで定義する
# まとめて定義できるな
# クラス名が変わったらめんどいけど、まとめて定義してれば被害は少ないな
class << MyTest
def my_class_method_4_1
puts __method__
end
def my_class_method_4_2
puts __method__
end
end
end
# 方法5 クラスの外で、クラスに対して追加する
def MyTest.my_class_method_5
puts __method__
end
# 方法6 クラスの外で、クラスを引いて特異クラスを定義して、そのなかに定義する
# この方法もまとめて定義できるな
class << MyTest
def my_class_method_6_1
puts __method__
end
def my_class_method_6_2
puts __method__
end
end
# 全部クラスメソッドだから、こんな感じで呼べるぜ!
MyTest.my_class_method_1
MyTest.my_class_method_2
MyTest.my_class_method_3_1
MyTest.my_class_method_3_2
MyTest.my_class_method_4_1
MyTest.my_class_method_4_2
MyTest.my_class_method_5
MyTest.my_class_method_6_1
MyTest.my_class_method_6_2
■ クラスインスタンス変数って何やねん?
独特のアクセススコープになるから、利用する場面は限られそうだが、知らないと意図せずにやってしまいそうな...
仕組みとして理解しておく必要はありそう。
●参考URL
※当記事のコメント欄も参照してください。
●クラスインスタンス変数 の位置付けを理解する
クラスインスタンス変数については、以下の流れで理解できました。
@scivola さん、@kts_h さん、ありがとうございました。
① 以下の前提がある。
・クラスの定義自体が、Classというクラスのインスタンスである
・そしてそれは、ソースコード上では、クラス名そのもの(たとえばFoo)として表記される、あのオブジェクトである
class Foo
end
puts Foo.class # Class
② 上記の前提で、クラスインスタンス変数は、
Fooのインスタンスではなく、Fooの定義を表す唯一のClassクラスのインスタンス(すなわちソースコード上で Foo と表記されるあのオブジェクト)に変数が作成される。
↓
だから、Foo.xxx と表記されるようなクラスメソッド(Fooというオブジェクトをレシーバとするメソッド)からしかアクセスできない。
という流れで理解できました。
↓初期の自分の解釈は念のため以下に残しておきます。
他の言語では、インスタンス変数って、メソッドの外(多くの場合はクラス定義の先頭に近い部分)で、まず宣言するよね
↓
でも、Rubyでそれをやると、「クラスインスタンス変数」と呼ばれる(?)不思議な存在になる
↓
クラスインスタンス変数は、@ではじまっているので、見た目はそのクラスのインスタンス変数と変わらないんだけど、
↓
そのクラスのクラスメソッドからしかアクセスできない(インスタンスメソッドからは見えない。サブクラスからも見えない)
↓
なぜか?
↓
Rubyでは、クラス自体も Class
というクラスのオブジェクトである
↓
つまり、あるクラス
を定義することは、Classクラスのインスタンスを生成し、その機能を拡張(再定義)することである
↓
なので、あるクラス
定義の直下(メソッドの外)でインスタンス変数を宣言すると、
↓
Classクラスのインスタンスに後付けでインスタンス変数を定義したことになる
↓
Classクラスのインスタンスのインスタンス変数である以上、
↓
あるクラス
のインスタンスからアクセスすることはできない(あるクラス
のインスタンスメソッドからは見えない)
↓
じゃあ、なんでクラスメソッドからは見えるのか?
↓
クラスメソッドは、クラスの定義に紐付いており、文脈的にClassクラスのインスタンスが見えているから?
●コードで振る舞いを確認する
class MyClass
# クラス直下(メソッドの外)で、@変数 を宣言(クラスインスタンス変数)
@c1_outer = "xxx"
# ある時点での @変数 にアクセスするインスタンスメソッドtest1
def test1
p @c1_outer
end
# 自分の文脈で @変数 を初期化してから @変数 にアクセスするインスタンスメソッドtest2
def test2
@c1_outer = "yyy"
p @c1_outer
end
# staticな文脈(?)で @変数 にアクセスするクラスメソッドtest3
def self.test3
p @c1_outer
end
end
# サブクラス
class MySubClass < MyClass
# ある時点での @変数 にアクセスするサブクラスのインスタンスメソッドtest4
def test4
p @c1_outer
end
# 自分の文脈で @変数 を初期化してから @変数 にアクセスするサブクラスのインスタンスメソッドtest5
def test5
@c1_outer = "zzz"
p @c1_outer
end
# staticな文脈(?)で @変数 にアクセスするサブクラスのクラスメソッドtest6
def self.test6
p @c1_outer
end
end
objParent = MyClass.new
objChild = MySubClass.new
objParent.test1 # nil この時点で見えるとしたらクラスインスタンス変数だが、インスタンスメソッドから見えていない
objParent.test2 # "yyy" メソッド内で同名で定義すると、そのクラスのインスタンス変数として初期化される
objParent.test1 # "yyy" なので、今度はアクセスできる(あくまでもtest2で初期化された方のインスタンス変数)
MyClass.test3 # "xxx" クラスメソッドからは、クラスインスタンス変数にアクセスできている
objChild.test4 # nil
# 親クラスのクラスインスタンス変数にインスタンスメソッドからアクセスできない。
# もちろん親クラスの普通のインスタンス変数は別インスタンスなので見えない。
# 自クラス的にも初期化していない
objChild.test5 # "zzz" サブクラス側のインスタンス変数として初期化しているから見える
objChild.test4 # "zzz" なので、今度はアクセスできる(あくまでもtest5で初期化されたサブクラスのインスタンス変数)
MySubClass.test6 # nil こちらはクラスメソッドだが、親クラスのクラスインスタンス変数は見えていない