Rails miharu

Railsことはじめの思い出〜Modelを見て「フィールド宣言とメモリ展開どうなってんの??」と首を傾げた話〜

元々はJavaを中心に活動していた者が、Ruby on Railsの世界に飛び込んだ際の思い出話です。

Rubyの触り始めで一番慣れなかったのは「変数の事前宣言(型指定)がいらない」ことでした。

Javaで慣れ親しんだ String name;int age; といった事前のフィールド宣言がなく、Rubyでは name = "名前" と代入した瞬間にローカル変数が誕生します。

なかなか戸惑いつつも、RailsのModel(ActiveRecord)を触り始めたとき、頭の中に強烈な違和感が生まれました。

「あれ? RailsのModelって、代入される前から『変数が宣言』されてない???」

「インスタンス化したときにメモリ上に何が展開されてる??」

そんな違和感を解消した話と、その裏側にある「動的メソッド生成」「メモリ展開の仕組み」について書きたいと思います。

「空っぽのクラス」が動く?

例として、データベースに users テーブル(nameage カラムを持つ)があり、それに対応する Users Modelを作ったとします。

ファイル app/models/user.rb の中身はこれだけです。

class User < ApplicationRecord
end

……空っぽです。 フィールドも、メソッドも、何もありません。

が、Railsコンソールでこんなコードを書くと、当たり前のように動きます。

user = User.new
user.name = "名前" # ← なぜか代入できる
puts user.name # ← "名前" が出力される

???????????

(Rubyって宣言と定義が同時におこなわれると思っていたが誤り??クラス展開した時点で変数が宣言されたってコト?????)

???????????

これ、最初はかなり混乱しました。
Modelの挙動をみていると、最初からnameという変数がクラスに「宣言」されていて、そこに値を代入しているように見えたのです。

変数だと思った? 残念! メソッドでした!

  はい。

確認したところ、私が変数への代入だと思った user.name = “名前”変数への直接代入ではございませんでした。

Rubyにおいて obj.hoge = val という書き方は、hoge=という名前のメソッド呼び出し(シンタックスシュガー)となっております。

# 変数への代入に見えるが…
user.name = "名前"

# 実際は `name=` メソッドの呼び出し!
user.name=("名前")

「変数が事前宣言されている」と錯覚していたものの正体は、Railsが実行時に動的に生成した「メソッド」だったのでした。

Railsはアプリケーション起動時やクラスロード時にデータベースのスキーマ情報を読み込み、メタプログラミングによって name(getter)や name=(setter)といったメソッドをクラスの「メソッドテーブル」に直接叩き込んでいるのでした。

メモリ上では何が起きているのか???(Java vs Rails)

ここで気になったのが、「インスタンス化(User.new)したとき、メモリ上には何が展開されているのか?」 という点です。

Javaの場合:厳格な固定レイアウト

Javaで new User() をすると、ヒープメモリ上にはクラスの型定義に基づいた固定サイズの領域が確保されます。

[Javaのメモリイメージ]
User Object
├── String name (参照ポインタ用領域: 8byte)
└── int age (値用領域: 4byte)

Javaでは「どのフィールドがどこにあるか」がコンパイル時に確定しているため、メモリのオフセット(配置場所)が決まっています。(だからこそ事前宣言が必要だったんですね)

Rails(ActiveRecord)の場合:ハッシュによる動的管理

Railsで user = User.new を実行するケースです。メモリ上には nameage という個別のインスタンス変数は展開されません

実体としてインスタンス内に保持されるのは、内部データ(属性)を管理するための単一のハッシュ構造(@attributes)です。

[Railsのメモリイメージ]
User Instance
└── @attributes = {
"name" => #<ActiveModel::Attribute ... value: "名前">,
"age" => #<ActiveModel::Attribute ... value: nil>
}

インスタンスに対して、user.name = “名前” を実行したとき、以下のような振る舞いとなっています。

  1. name= メソッドが呼ばれる
  2. メソッド内部で、インスタンスが持つ内部ハッシュ(@attributes)を検索する
  3. ハッシュ内の "name" キーに対応するオブジェクトの値を "名前" に書き換える

つまり、Javaのように「メモリ上の固定のフィールド領域に値を書き込む」のではなく、「内部で持っているハッシュ(@attributes)のキーと値を動的に書き換えている」 だけであり、「変数が宣言されている」のではなく、「単なるハッシュの操作を、ドット記法のメソッド呼び出しで見せかけていただけ」 というのが真相でした。

おわりに

JavaからRuby on Railsへ移行したエンジニアにとって、ActiveRecordの挙動は一種のデカルチャーです。
ここまでに記載した内容を認識するまでは違和感しかありませんでしたが、
動的メソッドと内部ハッシュのメモリ展開を理解して、しっくり受け入れることができたと思います。

静的型付け言語の「メモリ空間の常識」をアンラーンして覗くRubyの世界には、メタプログラミングと柔軟性に満ちた面白さがありました!