良いか悪いか?ゲッターでのオブジェクトの初期化


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の場合、「setter」リクエストに従わない
  • マイクロ最適化
  • 例外処理はコンストラクターで行う必要があります
  • クラスのコードでnullをチェックする必要があります

長所:

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

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

最後の更新:

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


14
これはレイジーロードのパターンであり、ここで素晴らしいメリットが得られるわけではありませんが、それでも私にとっては良いことです。
Machinarius、2013

28
遅延のインスタンス化は、パフォーマンスに測定可能な影響がある場合、またはそれらのメンバーがめったに使用されず、法外な量のメモリを消費している場合、またはインスタンス化に時間がかかり、オンデマンドでのみ実行したい場合に適しています。とにかく、スレッドセーフティの問題を考慮し(現在のコードはでない)、提供されているLazy <T>クラスの使用を検討してください。
クリスシンクレア


7
@PLBこれはシングルトンパターンではありません。
コリンマッカイ

30
誰もこのコードの深刻なバグについて言及していないことに驚いています。外部から設定できるパブリックプロパティがあります。NULLに設定すると、常に新しいオブジェクトが作成され、私のセッターアクセスは無視されます。これは非常に深刻なバグである可能性があります。プライベートプロパティの場合、これはokieです。個人的には、このような時期尚早の最適化をしたくありません。追加の利点がないために複雑さが追加されます。
SolutionYogi

回答:


170

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

短い答え:

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

背景と説明:

具体的な実装:
最初に具体的なサンプルを見て、なぜ私がその実装を単純であると考えるのかを見てみましょう。

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

4
@JohnWillemse:それはあなたのアーキテクチャの問題です。クラスをより小さく焦点を絞った方法でリファクタリングする必要があります。5つの異なるもの/タスクに対して1つのクラスを作成しないでください。代わりに5つのクラスを作成します。
Daniel Hilgarth、2013

26
@JohnWillemseおそらく、これは時期尚早の最適化のケースと考えられます。測定されたパフォーマンス/メモリのボトルネックがない限り、複雑さを増し、スレッド化の問題を引き起こすため、これをお勧めします。
クリスシンクレア

2
+1、これは95%のクラスに適した設計の選択ではありません。遅延初期化には長所がありますが、すべてのプロパティに対して一般化すべきではありません。複雑さ、コードの読み取り難さ、スレッドの安全性の問題が追加されます... 99%のケースで知覚可能な最適化は行われません。また、SolutionYogiがコメントとして述べたように、OPのコードはバグが多いため、このパターンを実装するのは簡単ではなく、遅延初期化が実際に必要でない限り、避ける必要があります。
ken2k 2013

2
@DanielHilgarthこのパターンを無条件で使用することで間違っていた(ほぼ)すべてを書き留めてくれてありがとう。よくやった!
アレックス

1
@DanielHilgarthはいはい、いいえ。違反はここの問題なのでそうです。しかし、「いいえ」もPOLSが厳密に原則であるため、コードに驚くことはおそらくありません。Fooがプログラムの外部に公開されなかった場合、それはあなたが取ることができるかどうかにかかわらずリスクです。この場合、プロパティへのアクセス方法を制御しないため、最終的には驚かれることをほぼ保証できます。リスクはバグになり、nullケースについてのあなたの主張はより強くなりました。:-)
atlaste 2013

49

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

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

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

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

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

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

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

「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)を参照してください。古いバージョンの.NETを使用している場合は、質問に示されているコードパターンを使用する必要があります。このコードパターンは非常に一般的になり、パターンの実装を容易にするために、最新の.NETライブラリにクラスを含めるのに適しているとMicrosoftは考えました。さらに、実装でスレッドセーフが必要な場合は、それを追加する必要があります。

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

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

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

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

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

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

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

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

「セッター」

遅延読み込みされたプロパティにセットプロパティ(「セッター」)を使用したことはありません。したがって、許可することはありませんfoo.Bar = null;。設定する必要がある場合は、遅延初期化を使用せずにBar呼び出されるメソッドを作成しSetBar(Bar value)ます

コレクション

クラスコレクションプロパティは、nullにすることはできないため、宣言すると常に初期化されます。

複雑なクラス

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

最後に

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


6
介入する値を設定せずに異なるスレッドでfoo.Barを複数回呼び出すことができても、異なる値を取得できる場合は、お粗末なクラスです。
リーライアン

25
これは、多くの考慮事項がない、悪い経験則だと思います。Barが既知のリソースを独占していない限り、これは不要なマイクロ最適化です。また、Barがリソースを大量に消費する場合、.netに組み込まれたスレッドセーフのLazy <T>があります。
Andrew Hanlon、2013

10
「プロパティで発生する可能性のある初期化例外を処理してから、クラスレベル変数またはコンストラクターの初期化中に発生する例外を処理する方が簡単です。」-わかりました、これはばかげています。何らかの理由でオブジェクトを初期化できない場合は、できるだけ早く知りたいです。すなわち、それが構築されるとすぐに。遅延初期化を使用することについては良い議論がありますが、それを広範に使用することは良い考えではないと思います。
ミリムース2013

20
他の開発者がこの答えを見て、これが本当に良い習慣だと思うのは本当に心配です(ああ)。無条件で使用している場合、これは非常に悪い習慣です。すでに述べられていることに加えて、(クライアント開発者にとっても、メンテナンス開発者にとっても)わずかな利益(あるとしても)のために、みんなの生活を非常に困難にしています。あなたはプロから聞くべきです:The Art of Computer Programmingのドナルド・クヌースは有名に「時期尚早な最適化がすべての悪の根源だ」と言っています。あなたがしていることは悪であるだけでなく、悪魔です!
アレックス

4
間違った答え(そして間違ったプログラミングの決定)を選択している非常に多くの指標があります。リストには長所より短所があります。あなたはそれに対してそれよりもそれを保証するより多くの人々を持っています。この質問に投稿したこのサイトのより経験豊富なメンバー(@BillKと@DanielHilgarth)はそれに反対しています。あなたの同僚はすでにそれが間違っているとあなたに言いました。真剣に、それは間違っています!チームの開発者の1人(私はチームリーダーです)がこれを行っているのを見つけた場合、彼は5分のタイムアウトをとり、なぜこれをしてはいけないのか説明を受けます。
アレックス

17

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

レイジーロードされたオブジェクトの簡単な作成に加えて、オブジェクトが初期化されている間、スレッドの安全性が得られます。

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


おかげで、私はそれを今理解し、今は絶対に調べてLazy<T>、いつものように使用することを控えます。
John Willemse 2013

1
あなたは魔法の糸の安全性を得ません...あなたはそれについてまだ考えなければなりません。MSDNから:Making the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
Eric J.

@EricJ。もちろん、もちろん。オブジェクトの初期化時にのみスレッドセーフを取得しますが、後で他のオブジェクトと同様に同期を処理する必要があります。
マティアスフィデムライザ2013

9

それはあなたが何を初期化しているかによると思います。構築コストが非常に小さいので、リストでそれを行うことはおそらくないでしょう。しかし、それが事前入力されたリストである場合は、初めて必要になるまでおそらくそうしません。

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


ありがとうございました!それは理にかなっている。
ジョンウィレムセ2013

9

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


それはマイナス面だとは思いません。
Peter Porfy 2013

なぜそれはマイナス面ですか?代わりにnullに対して確認してください。if(!Foo.Bars.Any())
s.meijer 2013

6
@PeterPorfy:POLSに違反しています。あなたは入れnullましたが、取り戻しません。通常、プロパティに入力したのと同じ値が返されると想定します。
Daniel Hilgarth、2013

@DanielHilgarthありがとうございます。これは、私が以前に考慮していなかった非常に有効な議論です。
John Willemse 2013

6
@AMissico:作り上げたコンセプトではありません。玄関のドアの横にあるボタンを押すとドアベルが鳴るのと同じように、プロパティのように見えるものはプロパティのように動作することが期待されます。特にボタンにそのようなラベルが付けられていない場合は、足の下でトラップドアを開くのは驚くべき動作です。
ブライアンベッチャー2013

8

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


1
同意し、質問を少し編集しました。基礎となるコンストラクターの完全なチェーンは、必要なときにのみクラスをインスタンス化するよりも時間がかかると思います。
John Willemse 2013

8

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

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

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

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

    public void setName(String newName)
    {
        assert(newName!= null)
        name = newName;
    } //年齢についてはこのパターンに従います
    ...
    public String toString(){
        文字列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(){
        文字列s = "Safe Class has name:" + name + "and age:" + age
    }

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

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

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

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


ご入力いただきありがとうございます。私の場合、どのクラスにもnullをチェックする必要のあるメソッドがないため、問題ありません。他の反対意見を考慮に入れます。
ジョンウィレムセ2013

よくわかりません。これは、メンバーが格納されているクラスでメンバーを使用していないこと、つまり、クラスをデータ構造として使用していることを意味します。その場合は、javaworld.com / javaworld / jw-01-2004 / jw-0102-toolbox.htmlを読んで、オブジェクトの状態を外部から操作しないようにすることでコードをどのように改善できるかを説明します。内部でそれらを操作している場合、すべてを繰り返しnullチェックすることなくどのように操作しますか?
Bill K

この回答の一部は良いですが、一部は不自然なようです。通常、このパターンを使用する場合は、直接使用せずにtoString()を呼び出します。getName()name
イズカタ

@BillKはい、クラスは巨大なデータ構造です。すべての作業は静的クラスで行われます。リンク先の記事をチェックします。ありがとう!
John Willemse 2013

1
@izkata実はクラス内ではゲッターを使うかどうかはおどかしいようで、今まで働いていたほとんどの場所で直接メンバーを使っていました。それはさておき、常にゲッターを使用している場合、分岐予測の失敗がより頻繁に発生するため、そしてランタイムが分岐するためにランタイムがゲッターを挿入するのにより困難になるため、if()メソッドはさらに有害です。しかし、それらはデータ構造と静的クラスであるというジョンの啓示により、それは私が最も気になることです。
ビルK

4

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

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

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


2

Fooが何かをインスタンス化する必要があると確信していますか?

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

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


4
@BenjaminGruenbaumいいえ、そうではありません。そして、敬意を表して、たとえそうであったとしても、あなたは何を主張しようとしていましたか?
KaptajnKold 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.