23

私は高校で最初のプログラミングクラスにいます。私たちは、最初の学期のプロジェクトの終わりをやっています。

このプロジェクトには1つのクラスのみが含まれますが、多くのメソッドが含まれます。私の質問は、インスタンス変数とローカル変数のベストプラクティスについてです。ほとんどインスタンス変数のみを使用してコーディングする方がはるかに簡単なようです。しかし、これが私がそれを行うべき方法なのか、それともローカル変数をもっと使うべきなのかはわかりません(メソッドにローカル変数の値をもっとたくさん取り入れなければならないだけです)。

これについての私の推論は、多くの場合、メソッドに2つまたは3つの値を返させたいためですが、これはもちろん不可能です。したがって、インスタンス変数を使用する方が簡単で、クラス内で普遍的であるため、心配する必要はありません。

4

10 に答える 10

33

誰もこれについて話し合っているのを見たことがないので、もっと考えてみます。短い答え/アドバイスは、値を返すのが簡単だと思うという理由だけで、ローカル変数よりもインスタンス変数を使用しないことです。ローカル変数とインスタンス変数を適切に使用しないと、コードの操作が非常に難しくなります。追跡が非常に困難な重大なバグがいくつか発生します。重大なバグの意味と、それがどのように見えるかを理解したい場合は、読み進めてください。

関数への書き込みを提案するように、インスタンス変数のみを使用してみましょう。非常に単純なクラスを作成します。

public class BadIdea {
   public Enum Color { GREEN, RED, BLUE, PURPLE };
   public Color[] map = new Colors[] { 
        Color.GREEN, 
        Color.GREEN, 
        Color.RED, 
        Color.BLUE, 
        Color.PURPLE,
        Color.RED,
        Color.PURPLE };

   List<Integer> indexes = new ArrayList<Integer>();
   public int counter = 0;
   public int index = 0;

   public void findColor( Color value ) {
      indexes.clear();
      for( index = 0; index < map.length; index++ ) {
         if( map[index] == value ) {
            indexes.add( index );
            counter++;
         }
      }
   }

   public void findOppositeColors( Color value ) {
      indexes.clear();
      for( index = 0; i < index < map.length; index++ ) {
         if( map[index] != value ) {
            indexes.add( index );
            counter++;
         }
      }
   }
}

これは私が知っているばかげたプログラムですが、これを使用して、このようなことにインスタンス変数を使用することは非常に悪い考えであるという概念を説明することができます。最も重要なことは、これらのメソッドがすべてのインスタンス変数を使用していることです。そして、呼び出されるたびに、インデックス、カウンター、およびインデックスを変更します。最初に見つかる問題は、これらのメソッドを次々と呼び出すと、以前の実行からの回答が変更される可能性があることです。たとえば、次のコードを記述したとします。

BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
idea.findColor( Color.GREEN );  // whoops we just lost the results from finding all Color.RED

findColor はインスタンス変数を使用して戻り値を追跡するため、一度に 1 つの結果しか返すことができません。もう一度呼び出す前に、これらの結果への参照を保存してみましょう。

BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findColor( Color.GREEN );  // this causes red positions to be lost! (i.e. idea.indexes.clear()
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;

この 2 番目の例では、3 行目の赤い位置を保存しましたが、同じことが起こりました!?なぜそれらを失ったのでしょうか?! idea.indexes が割り当てられたのではなくクリアされたため、一度に使用できる回答は 1 つだけです。再度呼び出す前に、その結​​果の使用を完全に終了する必要があります。メソッドを再度呼び出すと、結果がクリアされ、すべてが失われます。これを修正するには、毎回新しい結果を割り当てる必要があるため、赤と緑の回答が別々になります。それでは、答えを複製して新しいコピーを作成しましょう。

BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes.clone();
int redCount = idea.counter;
idea.findColor( Color.GREEN );
List<Integer> greenPositions = idea.indexes.clone();
int greenCount = idea.counter;

最後に、2 つの別々の結果が得られました。赤と緑の結果が分離されました。しかし、プログラムが機能する前に、BadIdea が内部でどのように動作しているかについて多くのことを知る必要がありましたね。結果が破壊されないように、呼び出すたびに戻り値を複製することを忘れないでください。発信者がこれらの詳細を覚えておく必要があるのはなぜですか? それをする必要がなければ、もっと簡単ではないでしょうか?

また、呼び出し元はローカル変数を使用して結果を記憶する必要があることにも注意してください。したがって、BadIdea のメソッドでローカル変数を使用しなかった場合でも、呼び出し元はそれらを使用して結果を記憶する必要があります。それで、あなたは本当に何を達成しましたか?あなたは本当に問題を発信者に移し、発信者にもっとや​​らせるように強制しました。また、呼び出し元にプッシュする作業は、規則には多くの例外があるため、従うのが簡単な規則ではありません。

それでは、2 つの異なる方法でそれを実行してみましょう。私がいかに「賢く」、同じインスタンス変数を再利用して「メモリを節約」し、コードをコンパクトに保ったかに注目してください。;-)

BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findOppositeColors( Color.RED );  // this causes red positions to be lost again!!
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;

同じことが起こりました!くそー、でも私はとても「賢く」、メモリを節約していたので、コードはより少ないリソースを使用していました!!! これは、メソッドの呼び出しが順序に依存するように、インスタンス変数を使用することの本当の危険です。メソッド呼び出しの順序を変更すると、BadIdea の基本的な状態を実際に変更していなくても、結果が異なります。マップの内容は変更していません。メソッドを異なる順序で呼び出すと、プログラムの結果が異なるのはなぜですか?

idea.findColor( Color.RED )
idea.findOppositeColors( Color.RED )

これら 2 つの方法を交換した場合とは異なる結果が生成されます。

idea.findOppositeColors( Color.RED )
idea.findColor( Color.RED )

これらのタイプのエラーは、特にこれらの行が隣り合っていない場合、追跡するのが非常に困難です。これらの 2 行の間に新しい呼び出しを追加するだけで、プログラムを完全に壊すことができ、まったく異なる結果が得られます。確かに、処理する行数が少ない場合は、エラーを簡単に見つけることができます。しかし、大規模なプログラムでは、プログラム内のデータが変更されていなくても、それらを再現しようとして何日も無駄にする可能性があります。

そして、これはシングルスレッドの問題だけを見ています。BadIdea がマルチスレッドの状況で使用されていた場合、エラーは非常に奇妙なものになる可能性があります。findColors() と findOppositeColors() が同時に呼び出されるとどうなりますか? 墜落 髪が抜け落ち 死と時空が崩壊して特異点 宇宙が飲み込まれる?おそらく、そのうちの少なくとも2つ。スレッドはおそらくあなたの頭の上にありますが、スレッドにたどり着いたときに、それらの悪い慣行があなたに本当の心痛を引き起こさないように、私たちが今あなたを悪いことから遠ざけることができることを願っています.

メソッドを呼び出すときに注意する必要があることに気付きましたか? それらは相互に上書きし、おそらくランダムにメモリを共有し、外部で機能させるために内部でどのように機能したかの詳細を覚えておく必要があり、呼び出される順序を変更すると、次の行に非常に大きな変化が生じます。シングルスレッドの状況でしか機能しませんでした。このようなことを行うと、触れるたびに崩壊するように見える非常に脆弱なコードが生成されます。私が示したこれらのプラクティスは、コードが脆弱であることに直接貢献しました。

これはカプセル化のように見えるかもしれませんが、それをどのように記述したかという技術的な詳細が呼び出し元に知られている必要があるため、これは正反対です。呼び出し元は、コードを機能させるために非常に特殊な方法でコードを記述する必要があり、コードの技術的な詳細を知らずにそれを行うことはできません。これは、クラスが抽象化/インターフェイスの背後にある技術的な詳細を隠すと想定されているため、リーキーな抽象化と呼ばれることがよくありますが、技術的な詳細が漏れて、呼び出し元に動作の変更を余儀なくさせます。すべてのソリューションにはある程度の漏れがありますが、これらのような上記の手法のいずれかを使用すると、解決しようとしている問題が何であれ、それらを適用するとひどく漏れることが保証されます. それでは、GoodIdea を見てみましょう。

ローカル変数を使って書き直してみましょう:

 public class GoodIdea {
   ...

   public List<Integer> findColor( Color value ) {
      List<Integer> results = new ArrayList<Integer>();
      for( int i = 0; i < map.length; i++ ) {
         if( map[index] == value ) {
            results.add( i );
         }
      }
      return results;
   }

   public List<Integer> findOppositeColors( Color value ) {
      List<Integer> results = new ArrayList<Integer>();
      for( int i = 0; i < map.length; i++ ) {
         if( map[index] != value ) {
            results.add( i );
         }
      }
      return results;
   }
 }

これにより、上で説明したすべての問題が修正されます。カウンターを追跡したり返したりしていないことはわかっていますが、追跡した場合は、新しいクラスを作成して、リストの代わりにそれを返すことができます。複数の結果をすばやく返すために、次のオブジェクトを使用することがあります。

public class Pair<K,T> {
    public K first;
    public T second;

    public Pair( K first, T second ) {
       this.first = first;
       this.second = second;
    }
}

長い答えですが、非常に重要なトピックです。

于 2011-01-03T22:03:26.147 に答える
8

クラスの中心的な概念である場合は、インスタンス変数を使用します。反復、再帰、または何らかの処理を行っている場合は、ローカル変数を使用してください。

同じ場所で 2 つ (またはそれ以上) の変数を使用する必要がある場合は、それらの属性 (およびそれらを設定する適切な手段) を持つ新しいクラスを作成する必要があります。これにより、コードがよりきれいになり、問題について考えるのに役立ちます (各クラスは語彙の新しい用語です)。

1 つの変数がコア概念である場合、クラスにすることができます。たとえば、実世界の識別子: これらは文字列として表すことができますが、多くの場合、それらを独自のオブジェクトにカプセル化すると、突然機能を「引き付け」始めます (検証、他のオブジェクトへの関連付けなど)。

また、(完全に関連しているわけではありませんが) オブジェクトの一貫性もあります。オブジェクトは、その状態が意味のあるものであることを保証できます。1 つのプロパティを設定すると、別のプロパティが変更される場合があります。また、後でプログラムをスレッドセーフに変更するのがはるかに簡単になります (必要な場合)。

于 2009-11-25T01:52:04.707 に答える
3

簡単な方法: 変数を複数のメソッドで共有する必要がある場合はインスタンス変数を使用し、それ以外の場合はローカル変数を使用します。

ただし、できるだけ多くのローカル変数を使用することをお勧めします。なんで?クラスが 1 つしかない単純なプロジェクトの場合、違いはありません。多くのクラスを含むプロジェクトの場合、大きな違いがあります。インスタンス変数は、クラスの状態を示します。クラスのインスタンス変数が多いほど、このクラスが持つことができる状態が多くなり、このクラスが複雑になればなるほど、クラスの保守が難しくなり、プロジェクトでエラーが発生しやすくなります。したがって、クラスの状態をできるだけ単純に保つために、できるだけ多くのローカル変数を使用することをお勧めします。

于 2009-11-25T02:26:14.610 に答える
3

簡単な話: 変数に複数のメソッド (またはクラス外) からアクセスする必要がある場合に限り、インスタンス変数として作成します。単一のメソッドでローカルにのみ必要な場合は、ローカル変数にする必要があります。

インスタンス変数は、ローカル変数よりもコストがかかります。

覚えておいてください: インスタンス変数はデフォルト値に初期化されますが、ローカル変数はそうではありません。

于 2009-11-25T01:51:51.137 に答える
3

各変数のスコープをできるだけ小さくしたいので、メソッド内部のローカル変数が常に優先されます。しかし、複数のメソッドが変数にアクセスする必要がある場合、それはインスタンス変数でなければなりません。

ローカル変数は、結果に到達したり、その場で何かを計算したりするために使用される中間値に似ています。インスタンス変数は、年齢や名前など、クラスの属性に似ています。

于 2009-11-25T01:54:42.473 に答える
2

変数のスコープをできるだけ狭く宣言します。最初にローカル変数を宣言します。これで十分でない場合は、インスタンス変数を使用してください。これで十分でない場合は、クラス (静的) 変数を使用してください。

複数の値を返す必要がある場合は、配列やオブジェクトなどの複合構造を返します。

于 2009-11-25T02:26:07.993 に答える
1

オブジェクトの観点から問題を考えてみてください。各クラスは、異なるタイプのオブジェクトを表します。インスタンス変数は、クラスがそれ自体または他のオブジェクトと連携するために記憶する必要があるデータの断片です。ローカル変数は、メソッドを終了すると保存する必要のないデータである中間計算にのみ使用する必要があります。

于 2009-11-25T01:56:07.120 に答える
1

まず、メソッドから複数の値を返さないようにしてください。それができない場合、場合によっては本当にできない場合は、それをクラスにカプセル化することをお勧めします。最後のケースとして、クラス内の別の変数 (インスタンス変数) を変更することをお勧めします。インスタンス変数アプローチの問題は、副作用が増加することです。たとえば、プログラムでメソッド A を呼び出すと、いくつかのインスタンス変数が変更されます。時間が経つにつれて、コードの複雑さが増し、メンテナンスがますます難しくなります。

インスタンス変数を使用する必要がある場合は、クラス コンストラクターで final にしてから初期化するようにします。これにより、副作用が最小限に抑えられます。このプログラミング スタイル (アプリケーションの状態変更を最小限に抑える) は、保守しやすい優れたコードにつながるはずです。

于 2009-11-25T05:11:34.463 に答える
0

一般に、変数のスコープは最小限にする必要があります。

残念ながら、最小化された変数スコープでクラスを構築するために、多くの場合、多くのメソッド パラメータの受け渡しを行う必要があります。

しかし、常にそのアドバイスに従い、変数のスコープを完全に最小化すると、必要なすべてのオブジェクトがメソッドに出入りすることで、多くの冗長性とメソッドの柔軟性が失われる可能性があります。

次のような何千ものメソッドを含むコード ベースを想像してください。

private ClassThatHoldsReturnInfo foo(OneReallyBigClassThatHoldsCertainThings big,
 AnotherClassThatDoesLittle little) {
    LocalClassObjectJustUsedHere here;
             ...
}
private ClassThatHoldsReturnInfo bar(OneMediumSizedClassThatHoldsCertainThings medium,
 AnotherClassThatDoesLittle little) {
    ...
}

一方、次のような多くのインスタンス変数を持つコード ベースを想像してみてください。

private OneReallyBigClassThatHoldsCertainThings big;
private OneMediumSizedClassThatHoldsCertainThings medium;
private AnotherClassThatDoesLittle little;
private ClassThatHoldsReturnInfo ret;

private void foo() { 
    LocalClassObjectJustUsedHere here; 
    .... 
}
private void bar() { 
    .... 
}

コードが増えると、最初の方法が変数のスコープを最小限に抑えるのに最適ですが、多くのメソッド パラメータが渡される可能性があります。通常、コードはより冗長になり、これらすべてのメソッドをリファクタリングすると複雑になる可能性があります。

より多くのインスタンス変数を使用すると、渡される多数のメソッド パラメータの複雑さが軽減され、わかりやすくするためにメソッドを頻繁に再編成する場合にメソッドに柔軟性がもたらされます。ただし、維持しなければならないオブジェクトの状態がさらに作成されます。一般的には、前者を実行し、後者は控えることをお勧めします。

ただし、非常に多くの場合、人によって異なりますが、最初のケースの何千もの余分なオブジェクト参照と比較して、状態の複雑さをより簡単に管理できます。メソッド内のビジネス ロジックが増加し、秩序と明快さを維持するために組織を変更する必要がある場合、これに気付くことがあります。

それだけでなく。明確さを保つためにメソッドを再編成し、プロセスで多くのメソッド パラメータの変更を行うと、多くのバージョン管理の差分が発生し、安定した製品品質のコードにはあまり適していません。バランスがあります。1 つの方法は、1 種類の複雑さを引き起こします。他の方法は、別の種類の複雑さを引き起こします。

自分に最適な方法を使用してください。時間の経過とともにそのバランスを見つけることができます。

この若いプログラマーは、メンテナンスの少ないコードに対して洞察に満ちた第一印象を持っていると思います。

于 2013-02-01T17:07:31.657 に答える