web-dev-qa-db-ja.com

良い練習か悪い練習か?ゲッターでオブジェクトを初期化する

私には奇妙な癖があります...少なくとも私の同僚によると。私たちは一緒に小さなプロジェクトに取り組んできました。クラスの記述方法は次のとおりです(簡単な例)。

[Serializable()]
public class Foo
{
    public Foo()
    { }

    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();

            return _bar;
        }
        set { _bar = value; }
    }
}

したがって、基本的に、ゲッターが呼び出され、フィールドがまだnullの場合にのみ、フィールドを初期化します。どこでも使用されていないプロパティを初期化しないことで、これによりオーバーロードが軽減されると考えました。

ETA:私がこれをした理由は、私のクラスが別のクラスのインスタンスを返すいくつかのプロパティを持っているためです。最上位クラスのコンストラクターを呼び出すと、これらのクラスすべてのコンストラクターがalways allでなくても、すべて呼び出されます。

個人的な好み以外に、この慣行に対する異議はありますか?

更新:私はこの質問に関して多くの異なる意見を検討しており、私は受け入れられた答えを支持します。しかし、今では概念についての理解が深まり、いつ使用するか、使用しないかを決定できるようになりました。

短所:

  • スレッドの安全性の問題
  • 渡された値がnullの場合、「セッター」リクエストに従わない
  • ミクロ最適化
  • 例外処理はコンストラクターで実行する必要があります
  • クラスのコードでnullをチェックする必要があります

長所:

  • ミクロ最適化
  • プロパティがnullを返すことはありません
  • 「重い」オブジェクトのロードを遅延または回避する

短所のほとんどは現在のライブラリには適用されませんが、「マイクロ最適化」が実際に何かを最適化しているかどうかをテストする必要があります。

最後の更新:

さて、答えを変えました。私の最初の質問は、これが良い習慣であるかどうかでした。そして、私はそうではないと確信しています。おそらく、現在のコードの一部でそれを使用しますが、無条件ではなく、常に使用するわけではありません。だから私は私の習慣を失い、それを使用する前にそれについて考えるつもりです。みんな、ありがとう!

166
John Willemse

ここにあるのは、素朴な「遅延初期化」の実装です。

簡潔な答え:

遅延初期化無条件を使用することはお勧めできません。場所はありますが、このソリューションが与える影響を考慮する必要があります。

背景と説明:

具体的な実装:
最初に具体的なサンプルを見てみましょう。なぜ実装が単純だと思いますか:

  1. 最小驚きの原則(POLS) に違反しています。値がプロパティに割り当てられると、この値が返されることが期待されます。実装では、これはnullには当てはまりません。

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
  2. スレッドに関するいくつかの問題が発生します。異なるスレッド上のfoo.Barの2つの呼び出し元は、潜在的にBarの2つの異なるインスタンスを取得でき、そのうちの1つはFooインスタンスに接続できません。そのBarインスタンスに加えられた変更は、静かに失われます。
    これはPOLS違反の別のケースです。格納されたプロパティの値のみにアクセスする場合、スレッドセーフであることが期待されます。クラスは単純にスレッドセーフではないと主張することもできますが(プロパティのゲッターを含む)、これは通常のケースではないため、適切に文書化する必要があります。さらに、すぐにわかるように、この問題の導入は不要です。

一般的に:
次は、一般的な遅延初期化について説明します。
通常、遅延初期化は、オブジェクトの構築を遅らせるために使用されます構築に時間がかかる、または大量のメモリを消費する完全に構築されると。
それが遅延初期化を使用する非常に正当な理由です。

ただし、通常、このようなプロパティにはセッターがありません。これにより、上記で指摘した最初の問題が解消されます。
さらに、2番目の問題を回避するために、 Lazy<T> のようなスレッドセーフな実装が使用されます。

遅延プロパティの実装でこれらの2つのポイントを検討する場合でも、次のポイントがこのパターンの一般的な問題です。

  1. オブジェクトの構築が失敗し、プロパティゲッターから例外が発生する可能性がありました。これはPOLSの違反であり、回避する必要があります。 「クラスライブラリ開発の設計ガイドライン」の プロパティのセクション でも、プロパティゲッターは例外をスローしてはならないことを明示しています。

    プロパティゲッターから例外をスローしないようにします。

    プロパティゲッターは、前提条件のない単純な操作である必要があります。ゲッターが例外をスローする可能性がある場合は、プロパティをメソッドとして再設計することを検討してください。

  2. コンパイラによる自動最適化、つまりインライン化と分岐予測が損なわれます。詳細な説明については、 ビルKの答え を参照してください。

これらのポイントの結論は次のとおりです:
遅延的に実装される各プロパティごとに、これらの点を考慮する必要があります。
つまり、これはケースごとの決定であり、一般的なベストプラクティスと見なすことはできません。

このパターンには場所がありますが、クラスを実装するときの一般的なベストプラクティスではありません。上記の理由により、無条件に使用しないでください


このセクションでは、レイジー初期化を無条件に使用するための引数として他の人が提起したいくつかのポイントについて説明します。

  1. シリアル化:
    EricJのコメント:

    シリアライズされる可能性のあるオブジェクトは、デシリアライズされるときにそのコンストラクターが呼び出されません(シリアライザーに依存しますが、多くの一般的なオブジェクトはこのように動作します)。コンストラクターに初期化コードを配置すると、逆シリアル化の追加サポートを提供する必要があります。このパターンは、特別なコーディングを回避します。

    この引数にはいくつかの問題があります。

    1. ほとんどのオブジェクトはシリアル化されません。不要なときに何らかのサポートを追加すると、 [〜#〜] yagni [〜#〜] に違反します。
    2. クラスがシリアル化をサポートする必要がある場合、一見シリアル化とは関係のない回避策なしでクラスを有効にする方法があります。
  2. マイクロ最適化:主な論点は、誰かが実際にアクセスしたときにのみオブジェクトを構築することです。あなたは実際にメモリ使用量の最適化について話しているのです。
    次の理由により、この議論に同意しません。

    1. ほとんどの場合、メモリ内のいくつかのオブジェクトは、何にも影響を及ぼしません。現代のコンピューターには十分なメモリがあります。プロファイラーによって実際の問題が確認されない場合、これは 未熟な最適化 であり、それには十分な理由があります。
    2. 時々、この種の最適化が正当化されるという事実を認めます。しかし、これらの場合でも、遅延初期化は正しい解決策ではないようです。それに反対する2つの理由があります。

      1. 遅延初期化はパフォーマンスを低下させる可能性があります。たぶんほんのわずかですが、ビルの答えが示したように、その影響は一見すると思うかもしれません。したがって、このアプローチは基本的にパフォーマンスとメモリのトレードオフです。
      2. クラスの一部のみを使用することが一般的なユースケースであるデザインがある場合、これはデザイン自体の問題を示唆しています。問題のクラスには複数の責任がある可能性があります。解決策は、クラスをいくつかのより焦点の合ったクラスに分割することです。
170
Daniel Hilgarth

これは良い設計選択です。ライブラリコードまたはコアクラスに強くお勧めします。

これは、いくつかの「遅延初期化」または「遅延初期化」によって呼び出され、一般にすべての人が適切な設計選択と見なしています。

まず、クラスレベルの変数またはコンストラクターの宣言で初期化した場合、オブジェクトが構築されると、使用されない可能性のあるリソースを作成するオーバーヘッドが生じます。

次に、必要な場合にのみリソースが作成されます。

3番目に、使用されなかったオブジェクトのガベージコレクションを回避します。

最後に、プロパティで発生する可能性のある初期化例外は、クラスレベル変数またはコンストラクターの初期化中に発生する例外よりも簡単に処理できます。

このルールには例外があります。

「get」プロパティでの初期化の追加チェックのパフォーマンス引数については、重要ではありません。オブジェクトを初期化して破棄することは、ジャンプを使用した単純なNULLポインターチェックよりもパフォーマンスが大幅に低下します。

クラスライブラリ開発の設計ガイドラインat http://msdn.Microsoft.com/en-US/library/vstudio/ms229042.aspx

_Lazy<T>_について

ジェネリック_Lazy<T>_クラスは、ポスターが望むもののために作成されました。 http://msdn.Microsoft.com/en-usのLazy Initializationを参照してください。 /library/dd997286(v=vs.100).aspx 。 .NETの古いバージョンを使用している場合は、質問に示されているコードパターンを使用する必要があります。このコードパターンは非常に一般的なものになったため、Microsoftは最新の.NETライブラリにクラスを含めて、パターンの実装を容易にしました。さらに、実装にスレッドセーフが必要な場合は、追加する必要があります。

プリミティブデータ型と単純なクラス

明らかに、プリミティブデータ型や_List<string>_のような単純なクラスの使用に遅延初期化を使用することはありません。

Lazyについてコメントする前に

_Lazy<T>_は.NET 4.0で導入されたため、このクラスに関する別のコメントを追加しないでください。

マイクロ最適化についてコメントする前に

ライブラリを構築するときは、すべての最適化を考慮する必要があります。たとえば、.NETクラスでは、コード全体でブールクラス変数に使用されるビット配列が表示され、メモリ消費とメモリの断片化が削減されます。

ユーザーインターフェースについて

ユーザーインターフェイスで直接使用されるクラスには、遅延初期化を使用しません。先週、コンボボックスのビューモデルで使用される8つのコレクションの遅延読み込みを削除するのに1日の大半を費やしました。ユーザーインターフェイス要素に必要なコレクションの遅延読み込みとキャッシュを処理するLookupManagerがあります。

「セッター」

遅延読み込みプロパティにset-property( "setter")を使用したことはありません。したがって、_foo.Bar = null;_は許可されません。 Barを設定する必要がある場合は、SetBar(Bar value)というメソッドを作成し、遅延初期化を使用しません

コレクション

クラスコレクションプロパティは、nullであってはならないため、宣言時に常に初期化されます。

複雑なクラス

繰り返しますが、複雑なクラスには遅延初期化を使用します。これは通常、設計が不十分なクラスです。

最後に

すべてのクラスまたはすべてのケースでこれを行うことはありません。それは悪い習慣です。

49
AMissico

Lazy<T>を使用してこのようなパターンを実装することを検討しますか?

遅延ロードされたオブジェクトを簡単に作成できるだけでなく、オブジェクトの初期化中にスレッドセーフを取得できます。

他の人が言ったように、オブジェクトが本当にリソースを大量に使用する場合、またはオブジェクトの構築時にオブジェクトをロードするのに時間がかかる場合、オブジェクトを遅延ロードします。

17

私が見ることができる欠点は、Barsがnullであるかどうかを尋ねたい場合、それは決してありません、そしてあなたはそこにリストを作成しているということです。

9
Luis Tellez

初期化する内容に依存すると思います。建設コストが非常に小さいので、おそらくリストにはそれをしないでしょう。したがって、コンストラクターに入れることができます。しかし、それが事前に作成されたリストである場合、私はおそらくそれが初めて必要になるまでそうしなかったでしょう。

基本的に、建設コストが各アクセスで条件付きチェックを行うコストを上回る場合、それを遅延作成します。そうでない場合は、コンストラクターで実行します。

9
Colin Mackay

私はダニエルの答えにコメントするつもりでしたが、正直に言ってそれが十分だとは思いません。

これは特定の状況(たとえば、オブジェクトがデータベースから初期化されるとき)で使用するのに非常に優れたパターンですが、入るのは大変な習慣です。

オブジェクトの最も良い点の1つは、安全で信頼できる環境を提供することです。最良のケースは、可能な限り多くのフィールドを「最終」にして、それらをすべてコンストラクターで埋める場合です。これにより、クラスはまったく防弾になります。セッターを介してフィールドを変更できるようにすることは少し少なくなりますが、ひどいことではありません。例えば:

 class SafeClass 
 {
 String name = ""; 
 Integer age = 0; 
 
 public void setName(String newName )
 {
 assert(newName!= null)
 name = newName; 
} // age 
 ... 
 public String toString(){
 String s = "セーフクラスの名前:" + name + "および年齢:" + age 
} 
} 

パターンでは、toStringメソッドは次のようになります。

 if(name == null)
 throw new IllegalStateException( "SafeClassが不正な状態になりました!name is null")
 if(age == null)
 throw new IllegalStateException( "SafeClassが不正な状態になりました!年齢はnull")
 
 public String toString(){
 String s = "Safe Class has name:" + name + "およびage:" + age 
} 

これだけでなく、クラスでそのオブジェクトを使用する可能性のあるすべての場所でヌルチェックが必要です(ゲッターでのヌルチェックによりクラスの外は安全ですが、ほとんどの場合はクラス内でクラスメンバーを使用する必要があります)

また、クラスは常に不確実な状態にあります。たとえば、いくつかの注釈を追加してそのクラスを休止状態のクラスにすると決めた場合、どうしますか?

要件やテストなしで、いくつかのマイクロ最適化に基づいて決定を下す場合、それはほぼ間違いなく間違った決定です。実際、ifステートメントはCPUで分岐予測エラーを引き起こし、それよりもはるかに多くの時間を遅くする可能性があるため、パターンが実際に最も理想的な状況下でも実際にシステムを遅くする可能性は非常に高い可能性があります作成するオブジェクトがかなり複雑であるか、リモートデータソースからのものでない限り、コンストラクタで値を割り当てるだけです。

Brance予測の問題の例(繰り返し発生し、1回だけ発生します)については、この素晴らしい質問に対する最初の答えを参照してください。 なぜソートされていない配列よりもソートされた配列を処理する方が速いのですか?

8
Bill K

遅延インスタンス化/初期化は完全に実行可能なパターンです。ただし、一般的なルールとして、APIのコンシューマーは、getterおよびsetterがエンドユーザーPOVから識別可能な時間を取る(または失敗する)ことを期待していないことに注意してください。

8
Tormod

他の人が作った多くの良い点にもう1点だけ付け加えます...

デバッガーは( デフォルト )コードをステップスルーするときにプロパティを評価します。これにより、コードを実行するだけで通常よりも早くBarをインスタンス化できます。言い換えれば、デバッグの単なる行為はプログラムの実行を変更することです。

これは問題である場合とそうでない場合があります(副作用によって異なります)が、注意する必要があります。

4

Fooがインスタンス化する必要がありますか?

私には、Fooに何もインスタンス化させないようにすることは、(必ずしもwrongとは限りませんが)臭いようです。 Fooがファクトリーであるという明確な目的でない限り、自身のコラボレーターをインスタンス化するべきではありませんが、 代わりにコンストラクターにインジェクトされます です。

しかし、Fooの目的がタイプBarのインスタンスを作成することである場合、遅延して実行しても何も問題はありません。

2
KaptajnKold