コードレビューをしているときに、ん?と思った挙動がありました。サンプルコードです。
class Hoge
attr_accessor :fuga
def piyo
fuga = "overwrite" if fuga.nil?
fuga
end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
この時の hoge.piyo
の結果はどうなると思いますか? まともなRubyistであれば、分かると思いますが僕にとっては予想外の結果でした。
僕はこのコードを最初に見たときにパッと思った答えは fuga
が返ってくるんじゃないかと思いました。が、実際の答えは overwrite
です。なぜでしょうか。その理由を探るためにRubyの仕様を確認してみると、とても面白かったのでまとめてみました。
(注) attr_accessor
で定義したインスタンス変数にアクセスするのに @
がないのがそもそもオカシイというツッコミは一旦置いといてもらえると助かります 🙇
Rubyはレシーバが省略できる
まずRubyはレシーバが省略できます。下記のコードのように self.fuga
と書かなくても現在のレシーバ self
を参照してくれます。
class Hoge
def piyo
fuga
end
def fuga
p "fuga"
end
end
しかし、同名のローカル変数がある場合はそちらを優先します。メソッド内に fuga
という変数がないかを確認し、なければ同名のメソッドを探しに行きます。
class Hoge
def piyo
fuga = "fuga"
fuga #=> ローカル変数の fuga
end
def fuga
p "fuga"
end
end
このことは object_id
を確認すると、よりわかります。
class Hoge
def piyo
p fuga.object_id #=> 8
fuga = "fuga"
p fuga.object_id #=> 70220665709200
end
def fuga
end
end
Hoge.new.piyo
privateメソッドはレシーバを指定できない
privateメソッドの場合はレシーバを指定できません。つまり、常に省略した形の記法になります。
class Hoge
def piyo
fuga
end
private
def fuga
"private fuga"
end
end
self.fuga
とレシーバを明示して呼ぶと NoMethodError: private method ``fuga' called for #<Hoge:0x007f9f3c0538a8>
エラーになります。これはよくよく考えると当然の話しでレシーバを指定できてしまうと他のオブジェクトから呼び出せることになってしまうからです。self
に対してのみ呼び出せないと駄目なのです。
privateメソッドと同名のローカル変数
privateメソッドと同名のローカル変数を宣言した場合はどうなるかというと前述した通りローカル変数が優先されます。
class Hoge
def piyo
fuga = "public fuga"
fuga
end
private
def fuga
"private fuga"
end
end
これは "public fuga"
が返って来ます。
ローカル変数の宣言
Rubyでは実行されていなくても宣言とみなされる仕様があります。以下のコードは一見 fuga
がNameErrorになりそうなきがしますが、nil
が返って来ます。
class Hoge
def piyo
if false
fuga = "fuga"
end
fuga
end
end
Hoge.new.piyo
# => nil
これは不思議に感じますが前述したレシーバを省略して書けるというのを考えると分かります。fuga
と書いた場合、それがローカル変数なのかメソッド呼び出しなのかが分かりにくいですが、変数代入のコードが書いてあるとローカル変数を参照するというのが判断できるため、デフォルト値として nil
を返す挙動になっています。実際に代入されなくても初期化式さえあればOKです。
Ruby は最初にスクリプト全体をコンパイルしてローカル変数を決定します。
https://docs.ruby-lang.org/ja/latest/library/irb.html
さすがに初期化子式が出てくる前に参照するとエラーになります。
class Hoge
def piyo
fuga #=> NameError: undefined local variable or method `fuga'
if false
fuga = "fuga"
end
end
end
ちなみに、インスタンス変数は @
が付いているため初期化式がなくてもデフォルトの nil
が返って来ます。
def hoge
@piyo
end
hoge
# => nil
attr_accessorの実態
attr_accessor
は単純にゲッターセッターメソッドを作っているだけです。以下の2つは同じことです。
class Hoge
attr_accessor :fuga
end
class Hoge
def fuga
@fuga
end
def fuga=(fuga)
@fuga = fuga
end
end
ちなみに def fuga=(fuga)
というのがちょっと見慣れないメソッド定義の書き方だと僕は思いました。が、単純に属性への代入を、変数への代入と同じように書けるようにするシンタックスシュガーということでした。
attr_accessorをメタプロで
実際の attr_accessor
はCで実装されていますが、それをメタプロで表現しようとすると以下のような感じになります。こんな事ができるRubyはカッコいいですね。
def accessor(sym)
setter(sym)
getter(sym)
end
def setter(sym)
define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end
end
def getter(sym)
define_method(sym) do
instance_variable_get("@#{sym}")
end
end
class Hoge
accessor :piyo
end
hoge = Hoge.new
hoge.piyo = "piyo"
hoge.piyo #=> piyo
Class内に任意の式を書ける
前述の attr_accessor
をメタプロで書いた際に Hogeクラス
の直下にメソッド呼び出しをしていますが、これがまたRubyらしい面白いところで、Rubyではクラス定義の中では任意の式が書けます。これがあることでメタプロがよりメタプロっぽく感じますね。
class Hoge
p "This is Hoge class"
10.times {|i| p i }
end
まとめ
色々と寄り道しましたが、上記のことを簡単にまとめると以下の2点になります。
- レシーバ省略した同名のローカル変数とメソッド呼び出しは、ローカル変数が優先される
- ローカル変数の初期化は、初期化式さえあればデフォルトの
nil
で宣言される
これらの仕様を照らし合わせると最初のコードがエラーになるのがわかります。
class Hoge
attr_accessor :fuga
def piyo
fuga = "overwrite" if fuga.nil?
fuga
end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
僕が考えた処理の流れは以下のとおりです。間違っている可能性があるので、ツッコミをお待ちしております 🙇
-
fuga = "overwrite"
があるな… - 代入メソッドの
def fuga=(fuga)
があるけど、ローカル変数の方が優先度高いな -
fuga = "overwrite"
が初期化式だからfuga
をnil
で初期化しよう -
nil.nil?
はtrue
だからfuga
にoverwrite
代入しよう
hoge
オブジェクトの中身を見てみると fuga
がインスタンス変数ではなくローカル変数を参照しているのがわかります。
hoge
# => #<Hoge:0x007fc0511a5db0 @fuga="fuga">
正しい書き方の前に、以下のコードでは何が返ってくるか考えてみると、より分かります。違いは単純にifの条件が false
固定になっていることです。
class Hoge
attr_accessor :fuga
def piyo
fuga = "overwrite" if false
fuga
end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
hoge.piyo
の返り値は nil
になります。流れは同じですね。
ではどのように書くのが正しいのかというと以下のようにインスタンス変数の参照としてちゃんと @
を付けるという超基本的な事になります。
class Hoge
attr_accessor :fuga
def piyo
@fuga = "overwrite" if @fuga.nil?
@fuga
end
end
hoge = Hoge.new
hoge.fuga = "fuga"
hoge.piyo
めでたく fuga
が返ってきました。ありがとうございます。Rubyって面白くて楽しいですね。