Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
55
Help us understand the problem. What are the problem?

posted at

updated at

Organization

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

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

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

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

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')
ito.name       #=> "いとう"
ito.blood_type #=> "A"

sato = Person.new('さとう', 'AB')
sato.name       #=> "さとう"
sato.blood_type #=> "AB"

基本的な考え方として、インスタンス変数はインスタンスが作成されてから(newされてから)最後までほとんど変わらない値だと思ってください。たとえば上の例であれば、itoさんの名前と血液型は「いとう」と「A」のまま、最後まで変わることはありません。

インスタンス変数は「変数」という名前が付いていますが、処理の最中にコロコロ変わる値ではなく、異なるインスタンスが別個に保持するデータを格納するための変数だと考えるのが良いでしょう。たとえば上の例だと、itoさんとsatoさんという異なるインスタンスが、それぞれに名前と血液型を別個に保持しています。

少なくとも、自分で独自のクラスを定義していないのであれば、基本的にインスタンス変数の出番は無いものだと考えてください。

なお、ここではかなり単純化した例でインスタンス変数の適切な使い方を説明しましたが、クラスとインスタンス変数の関係はオブジェクト指向プログラミングの話題になるため、より踏み込んだ話はオブジェクト指向プログラミングの専門書を参照してください。

まとめ

というわけで、この記事ではプログラミング初心者の人がやりがちなインスタンス変数の間違った使い方と、その解決策について解説してみました。

メソッド同士のデータのやりとりはインスタンス変数を使えばOK!と思っていた人は、この記事を読んで自分のインスタンス変数の使い方を見直してみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
55
Help us understand the problem. What are the problem?