LoginSignup
33
29

More than 5 years have passed since last update.

Ruby初心者が変数宣言のタイミング、レシーバを省略した時の挙動にハマったので仕様を確認したら面白かった

Posted at

コードレビューをしているときに、ん?と思った挙動がありました。サンプルコードです。

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 methodfuga' 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" が初期化式だから fuganil で初期化しよう
  • nil.nil?true だから fugaoverwrite 代入しよう

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って面白くて楽しいですね。

33
29
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
29