この記事は僕が以前書いた「(あなたの周りでも見かけるかもしれない)インスタンス変数の間違った使い方」という記事の改訂版(というか、全面書き直し版)です。
はじめに:そのインスタンス変数、本当に必要ですか?
僕はフィヨルドブートキャンプでメンターをやっています。そこで提出物のコードレビューをしていると、間違ったインスタンス変数の使い方をよく見かけます。例を挙げるとこんな感じです(説明用のサンプルコードなので、処理自体に意味はありません)。
def run
collect_data
display_data
end
def collect_data
# インスタンス変数にデータを詰める
@data = ['a', 'b', 'c']
end
def display_data
# インスタンス変数を読み取って画面に出力する
@data.each do |str|
puts str.upcase
end
end
run
こういったコードを書く生徒さんはメソッドの引数と戻り値をうまく使いこなせていない印象を受けます。メソッドAとメソッドBでデータをやりとりするには、インスタンス変数を使うしかない(またはその方がラク)と思い込んでるのではないでしょうか。
しかし、こうしたコードは規模が小さいうちはまだ良いですが、業務レベルの大きくて複雑なプログラムになると、一気に可読性と保守性を悪化させる原因になります。なぜなら、インスタンス変数を使うと変数のスコープ(変数の寿命)が不必要に大きくなってしまうためです。
解決策:処理の過程で使う一時的なデータは、戻り値と引数を使おう
上のコードはインスタンス変数を使うのではなく、次のように戻り値とローカル変数と引数を使う方がベターです。
def run
# メソッドの戻り値をローカル変数で受け取る
data = collect_data
# メソッドの引数としてデータを渡す
display_data(data)
end
def collect_data
# メソッドの戻り値としてデータを返す
['a', 'b', 'c']
end
def display_data(data)
# 引数としてデータを受け取り、そのデータを画面に出力する
data.each do |str|
puts str.upcase
end
end
run
引数や戻り値、ローカル変数を使うのであれば、対象となるデータのスコープを過度に広げてしまうことはありません。そのため、可読性や保守性もよくなります。
なお、学習用に作るプログラムと、業務レベルの大きくて複雑なプログラムの違いについては以前こちらの記事にまとめたので、まだ読んでない方はこちらも参考にどうぞ。
たとえ話:その荷物、コインロッカーで渡すか?手渡しで渡すか?
僕はインスタンス変数を使ってデータをやりとりするプログラムを見ると、コインロッカーの画像が頭に思い浮かびます。
最初に見せたコード例で言えば、
collect_data
くんが「一番左上のロッカーにデータを入れといたよ!」
と言い、
display_data
くんが「OK、一番左上ね!うん、ちゃんとあった!」
とやりとりしながらデータ(荷物)を受け渡ししているイメージです。
ただ、これだと先ほども述べたようにプログラムが大きくなってくると破綻します。なぜなら、ロッカーに入れるデータが無数に増えて、
「ちょっと、このロッカーのデータを書き換えたのはいったいどこの誰!?」
「あのデータが欲しいんだけど、どのロッカーに入ってるの!?」
「ロッカーを開けたら中が空っぽ(nil
)なんだけど、なんで!?」
「このロッカーの中身、使ってなさそうだから捨てたい(変数を削除したい)んだけど、本当に捨てても大丈夫!?」
みたいなことが多発するからです。
メソッドの引数や戻り値を使うプログラムは「手渡し」のイメージ
一方、メソッドの引数や戻り値を使ってデータをやりとりするプログラムは、荷物を手渡しするイメージです。
先ほど載せた改善後のコード例で言えば、
collect_data
くんが「作ったデータはこれですよ。はい!」
と、戻り値としてデータ(荷物)を返し、それをいったん変数に入れてから、
display_data
くんに「これが表示してほしいデータなんで、あとは任せた!」
と手渡しする感じですね。
こうすれば荷物が突然行方不明になったり、差出人不明(=誰がいつどうやって作ったのかわからない)の荷物が生まれたりしにくくなります。少なくとも、メソッドの呼び出し履歴をさかのぼっていけば、誰がいつどこで作ったデータなのかは突き止められるはずです。
「じゃあインスタンス変数はいつ使えばいいの?」
こんなふうに説明すると、初心者のみなさんは「じゃあインスタンス変数はいつ使えばいいの?」と思うかもしれません。
インスタンス変数はオブジェクト指向プログラミングの文脈で、クラスを定義するときに使います。たとえば、Person(人間)クラスに名前と血液型を保持させたいときは、名前と血液型をインスタンス変数に保存します。
class Person
attr_reader :name, :blood_type
def initialize(name, blood_type)
# インスタンス変数に名前と血液型を保存する
@name = name
@blood_type = blood_type
end
end
ito = Person.new('いとう', 'A')
sato = Person.new('さとう', 'AB')
ito.name #=> "いとう"
ito.blood_type #=> "A"
sato.name #=> "さとう"
sato.blood_type #=> "AB"
基本的な考え方として、インスタンス変数はインスタンスが作成されてから(newされてから)最後までほとんど変わらない値だと思ってください。たとえば上の例であれば、ito
さんの名前と血液型は「いとう」と「A」のまま、最後まで変わることはありません。
インスタンス変数は「変数」という名前が付いていますが、処理の最中にコロコロ変わる値ではなく、異なるインスタンスが別個に保持するデータを格納するための変数だと考えるのが良いでしょう。たとえば上の例だと、ito
さんとsato
さんという異なるインスタンスが、それぞれに名前と血液型を別個に保持しています。
少なくとも、自分で独自のクラスを定義していないのであれば、基本的にインスタンス変数の出番は無いものだと考えてください。
なお、ここではかなり単純化した例でインスタンス変数の適切な使い方を説明しましたが、クラスとインスタンス変数の関係はオブジェクト指向プログラミングの話題になるため、より踏み込んだ話はオブジェクト指向プログラミングの専門書を参照してください。
おまけ:インスタンス変数をグローバル変数で置き換えてみる
インスタンス変数の使い方が正しいかどうかは、自分のコードに書かれているインスタンス変数をグローバル変数に置き換えると判断しやすいかもしれません。
- グローバル変数に置き換えても挙動が変わらない → インスタンス変数の使い方がおかしい
- グローバル変数に置き換えると挙動が変わる → インスタンス変数の使い方が正しい
具体的にやってみるとこんな感じです。
インスタンス変数の使い方がおかしい場合
以下のコードはインスタンス変数でもグローバル変数でも挙動が変わりません。
def run
collect_data
display_data
end
def collect_data
$data = ['a', 'b', 'c']
end
def display_data
$data.each do |str|
puts str.upcase
end
end
run
# この結果はインスタンス変数を使ったときと同じ
$ ruby sample.rb
A
B
C
インスタンス変数の使い方が正しい場合
以下のコードはグローバル変数に置き換えると挙動が変わります。
class Person
attr_reader :name, :blood_type
def initialize(name, blood_type)
$name = name
$blood_type = blood_type
end
def name
$name
end
def blood_type
$blood_type
end
end
ito = Person.new('いとう', 'A')
sato = Person.new('さとう', 'AB')
# いとうさんがさとうさんになっちゃった!
puts ito.name #=> "さとう"
puts ito.blood_type #=> "AB"
puts sato.name #=> "さとう"
puts sato.blood_type #=> "AB"
上の結果からわかること
インスタンス変数はインスタンスごとに保持される変数なので、インスタンスごとに別々のデータを保持しなければなりません。これをグローバル変数に置き換えると、どのインスタンスも同じデータを読み書きするので挙動が変わってしまいます。
一方、インスタンスごとに保持する必然性がないデータをインスタンス変数に保持している場合は、単に「どこからでもアクセスできる便利な変数」が欲しいだけなので、グローバル変数に置き換えても何も問題ありません。ですが、「どこからでもアクセスできる便利な変数」は大きなプログラムになると混沌を招く原因になるので、原則NGです。
まとめ
というわけで、この記事ではプログラミング初心者の人がやりがちなインスタンス変数の間違った使い方と、その解決策について解説してみました。
メソッド同士のデータのやりとりはインスタンス変数を使えばOK!と思っていた人は、この記事を読んで自分のインスタンス変数の使い方を見直してみてください。