はじめに
たとえばUserクラスにname
というprotectedメソッドがあったとします。
(protectedメソッドの登場頻度はかなり少ないと思いますが)
class User
protected
def name
'Alice'
emd
end
さらに、fetch_names
というpublicメソッドも定義されていたとします。
class User
def fetch_names(users)
users.map { |u| u.name }
end
protected
def name
'Alice'
end
end
ではfetch_names
メソッドを呼び出してみましょう。
users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> ["Alice", "Alice", "Alice"]
はい、問題なく呼び出せました。
(注:Rubyのprotectedメソッドは同じクラス、またはサブクラスから呼び出せるメソッドです)
しかし、リファクタリングすると……?
Ruby経験者であれば、fetch_names
の実装を見て「もっと短く書きたい!」と思うはずです。
そう、経験者ならRubyのイディオム的にusers.map(&:name)
のように書くことが多いはずです。
やってみましょう。
class User
def fetch_names(users)
users.map(&:name) # リファクタリング!?
end
protected
def name
'Alice'
end
end
users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> protected method `name' called for an instance of User (NoMethodError)
おっと、リファクタリングしたつもりが NoMethodError になってしまいました!
users.map(&:name) の形式で呼び出せるのはpublicメソッドだけ
Rubyの仕様として、users.map(&:name)
の&:name
の部分にあたるメソッドはpublicメソッドしか呼び出せません。
protectedメソッドは呼び出せないので、最初に示したコードのように、
users.map { |u| u.name }
通常のブロックで呼び出す必要があります。
おまけ:Ruby 3.2以降で微妙に変わった言語仕様
ところで、users.map(&:name)
のような呼び出し方は、以下のようなコードを書いているのとほぼ同じです。
# レシーバ u に対してメソッド :name を呼び出す
users.map { |u| :name.to_proc.call(u) }
実は :name.to_proc.call(u)
のような呼び出し方は、Ruby 3.1までエラーなく呼び出せていました。
RUBY_VERSION
#=> "3.1.2"
class User
def fetch_names(users)
# Ruby 3.1 まではエラーなく呼び出せていた
users.map { |u| :name.to_proc.call(u) }
# ただし、これは3.1以前でもNG
# users.map(&:name)
end
protected
def name
'Alice'
end
end
users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> ["Alice", "Alice", "Alice"]
しかし、Ruby 3.2以降ではエラーが起きます。
RUBY_VERSION
#=> "3.2.2"
# (略)
users = [User.new, User.new, User.new]
users[0].fetch_names(users)
#=> protected method `name' called for an instance of User (NoMethodError)
これは下記issueでの議論の結果、シンボルをproc化したときの可視性チェックが非常に難しいため、一律で「publicメソッド以外は呼び出せないようにする」という仕様変更が入ったためです。
In general,
a.b(&:c)
should behave exactly the same asa.b{_1.c}
. But visibility check forprotected
methods may be too difficult forto_proc
. So rejectingprotected
methods altogether likeprivate
methods is acceptable.Matz.
この仕様変更、僕は把握してなかったんですが、Ruby 3.2のNEWSには書いてなかったみたいです。(バグ修正扱いだったせいでしょうか? )
まとめ
というわけで、この記事では users.map(&:name)
の形式で呼び出せるのはpublicメソッドだけ、という話を書いてみました。
protectedメソッドを取り扱う機会はあまりないと思いますが、protectedメソッドは users.map(&:name)
では呼び出せないということを頭の片隅に置いておくと、いつか役に立つときが来るかもしれません(いや、来ないかな……😅)。