106
82

More than 1 year has passed since last update.

改訂版・(あなたの周りでも見かけるかもしれない)インスタンス変数の間違った使い方

Last updated at Posted at 2021-08-28

この記事は僕が以前書いた「(あなたの周りでも見かけるかもしれない)インスタンス変数の間違った使い方」という記事の改訂版(というか、全面書き直し版)です。

はじめに:そのインスタンス変数、本当に必要ですか?

僕はフィヨルドブートキャンプでメンターをやっています。そこで提出物のコードレビューをしていると、間違ったインスタンス変数の使い方をよく見かけます。例を挙げるとこんな感じです(説明用のサンプルコードなので、処理自体に意味はありません)。

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

引数や戻り値、ローカル変数を使うのであれば、対象となるデータのスコープを過度に広げてしまうことはありません。そのため、可読性や保守性もよくなります。

なお、学習用に作るプログラムと、業務レベルの大きくて複雑なプログラムの違いについては以前こちらの記事にまとめたので、まだ読んでない方はこちらも参考にどうぞ。

たとえ話:その荷物、コインロッカーで渡すか?手渡しで渡すか?

僕はインスタンス変数を使ってデータをやりとりするプログラムを見ると、コインロッカーの画像が頭に思い浮かびます。

coin_locker_big.png

最初に見せたコード例で言えば、

collect_dataくんが「一番左上のロッカーにデータを入れといたよ!」

と言い、

display_dataくんが「OK、一番左上ね!うん、ちゃんとあった!」

とやりとりしながらデータ(荷物)を受け渡ししているイメージです。

ただ、これだと先ほども述べたようにプログラムが大きくなってくると破綻します。なぜなら、ロッカーに入れるデータが無数に増えて、

「ちょっと、このロッカーのデータを書き換えたのはいったいどこの誰!?」

「あのデータが欲しいんだけど、どのロッカーに入ってるの!?」

「ロッカーを開けたら中が空っぽ(nil)なんだけど、なんで!?」

「このロッカーの中身、使ってなさそうだから捨てたい(変数を削除したい)んだけど、本当に捨てても大丈夫!?」

みたいなことが多発するからです。

メソッドの引数や戻り値を使うプログラムは「手渡し」のイメージ

一方、メソッドの引数や戻り値を使ってデータをやりとりするプログラムは、荷物を手渡しするイメージです。

bucket_relay_nimotsu.png

先ほど載せた改善後のコード例で言えば、

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!と思っていた人は、この記事を読んで自分のインスタンス変数の使い方を見直してみてください。

106
82
1

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
106
82