💬

Railsのselfとインスタンス変数について

2023/12/14に公開

1. はじめに

こんにちは。アプレンティス2期生のsakanaです。

現在、Railsチュートリアルを学習中なのですが、9章の勉強中にselfやインスタンス変数周りでだいぶ混乱したので、何を疑問に思って、どう解決していったのかを記事としてまとめていきます。

Rails7 (第7版) 第9章 発展的なログイン機構にでてくるコードを参照しつつ、説明してきます。

2. 疑問:self.remember_tokenって何?

8章までのコードでは、ブラウザを終了したときにログイン状態が必ず切れてしまう状態になっていたので、9章ではログインにremember_me機能を追加していきます。

そして、リスト9.3ではrememberメソッドをUserモデルに追加しています。

class User < ApplicationRecord
  attr_accessor :remember_token

  (中略)

  # 永続的セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

ここで、このコードに関する説明の一部分を引用します

rememberメソッドの1行目の代入にご注目ください。selfというキーワードを使わないと、Rubyによってremember_tokenという名前のローカル変数が作成されてしまいます。

即ち、remember_tokenという名前のインスタンス変数を作成したいからselfをつけたいということです。それを意味していることは、attr_accessorとして:remember_tokenを指定していることからも明らかです。

2.1. attr_accessorとは

attr_accessorが何者かを知らないと前に進まないので、それについて説明しておきましょう。

インスタンス変数は通常、クラスの外からは変数への代入や、変数の中身を表示することができません。そのためには、専用のメソッドを書く必要があります。

class Student
  def set_name=(val)
    @name = val
  end

  def display_name
    @name
  end
end

student = Student.new
student.set_name = "sakana"
puts student.display_name # output: sakana

メソッド名は、インスタンス変数の名前と同じでも構わないので、このコードは次のように書くこともできます。

class Student
  def name=(val)
    @name = val
  end

  def name
    @name
  end
end

student = Student.new
student.name = "sakana"
puts student.name # output: sakana

そして、こういった使い方をよくすることから、Rubyでは、attr_writer, attr_reader, attr_accessorといったアクセサが用意されています。

attr_writerが指定した変数と同じ名前のセッターメソッド(値を代入するメソッド)を作成してくれるアクセサ。

attr_readerが指定した変数と同じ名前のゲッターメソッド(値を取得するメソッド)を作成してくれるアクセサ。

attr_accessorがその両方を兼ね備えたアクセサです。

そのため、上のコードは、次のように書き直せます。

class Student
  attr_accessor :name
end

student = Student.new
student.name = "sakana"
puts student.name # output: sakana

というわけなので、インスタンス変数@nameへの代入や表示を行うときの.nameというのは、実際はメソッド名を表していて、結果的にインスタンス変数@nameへの代入や表示ができているということになります。

2.2. メソッド内のselfの働き

selfは、宣言される場所で表す意味が異なります。

今回の場合はクラスのメソッド内で宣言されていますね。この場合は、インスタンス自身を指すことになるようです。

class Student
  attr_accessor :name

  def set_name_fish
    self.name = "fish"
  end

  def display_multi_name
    "#{self.name}es"
  end
end

student = Student.new
student.set_name_fish
puts student.display_multi_name # output: fishes

当然ながら、次のように書くこともできます。

class Student
  attr_accessor :name

  def set_name_fish
    @name = "fish"
  end

  def display_multi_name
    "#{@name}es"
  end
end

student = Student.new
student.set_name_fish
puts student.display_multi_name # output: fishes

2.3. selfの省略について

メソッド内のselfについては、省略できるときは省略するのが慣例のようです。

そして、メソッド内でのselfはほとんど省略できますが、唯一省略できないのがセッターメソッドの時です。

だから、リスト9.3ではrememberメソッド内1行目の

self.remember_token = User.new_token

については、remember_tokenというのがセッターメソッドだからselfを省略できずに書いてあるし、2行目の

update_attribute(:remember_digest, User.digest(remember_token))

については、remembre_tokenというのがゲッターメソッドだから、selfを省略して書いてあるということです。

3. 疑問:@remember_tokenでも良いのでは?

ここまでの話で、remember_tokenがインスタンス変数であることがわかりました。

Studentクラスのnameの例で書いたように、それなら@remember_tokenと書いた方がわかりやすくないか?と過去の自分は考えたわけです。

そして、そのように書くことは実際可能でした。

3.1. 使用可能な記法

というわけで、rememberメソッド内にdebuggerを仕込んで、メソッド内で使える記法を調べてみた結果がこちらです。

セッターメソッドのとき:

記法 可/不可
self.remember_token
remember_token ×
@remember_token

ゲッターメソッドのとき:

記法 可/不可
self.remember_token
remember_token
@remember_token

よって、@remember_tokenと書いても問題ないことが確認できました。

4. 疑問:@remember_digestでも良いのでは?

今度はリスト9.6で、authenticated?メソッドが追加されました。

class User < ApplicationRecord

  (中略)

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

ここで登場するremember_digestはDBに保存されるかどうかが異なるだけで、本質的にはremember_tokenとなんら変わらないと思いました。

なので、remember_tokenと同様に@remember_digestと書けるのではないかと思ったわけです。

4.1. 使用可能な記法

それでは、先ほどと同様にしてメソッド内で使える記法を調べてみた結果がこちらです。

セッターメソッドのとき:

記法 可/不可
self.remember_digest
remember_digest ×
@remember_digest ×
self[:remember_digest]
:remember_digest ×

ゲッターメソッドのとき:

記法 可/不可
self.remember_digest
remember_digest
@remember_digest ×
self[:remember_digest]
:remember_digest ×

なんと、@remember_digestは使えませんでした。

先ほどはなかった後ろ2つの記法は一旦おいておいて、この謎を解明するには、まずはuserインスタンスの正体を考える必要があります。

4.2. User.newで生成されたuserインスタンスの正体

User.newによって生成されるインスタンスについて思い出してみましょう。

nameとかemailという属性が付与されるということは、

class User
  attr_accessor :name, :email
end

ということでしょうか。これなら確かにuser.nameuser.emailという書き方ができるはずです。

しかし、実はuserインスタンスは、user[:name]user[:email]といった記法を使うことも可能です(rails consoleから簡単に確認できます)。

すなわち、userインスタンスはハッシュだった!ということです。そして、似たような構造は以下のコードで再現できます。

class User < Hash
  def name=(val)
    self[:name] = val
  end  
  
  def name
    self[:name]
  end
  
  def password=(val)
    self[:password] = val
  end
  
  def password
    self[:password]
  end
end

user = User.new
puts user[:name].inspect # output: {}
user[:name] = "sakana"
puts user[:name] # output: sakana

puts user[:password].inspect # output: {}
user[:password] = "foobar"
puts user[:password] # output: foobar

user.name = "aaa"
puts user.name # output: aaa

user.password = "bazbaz"
puts user.password # output: bazbaz

例えばこのように定義すると、user[:name]としても、user.nameとしても扱うことができます。

そして、attr_accessorのときはメソッド名がそのままインスタンス変数名になっていましたが、userインスタンスはハッシュとして保存されていることを忘れてはいけません。それがインスタンス変数を用いて表すことができるかの違いにつながっているのです。

ただし、これはあくまで簡単な例にすぎません。実際は、こういったハッシュの書き方が可能な上で、ここに@remember_tokenのようなインスタンス変数を設定できなければなりませんが、上記のコードではそこまでカバーすることはできていません。

ハッシュとしての値をもったインスタンスとしてuserインスタンスを作れれば一番わかりやすかったのですが、その方法はよくわかりませんでした。

4.3. だからRailsでは、selfを使っている

ここで少し振り返ってみると、remember_tokenとremember_digestで、共通して使えた記法がありました。それがselfを使った記法です。

これなら、インスタンス変数とかハッシュとかを気にせずに同じように扱うことができて便利です。

Railsチュートリアルでは、きっとこのためにselfを使った記法を多用しているのでしょう。

5. まとめ

  • クラスのメソッド内のselfは、インスタンス自身を表す
  • メソッド内のselfは、セッターメソッド以外のときは省略可能
  • User.newで作成されたuserインスタンスの属性や仮属性にアクセスしたいときは、selfを使うとどちらも同じように扱うことができて便利

6. おまけ:@userとか@current_userという存在

ふと、コントローラやビューに登場する@userや@current_userってなんだったんだろうとなるかもしれません。

これについてRailsチュートリアル2章で触れられているので、引用してみます。

@記号で始まる変数をRubyではインスタンス変数と呼び、Railsのコントローラ内で宣言したインスタンス変数はビューでも使えるようになります。

つまり、コントローラで定義した変数をビューで使うためにインスタンス変数として定義しているわけです。

インスタンス変数は、同じクラス内からなら読み込みができるという性質があります。

Railsくんが裏でなんやかんや処理してくれてるおかげで、クラスの異なるビューからコントローラのインスタンス変数が参照できるようになっているのでしょう。

というわけで、この話は先ほどまでしていたUser.newで生成されたuserインスタンスの属性に関する話とは、またレイヤーが異なります。(そして、それを明確に区別するためにも、userインスタンスの属性についてはselfを使って表した方が見通しが良くなるのでしょう)

7. 最後に

以前Railsチュートリアルを勉強したときは、ここのself周りの話は全然わからないまま進めてしまいました。しかし、Rubyの勉強をある程度したことで、理解のとっかかりをつかむことができました。少しは成長しているのかなと感じることができて嬉しいです。

そして、この記事が少しでもRailsチュートリアルを学習中の方の助けとなれば幸いです。

参考記事

Discussion