WEB+DB PRESS Vol.121の「特集 Ruby 3」を読んでいて、「その他の追加機能」に以下がありました。
private attr_reader :fooのようにシンボルを書けるようになったprivateがシンボルの配列を受け取れるようになった
1つ目はともかく2つ目がよくわからなかったので調べてみました。
Ruby 3.0で改善された点
結論から言うと、Ruby 3.0からは以下のように書けるようになりました。
class Foo
private attr_accessor :foo, :bar
# ここから下はpublic
end
- たとえばprivateなアクセサメソッドが欲しい場合なら、
private attr_accessorにシンボルまたは文字列を渡す形で1行に書けるようになった - 引数ありのアクセサメソッドはその行にだけ効くので、その後
publicで解除しなくてもよい - それによって
private attr_accessorをクラスの冒頭に簡潔に書けるようになった
さらにありがたい改修も含まれています。
attr_accessor/attr_reader/attr_writerにシンボル(文字列でもよい)を渡すと、それぞれのアクセサメソッドに応じて適切なゲッター/セッターメソッドをシンボルの配列で返すようになった
具体的にはこうです。
# Ruby 3.0.0
class Foo
@@accessor = attr_accessor :foo, :bar
@@reader = attr_reader :baz, :bam
@@writer = attr_writer :hoge, :huga
def accessor
p @@accessor
end
def reader
p @@reader
end
def writer
p @@writer
end
end
Foo.new.accessor #=> [:foo, :foo=, :bar, :bar=]
Foo.new.reader #=> [:baz, :bam]
Foo.new.writer #=> [:hoge=, :huga=]
つまり、attr_accessorの場合はゲッターとセッター、attr_readerの場合はゲッターだけ、attr_writerの場合はセッターだけを、シンボルの配列で返すようになりました。
従来は、アクセサメソッドを定義した後にアクセス制御をかけようとすると、以下のように:fooや:foo=を毎回自分で指定しなければならず、ついつい:foo=のセッターを書き忘れてしまったりしました。
# Ruby 2.7.2まで
class Foo
attr_accessor :foo, :bar
private :foo, :foo=, :bar, :bar=
end
なお、上のような書き方は、アクセサメソッドをprivateで保護しながら、クラス内のインスタンス変数にアクセサメソッド経由でアクセスする方法です。クラス内で@を書かずに.でインスタンス変数にアクセスできるので、この書き方を好む人もいるそうです。
参考: Rubyのインスタンス変数の直接参照について - 雑草SEの備忘録
Ruby 3.0では、それと同じことを以下のように1行で簡潔に書けるようになったわけです。地味にありがたい機能です。
class Foo
private attr_accessor :foo, :bar
end
Ruby 2.7.2まではどうだったか
クラス内のアクセス制御メソッドは、引数を与えればその引数にだけアクセス制御が効きますが、引数なしだと以後の行すべてに効きます。
以下のように、1行の中でprivateなどのアクセス制御メソッドに続けてattr_*などのアクセサメソッドを書くとエラーになります。書けそうなのに書けなかったんですね。
class Foo
private attr_accessor :foo, :bar # TypeError (nil is not a symbol nor a string)
def debug
@aa
end
end
これを回避するには、たとえばprivateなどのアクセス制御メソッドと、attr_*を別の行に書く必要がありました。
アクセス制御メソッドはクラスの冒頭に書きたいところですが、以下のように冒頭でprivateを使った場合、以後の行をprivateのままにしたくなければその後publicを呼んで、以後のアクセス制御を解除する必要がありました。
class Foo
private
attr_accessor :foo, :bar
public
def debug
@aa
end
end
publicを呼びたくない場合はprivateとアクセス制御メソッドをクラスの末尾に書くことになりますが、そうするとattr_*をクラスの冒頭に置けません。
class Foo
def debug
@aa
end
private
attr_accessor :foo, :bar
end
いずれにしろ1行では書けませんでした。
アクセサメソッドはnilを返していた
Module#attr_reader(Ruby 2.7.0 リファレンスマニュアル)Module#attr_writer(Ruby 2.7.0 リファレンスマニュアル)Module#attr_accessor(Ruby 2.7.0 リファレンスマニュアル)
attr_accessor、attr_reader、attr_writerは従来nilを返していました。publicやprivateなどと1行内で組み合わせる利用法は想定されていなかったのだろうと想像しました。
# Ruby 2.7.2
class Foo
@@accessor = attr_accessor :foo, :bar
@@reader = attr_reader :baz, :bam
@@writer = attr_writer :hoge, :huga
def accessor
p @@accessor
end
def reader
p @@reader
end
def writer
p @@writer
end
end
Foo.new.accessor #=> nil
Foo.new.reader #=> nil
Foo.new.writer #=> nil
なお、Rubyにはattrという短いアクセサメソッドも一応あり、Ruby 1.9以降はattr_readerと同等ですが、attrはRuboCopで怒られます。
参考: 5-11【統一】attrは原則使わない: Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど
アクセス制御メソッドはシンボルを配列で受け取れなかった
Module#private(Ruby 2.7.0 リファレンスマニュアル)Module#public(Ruby 2.7.0 リファレンスマニュアル)Module#protected(Ruby 2.7.0 リファレンスマニュアル)
Ruby 2.7.2までは、private/public/protectedには文字列かシンボルしか渡せませんでした。以下のように配列を渡すとエラーになりました。
# Ruby 2.7.2
class Foo
def foo
"foo"
end
def bar
"bar"
end
private [:foo, :bar] # TypeError ([:foo, :bar] is not a symbol nor a string)
end
その他
#17314のコメントを見ると、シンボルの配列を受け取れるようにする改修は元々public/protected/privateを対象としていたのが、結果としてprivate_class_methodとpublic_class_methodとトップレベルのprivateとprivateでも同じことができるようになっていたことがマージ後にわかったそうです。
# #17314#17より
class Foo
def self.foo; end
def self.bar; end
private_class_method [:foo, :bar] # No error
end
# #17314#17より
def foo = nil
def bar = nil
private [:foo, :bar]
public [:foo, :bar]
注
RubyのModuleにある可視性変更用の
private/public/protectedメソッドを本記事では「アクセス制御メソッド」と総称します。また、RubyのModuleにある
attr_accessor/attr_reader/attr_writerを本記事では「アクセサメソッド」と総称します。アクセサメソッドで定義されるメソッドについて、便宜上「ゲッター」「セッター」というJavaの用語も使っています。