attr_accessorとは
attr_accessor
はインスタンス変数に対するゲッター・セッターを同時に定義するための便利メソッド。
例えば、以下のコードでは @earth_shape
というインスタンス変数に対する #earth_shape
(ゲッター) 、#earth_shape=
(セッター) を同時に定義してくれる。
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
end
secret = TopSecret.new
# ゲッターを使って、インスタンス変数の値を得る
puts secret.earth_shape # 'flat' を出力
# セッターを使って、インスタンス変数の値を変える
secret.earth_shape = 'round'
puts secret.earth_shape # 'round' を出力
定義されるのは通常のメソッドと変わりないので、クラス内でゲッター・セッターメソッドを呼ぶこともできる。
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
def reveal
# クラス内からゲッターを呼ぶ
puts "The Earth is #{earth_shape}."
end
end
secret = TopSecret.new
secret.reveal # 'The Earth is flat.' と出力
セッターを使う際の落とし穴
しかし、クラス内でセッターを使う際には落とし穴がある:
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
def reveal
# クラス内からセッターを呼ぶ?
earth_shape = 'round'
# クラス内からゲッターを呼ぶ?
puts "The Earth is #{earth_shape}."
end
end
secret = TopSecret.new
secret.reveal # 'The Earth is round.' と出力
上の #reveal
メソッドは一見セッターとゲッターを呼んでいるように見えるが、実はそうではない。
コードの末尾に以下を追加すれば、何かがおかしいことが分かるはず:
puts secret.earth_shape # 'flat' と出力
#reveal
メソッドでセッターが呼ばれて @earth_shape
の値が 'round'
に変わったのであれば、上のコードは 'round'
と出力するはずである。
では、どこがおかしいのか?
問題は、 #reveal
メソッド内の以下の行だ:
# クラス内からセッターを呼ぶ?
earth_shape = 'round'
実は、Rubyはこの式をローカル変数の代入式として解釈するのである。
つまり、この時点でローカル変数 earth_shape
が誕生し、その値に 'round'
が入ったということだ。
さらに、 attr_accessor
で定義されたゲッターメソッド、 #earth_shape
は、「たまたま」そのローカル変数 earth_shape
と名前が同じだったことで、ローカル変数の背後に隠れてしまう (この挙動は「シャドーイング」と呼ばれる) 。
よって、以下の
# クラス内からゲッターを呼ぶ?
puts "The Earth is #{earth_shape}."
も、実はゲッターを呼び出しているのではなく、ローカル変数の値を参照しているのだ。
このため、 #reveal
メソッド内ではインスタンス変数 @earth_shape
には指一本触れていないので、その値は元々の 'flat'
のままということだ。
puts secret.earth_shape # 'flat' と出力
さらなる落とし穴: 条件分岐
この程度の落とし穴にハマるものかという方に、以下のコードを見ていただきたい:
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
def reveal(redact:)
earth_shape = 'round' if redact
puts "The Earth is #{earth_shape}."
end
end
secret = TopSecret.new
secret.reveal(redact: true) # 何が出力される?
puts secret.earth_shape # 何が出力される?
secret.reveal(redact: false) # 何が出力される?
puts secret.earth_shape # 何が出力される?
4つの質問の答え、分かるだろうか?
最初の2つに関してはif節がない版のコードと同様で、ゲッターとセッターがローカル変数 earth_shape
に隠されて、インスタンス変数 @earth_shape
は変化しない。
secret.reveal(redact: true) # 'The Earth is round.' と出力
puts secret.earth_shape # 'flat' と出力
残りの2つが異端児である。
先に解答を見てみよう:
secret.reveal(redact: false) # 'The Earth is .' と出力
puts secret.earth_shape # 'flat' と出力
インスタンス変数 @earth_shape
の値が 'flat'
のままなのは分かるとして、 'The Earth is .'
という出力は予想外ではないだろうか。
理由は、条件分岐内での変数代入の意外ともいえる挙動にある。
#reveal
メソッドを redact: false
で呼んだ場合、以下のコードでローカル変数への代入が行われないのはお分かりだろう。
earth_shape = "round" if redact
変数への代入が行われなかったので、シャドーイングは発生せずに earth_shape
でゲッターメソッドが呼ばれると考えた方もいるはずだ:
# ゲッターを呼んでいる?
puts "The Earth is #{earth_shape}." # 'The Earth is flat.' と出力?
残念ながらそうではない。
変数への代入が行われなかったのは事実だが、 earth_shape
はどのみちローカル変数を指すのである。
なぜなら、if節・case節などの分岐の中で変数への代入を行った場合、たとえその分岐が実行されなくとも変数の「作成」は行われるというRubyの言語上の決まりがあるからだ。
そして、作成直後の変数の値は nil
となるため、文字列に埋め込まれたときに空文字列に置換され、 'The Earth is .'
という出力になったのである。
どうすれば回避できるか
前述の通り、クラス内でゲッター・セッターメソッドを使う場合「シャドーイング」による落とし穴に気をつけなければならない。
予期せぬシャドーイングを未然に防ぐには、「self
キーワードを使う方法」や「インスタンス変数を直接使う方法」などがある。
self
キーワードを使う方法
self
キーワードをインスタンスメソッド内で使うと、そのメソッドを呼び出す際に使ったクラスのインスタンスを指す (「レシーバー」という言い方もある) 。
ゲッター・セッターメソッドの呼び出しの前に self.
をつけることで、「ローカル変数ではなくインスタンスメソッドを参照しています」という意図をRubyに伝えることができる:
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
def reveal
self.earth_shape = 'round'
puts "The Earth is #{self.earth_shape}."
end
end
secret = TopSecret.new
secret.reveal # 'The Earth is round.' と出力
puts secret.earth_shape # 'round' と出力
実行結果から、ローカル変数の作成は行われずにインスタンス変数が直接変更されていることが分かる。
ただし、Rubocopを使ってリントをしている方は、以下のようなお節介アドバイスをされるかもしれない:
super_secret.rb:10:26: C: [Correctable] Style/RedundantSelf: Redundant self detected.
puts "The Earth is #{self.earth_shape}."
^^^^^^^^^^^^^^^^
Rubocopののデフォルト設定では、余計な self
を書くと怒られるようになっている。
理由としては、メソッド内に earth_shape
というローカル変数がないのだから、わざわざ self.
をつけなくともゲッターの呼び出しだと分かるというところだろう。
Rubocopに逆らわない善良な市民であれば、以下のように書くこととなる:
def reveal
self.earth_shape = 'round'
puts "The Earth is #{earth_shape}."
end
個人的に、セッターの前にだけ self.
が付いていることに不釣り合いさを感じる。
さらには、メソッドが長くなった場合、一目見てローカル変数かゲッターメソッドかが分からないという問題もあるだろう。
あくまでデフォルト設定なので、Rubocopの設定ファイルをいじってRubocopの権力乱用に反旗を翻すのも一つの手である。
ただ、その前に2つ目の方法を見ていただきたい。
インスタンス変数を直接使う方法
attr_accessor
を使うと、ゲッターと同じ名前のインスタンス変数が作られることはご存知だろう。
インスタンスメソッド内であればインスタンス変数を使えるのだから、以下のように書くことができる:
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
def reveal
@earth_shape = 'round'
puts "The Earth is #{@earth_shape}."
end
end
secret = TopSecret.new
secret.reveal # 'The Earth is round.' と出力
puts secret.earth_shape # 'round' と出力
この方法であれば、ローカル変数とインスタンス変数の混乱もなく分かりやすいだろう。
こちらのほうがむしろ自然という見方もできる。
attr_accessor
はあくまでクラスの外部からインスタンス変数にアクセスするセッターとゲッターを定義するためのものと考えるのであれば、クラスの内部でそれらを使う必要はないからだ。
ただし、こちらはこちらでいくつか考察に値する点がある。
まず、インスタンス変数の性質上、名前を間違えてもエラーが検知され辛いという点がある:
class TopSecret
attr_accessor :earth_shape
def initialize
@earth_shape = 'flat'
end
def reveal
# つづりこれで合ってたっけ?
@earf_shape = 'round'
puts "The Earth is #{@earf_shape}."
end
end
secret = TopSecret.new
secret.reveal # 'The Earth is round.' と出力
puts secret.earth_shape # 'flat' と出力
ここでは、 @earth_shape
とは別のインスタンス変数 @earf_shape
が作成され、その値が読み書きされている。
単体テスト等を念入りに書いていれば検知はできるだろうが、ゲッター・セッターを使っていた場合は存在しないメソッドを呼び出した時点でランタイムエラーとなることと比較してほしい。
「間違っているけど何故か動く」コードよりは「間違っていたからエラー終了する」コードのほうが、長期的に見れば負債は少ないことも合わせて考えたいところだ。
また、 attr_accessor
で定義されるゲッター・セッターは値を直接読み書きするだけの単純なメソッドであるが、仮にゲッターの初回呼び出し時に値を初期化したい場合や、セッターにバリデーション処理を加えた場合などはどうなるだろう?
class TopSecret
def earth_shape
@earth_shape ||= 'flat'
end
def earth_shape=(shape)
raise "We can't allow that." if shape == 'round'
@earth_shape = shape
end
def reveal
# `self.earth_shape` を経由しないので、インスタンス変数は初期化されない
puts "The Earth is #{@earth_shape}." # 'The Earth is .' と出力
# `self.earth_shape=` を経由しないので、バリデーションは実行されない
@earth_shape = 'round'
end
end
(もはや attr_accessor
の面影がなくなってしまっているが、実は主役はゲッター・セッターなので気にしないでほしい。)
インスタンス変数に直接アクセスすることによって、ゲッター内の「インスタンス変数の初期化」、セッター内の「値のバリデーションによる例外」といった「副作用」を通り越してしまっている。
ゲッター・セッターが副作用を持っていることが問題なのだろうか、それとも副作用を持っているゲッター・セッターを無視してインスタンス変数にアクセスしていることが問題なのだろうか。
深追いするとなかなか奥が深そうなので、余力があれば機会を改めて考えてみたい。
ひとまずは、 attr_accessor
で事足りないようなゲッター・セッターの場合は、前述の self
キーワードを使う方法に頼らざるを得ないというところか。
まとめ
- インスタンスメソッド内でセッター
earth_shape=
を呼びたい場合、self.earth_shape = 'round'
のようにself
を使わないとローカル変数が作られてしまう -
self
を使いたくない場合、@earth_shape = 'round'
のようにインスタンス変数に直接アクセスすることもできる