Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby 3でprivate/public/protectedとattr_*アクセサを1行で書けるようになった

WEB+DB PRESS Vol.121の「特集 Ruby 3」を読んでいて、「その他の追加機能」に以下がありました。

  • private attr_reader :fooのようにシンボルを書けるようになった
  • privateがシンボルの配列を受け取れるようになった

1つ目はともかく2つ目がよくわからなかったので調べてみました。

RubyのModuleにある可視性変更用のprivate/public/protectedメソッドを本記事では「アクセス制御メソッド」と総称します。

また、RubyのModuleにあるattr_accessor/attr_reader/attr_writerを本記事では「アクセサメソッド」と総称します。アクセサメソッドで定義されるメソッドについて、便宜上「ゲッター」「セッター」というJavaの用語も使っています。

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を返していた

attr_accessorattr_readerattr_writerは従来nilを返していました。publicprivateなどと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)クラス設計・アクセサ・ダックタイピングなど

アクセス制御メソッドはシンボルを配列で受け取れなかった

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_methodpublic_class_methodとトップレベルのprivateprivateでも同じことができるようになっていたことがマージ後にわかったそうです。

# #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: 文字列リテラル同士はスペース文字で結合される


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。