167

私には奇妙な癖があるようです...少なくとも私の同僚によると。私たちは一緒に小さなプロジェクトに取り組んできました。私がクラスを書いた方法は(単純化された例)です:

[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:私がこれを行った理由は、私のクラスには別のクラスのインスタンスを返すいくつかのプロパティがあり、さらに多くのクラスを持つプロパティもあるからです。最上位クラスのコンストラクターを呼び出すと、必ずしもすべてが必要なわけではない場合でも、これらすべてのクラスのすべてのコンストラクターが後で呼び出されます。

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

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

短所:

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

長所:

  • マイクロ最適化
  • プロパティがnullを返すことはありません
  • 「重い」オブジェクトの読み込みを遅らせるか回避する

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

最後の更新:

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

4

9 に答える 9

172

ここにあるのは、「レイジー初期化」の-ナイーブ-実装です。

短い答え:

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

背景と説明:

具体的な実装:
まず、具体的なサンプルと、その実装がナイーブであると考える理由を見てみましょう。

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

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

一般
的に:レイジー初期 化を一般的に見てみましょう:レイジー初期化は通常、構築に時間がかかるオブジェクトや、完全に構築されると大量のメモリを消費
するオブジェクトの構築を遅らせるために使用されます。 これは、遅延初期化を使用する非常に有効な理由です。

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

レイジープロパティの実装でこれらの2つのポイントを考慮する場合でも、次のポイントはこのパターンの一般的な問題です。

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

    プロパティゲッターから例外をスローすることは避けてください。

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

  2. コンパイラによる自動最適化、つまりインライン化と分岐予測は問題になります。詳細な説明については、BillKの回答を参照してください。

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

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


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

  1. シリアル化:
    EricJは1つのコメントで述べています:

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

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

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

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

      1. 遅延初期化は、パフォーマンスを低下させる可能性があります。ほんのわずかかもしれませんが、ビルの答えが示したように、その影響は一見すると想像以上に大きいものです。したがって、このアプローチは基本的にパフォーマンスとメモリを交換します。
      2. クラスの一部のみを使用することが一般的なユースケースである設計がある場合、これは設計自体に問題があることを示唆しています。問題のクラスには複数の責任がある可能性があります。解決策は、クラスをより焦点を絞ったいくつかのクラスに分割することです。
于 2013-02-08T13:50:50.810 に答える
49

それは良いデザインの選択です。ライブラリコードまたはコアクラスに強くお勧めします。

これは、「遅延初期化」または「遅延初期化」によって呼び出され、一般的に、すべての人が適切な設計上の選択であると考えています。

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

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

第三に、使用されなかったオブジェクトをガベージコレクションすることを回避します。

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

この規則には例外があります。

「get」プロパティの初期化の追加チェックのパフォーマンス引数に関しては、重要ではありません。オブジェクトの初期化と破棄は、ジャンプを伴う単純なnullポインターチェックよりもパフォーマンスに大きな影響を及ぼします。

クラスライブラリを開発するための設計ガイドライン( http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx )

それにかんするLazy<T>

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

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

当然のことながら、プリミティブデータ型やのような単純なクラスの使用にはレイジー初期化を使用しませんList<string>

怠惰についてコメントする前に

Lazy<T>.NET 4.0で導入されたので、このクラスに関してさらにコメントを追加しないでください。

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

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

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

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

「セッター」

遅延ロードされたプロパティにset-property( "setters")を使用したことはありません。したがって、を許可することはありませんfoo.Bar = null;。設定する必要がある場合は、lazy-initializationを使用せずにBar呼び出されるメソッドを作成しますSetBar(Bar value)

コレクション

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

複雑なクラス

これを別の方法で繰り返します。複雑なクラスにはレイジー初期化を使用します。これは通常、不十分に設計されたクラスです。

最後に

私はすべてのクラスまたはすべての場合にこれを行うとは決して言いませんでした。それは悪い習慣です。

于 2013-02-08T14:16:29.993 に答える
17

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

遅延ロードされたオブジェクトを簡単に作成できることに加えて、オブジェクトの初期化中にスレッドセーフが得られます。

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

于 2013-02-08T14:08:12.670 に答える
9

何を初期化するかによると思います。建設費が非常に小さいので、リストにはおそらくそれをしません。それで、コンストラクターに入れることができます。しかし、それが事前に入力されたリストである場合、それが初めて必要になるまで、私はおそらくそうしませんでした。

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

于 2013-02-08T13:51:22.353 に答える
8

私が見ることができる欠点は、Barsがnullかどうかを尋ねたい場合、nullになることはなく、そこでリストを作成することです。

于 2013-02-08T13:51:14.187 に答える
8

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

于 2013-02-08T14:26:27.633 に答える
8

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

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

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

クラスSafeClass
{{
    文字列名="";
    整数年齢=0;

    public void setName(String newName)
    {{
        assert(newName!= null)
        name = newName;
    }//年齢についてはこのパターンに従ってください
    ..。
    public String toString(){
        String s = "Safe Class has name:" + name + "and age:" + age
    }
}

パターンを使用すると、toStringメソッドは次のようになります。

    if(name == null)
        新しいIllegalStateException( "SafeClassが不正な状態になりました!名前がnullです")をスローします
    if(age == null)
        新しいIllegalStateException( "SafeClassが不正な状態になりました!年齢はnullです")をスローします

    public String toString(){
        String s = "Safe Class has name:" + name + "and age:" + age
    }

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

また、クラスは常に不確実な状態にあります。たとえば、いくつかのアノテーションを追加してそのクラスを休止状態のクラスにすることにした場合、どのように行いますか?

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

ブランス予測の問題の例(1回だけではなく、繰り返し発生します)については、このすばらしい質問に対する最初の回答を参照してください。並べ替えられた配列の処理が、並べ替えられていない配列よりも速いのはなぜですか。

于 2013-02-09T00:09:05.700 に答える
4

他の人が作った多くの良い点にもう1点追加しましょう...

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

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

于 2013-02-28T00:44:08.840 に答える
2

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

私には、Fooに何かをインスタンス化させるのは(必ずしも間違っているわけではありませんが)臭いようです。Fooがファクトリであることが明確な目的でない限り、Fooは独自のコラボレーターをインスタンス化するのではなく、コンストラクターに注入する必要があります。

ただし、Fooの目的がBarタイプのインスタンスを作成することである場合、それを怠惰に行うことに何の問題もありません。

于 2013-02-09T11:57:54.953 に答える