Railsのselfとインスタンス変数について
1. はじめに
こんにちは。アプレンティス2期生のsakanaです。
現在、Railsチュートリアルを学習中なのですが、9章の勉強中にselfやインスタンス変数周りでだいぶ混乱したので、何を疑問に思って、どう解決していったのかを記事としてまとめていきます。
Rails7 (第7版) 第9章 発展的なログイン機構にでてくるコードを参照しつつ、説明してきます。
self.remember_token
って何?
2. 疑問: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を省略して書いてあるということです。
@remember_token
でも良いのでは?
3. 疑問:ここまでの話で、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
と書いても問題ないことが確認できました。
@remember_digest
でも良いのでは?
4. 疑問:今度はリスト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インスタンスの正体を考える必要があります。
User.new
で生成されたuserインスタンスの正体
4.2. User.newによって生成されるインスタンスについて思い出してみましょう。
nameとかemailという属性が付与されるということは、
class User
attr_accessor :name, :email
end
ということでしょうか。これなら確かにuser.name
やuser.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を使うとどちらも同じように扱うことができて便利
@user
とか@current_user
という存在
6. おまけ:ふと、コントローラやビューに登場する@userや@current_userってなんだったんだろうとなるかもしれません。
これについてRailsチュートリアル2章で触れられているので、引用してみます。
@
記号で始まる変数をRubyではインスタンス変数と呼び、Railsのコントローラ内で宣言したインスタンス変数はビューでも使えるようになります。
つまり、コントローラで定義した変数をビューで使うためにインスタンス変数として定義しているわけです。
インスタンス変数は、同じクラス内からなら読み込みができるという性質があります。
Railsくんが裏でなんやかんや処理してくれてるおかげで、クラスの異なるビューからコントローラのインスタンス変数が参照できるようになっているのでしょう。
というわけで、この話は先ほどまでしていたUser.newで生成されたuserインスタンスの属性に関する話とは、またレイヤーが異なります。(そして、それを明確に区別するためにも、userインスタンスの属性についてはselfを使って表した方が見通しが良くなるのでしょう)
7. 最後に
以前Railsチュートリアルを勉強したときは、ここのself周りの話は全然わからないまま進めてしまいました。しかし、Rubyの勉強をある程度したことで、理解のとっかかりをつかむことができました。少しは成長しているのかなと感じることができて嬉しいです。
そして、この記事が少しでもRailsチュートリアルを学習中の方の助けとなれば幸いです。
Discussion