はじめに
Rubyのアクセス修飾子(メソッドの呼び出し制限の識別子)について、学習を開始してすぐに調べたことがあったのですが、その時の曖昧な理解のまま現在に至っていたので、機会があったのであらためて調べてみました。その結果、わかったことをまとめます。
定義をRubyリファレンスマニュアルで確認
メソッドは public、private、protected の三通りの呼び出し制限を持ちます。
- public に設定されたメソッドは制限なしに呼び出せます。
- private に設定されたメソッドは関数形式でしか呼び出せません。 ただし self.foo のように self. と書かれている場合は呼び出すことができます。
- protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せ ます。
引用:クラス/メソッドの定義
実際に確認してみる
うーん。リファレンスマニュアルの説明書きでは、どうもわかりづらいですね。。。。。
何はともあれ、文字で理解するより実際に動かしてみて理解する方が早い!
次の4つの場合について、getNameというメソッドで3通りの呼び出し制限(public,private,protected)を検証していきます。
- クラス外から呼び出してみる。
- クラス内の別のメソッドから関数形式で呼び出してみる
- 別のインスタンスから呼び出してみる。
- サブクラスから呼び出してみる
public
class User
#デフォルトは、public
def setName(name)
@name=name
end
def getName
@name
end
#②クラス内の別のメソッドから呼び出してみる
def sayHello
p "初めまして、#{getName}さん。"
p self
end
#③別のインスタンスから呼び出してみる。
def sayHelloTo(user)
p "初めまして、#{user.getName}さん。私は、#{getName}です。"
p self
p user.itself
end
end
#④サブクラスから呼び出してみる
class SubUser < User
end
#インスタンス作成して、インスタンス変数を持たせる。
user=User.new
user.setName("Syouta")
#①クラス外から呼び出してみる。
p user.getName
#=>"Syouta"
#②クラス内の別のメソッドから呼び出してみる
user.sayHello
#=>"初めまして、Syoutaさん。"
#=><User:0x00007f7b74894d28 @name="Syouta">
#③別のインスタンスから呼び出してみる。
#別のインスタンスを作成
anotherUser=User.new
anotherUser.setName("Takuya")
anotherUser.sayHelloTo(user)
#=>"初めまして、Syoutaさん。私は、Takuyaです。"
#=><User:0x00007f7b74894530 @name="Takuya">
#=><User:0x00007f7b74894d28 @name="Syouta">
#④サブクラスから呼び出してみる
subUser=SubUser.new
subUser.setName("Nobuto")
subUser.sayHelloTo(user)
#=>"初めまして、Syoutaさん。私は、Nobutoです。"
#=><SubUser:0x00007fef960c96c0 @name="Nobuto">
#=><User:0x00007fef960ca250 @name="Syouta">
結果
1~4全てエラーが出ませんでした。
public に設定されたメソッドは制限なしに呼び出せます。
補足:クラス内に定義したメソッドは、デフォルトでpublicになります。
デフォルトでは def 式がクラス定義の外(トップレベル)にあれば private、クラス定義の中にあれば public に定義します。これは Module#public、Module#private、 Module#protected を用いて変更できます。ただし Object#initialize という名前のメソッドと Object#initialize_copy という名前のメソッドは定義する場所に関係なく常に private になります。
private
class User
#デフォルトは、public
def setName(name)
@name=name
end
def getName
@name
end
#②クラス内の別のメソッドから呼び出してみる
def sayHello
p "初めまして、#{getName}さん。"
p self
end
#③別のインスタンスから呼び出してみる。
def sayHelloTo(user)
p "初めまして、#{user.getName}さん。私は、#{getName}です。"
p self
p user.itself
end
#getNameメソッドをpublicからprivateに変更
private :getName
end
#④サブクラスから呼び出してみる
class SubUser < User
end
#インスタンス作成して、インスタンス変数を持たせる。
user=User.new
user.setName("Syouta")
#①クラス外から呼び出してみる。=>エラー発生。
# p user.getName
#=>private method `getName' called for #<User:0x00007fa7458864b0 @name="Syouta"> (NoMethodError)
#②クラス内の別のメソッドから呼び出してみる
user.sayHello
#=>"初めまして、Syoutaさん。"
#=><User:0x00007fc62507e250 @name="Syouta">
#③別のインスタンスから呼び出してみる。=>エラー発生。
#別のインスタンスを作成
anotherUser=User.new
anotherUser.setName("Takuya")
anotherUser.sayHelloTo(user)
#=> private method `getName' called for #<User:0x00007fc62507e250 @name="Syouta"> (NoMethodError)
#④サブクラスから呼び出してみる=>エラー発生
subUser=SubUser.new
subUser.setName("Nobuto")
subUser.sayHelloTo(user)
#=> private method `getName' called for #<User:0x00007fcaf502c340 @name="Syouta"> (NoMethodError)
結果
1、3、4でNoMethodErrorが発生しました。
2は、出力に成功しています。
private に設定されたメソッドは関数形式でしか呼び出せません。 ただし self.foo のように self. と書かれている場合は呼び出すことができます。
protected
class User
#デフォルトは、public
def setName(name)
@name=name
end
def getName
@name
end
#②クラス内の別のメソッドから呼び出してみる
def sayHello
p "初めまして、#{getName}さん。"
p self
end
#③別のインスタンスから呼び出してみる。
def sayHelloTo(user)
p "初めまして、#{user.getName}さん。私は、#{getName}です。"
p self
p user.itself
end
#getNameメソッドをpublicからprivateに変更
protected :getName
end
#④サブクラスから呼び出してみる
class SubUser < User
end
#インスタンス作成して、インスタンス変数を持たせる。
user=User.new
user.setName("Syouta")
#①クラス外から呼び出してみる。=>エラー発生。
# p user.getName
#=>private method `getName' called for #<User:0x00007fa7458864b0 @name="Syouta"> (NoMethodError)
#②クラス内の別のメソッドから呼び出してみる
user.sayHello
#=>"初めまして、Syoutaさん。"
#=><User:0x00007fc62507e250 @name="Syouta">
#③別のインスタンスから呼び出してみる。=>エラー発生。
#別のインスタンスを作成
anotherUser=User.new
anotherUser.setName("Takuya")
anotherUser.sayHelloTo(user)
#=>"初めまして、Syoutaさん。私は、Takuyaです。"
#=><User:0x00007fc785955bd8 @name="Takuya">
#=><User:0x00007fc78595c488 @name="Syouta">
#④サブクラスから呼び出してみる
subUser=SubUser.new
subUser.setName("Nobuto")
subUser.sayHelloTo(user)
#=>"初めまして、Syoutaさん。私は、Nobutoです。"
#=><SubUser:0x00007fef960c96c0 @name="Nobuto">
#=><User:0x00007fef960ca250 @name="Syouta">
結果
1.のみエラーが発生しました。
2、3、4については、出力に成功しています。
3.で別のインスタンスをレシーバーにして呼び出しができていることから、privateメソッドのように関数形式のみという縛りもないようです。
protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せ ます。
結果・考察
場合 | public | private | protected |
---|---|---|---|
1. クラス外から呼び出してみる。 | 成功 | NoMethodError | NoMethodError |
2. クラス内の別のメソッドから関数形式で呼び出してみる | 成功 | 成功 | 成功 |
3. 別のインスタンスから呼び出してみる。 | 成功 | NoMethodError | 成功 |
4. サブクラスから呼び出してみる | 成功 | NoMethodError | 成功 |
privateとprotectedの違いが微妙でしたが、実際に動かしてみると一目瞭然。理解ができました。
私なりの解釈です。間違っていたら訂正していただけると幸いです。
- クラス内に定義されたprivateメソッドは、クラス外から呼び出すことができない。同一インスタンス内からのみ関数形式で呼び出すことができる。
- クラス内に定義されたprotectedメソッドは、クラス外から呼び出すことができない。同一インスタンス内で呼び出すことができ、別のインスタンスでも同一クラスやサブクラスのメソッドであれば実行するインスタンス毎に呼び出せる。(privateのように関数形式のみという縛りはなく、レシーバー付きで呼び出せるため、クラス外部から呼び出せないインスタンスメソッドのようなイメージでしょうか。)
呼び出し制限 | イメージ |
---|---|
public | 外部に公開されたメソッド |
private | 私的な個人メソッド(自分以外から隠蔽) |
protected | 仲間内で共有されるメソッド(仲間内以外から隠蔽) |
おまけ
protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せます。
この『instance_eval』って??
オブジェクトのコンテキストで文字列 expr またはオブジェクト自身をブロックパラメータとするブロックを評価してその結果を返します。
オブジェクトのコンテキストで評価するとは評価中の self をそのオブジェクトにして実行するということです。また、文字列 expr やブロック中でメソッドを定義すればそのオブジェクトの特異メソッドが定義されます。
オブジェクトの特異メソッドを追加するメソッドのようです。
このinstance_evalを使えばprivateメソッドも特異メソッド化できるので、外部から呼び出せちゃいます。
class User
#デフォルトは、public
def setName(name)
@name=name
end
def getName
@name
end
#②クラス内の別のメソッドから呼び出してみる
def sayHello
p "初めまして、#{getName}さん。"
p self
end
#③別のインスタンスから呼び出してみる。
def sayHelloTo(user)
# p "初めまして、#{user.getName}さん。私は、#{getName}です。"
#次のようにすればインスタンスの特異メソッド(privateメソッドとは別のメソッドとして扱える)として実行できるため
p "初めまして、#{user.instance_eval{getName}}さん。私は、#{getName}です。"
p self
p user.itself
end
#getNameメソッドをpublicからprivateに変更
private :getName
end
#④サブクラスから呼び出してみる
class SubUser < User
end
#インスタンス作成して、インスタンス変数を持たせる。
user=User.new
user.setName("Syouta")
#①クラス外から呼び出してみる。=>成功
#instance_evalを使えばprivateメソッドも外から呼び出せる。(特異メソッド化)
p user.instance_eval{getName}
#=>"Syouta"
#②クラス内の別のメソッドから呼び出してみる =>成功
user.sayHello
#=>"初めまして、Syoutaさん。"
#=><User:0x00007fc62507e250 @name="Syouta">
#③別のインスタンスから呼び出してみる。=>成功
#別のインスタンスを作成
anotherUser=User.new
anotherUser.setName("Takuya")
anotherUser.sayHelloTo(user)
#=> private method `getName' called for #<User:0x00007fc62507e250 @name="Syouta"> (NoMethodError)
#④サブクラスから呼び出してみる =>成功
subUser=SubUser.new
subUser.setName("Nobuto")
subUser.sayHelloTo(user)
#=> "初めまして、Syoutaさん。私は、Nobutoです。"
#=><SubUser:0x00007f7ca2144c38 @name="Nobuto">
#=><User:0x00007f7ca2145c00 @name="Syouta">
instance_evalメソッドを使うことで、特異メソッド化をすることができ、レシーバー付きで呼び出すことができるようになっています。