1. ホーム
  2. c#

[解決済み] グッドプラクティスかバッドプラクティスか?ゲッターでオブジェクトを初期化する

2022-04-24 14:07:55

質問

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

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

    private Bar _bar;

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

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

ですから、基本的には、ゲッターが呼ばれて、フィールドがまだヌルであるときだけ、どのフィールドも初期化するようにしています。どこにも使われていないプロパティを初期化しないことで、過負荷を減らすことができると考えたのです。

ETA: なぜこのようにしたかというと、私のクラスには別のクラスのインスタンスを返すプロパティがいくつかあり、そのプロパティもまた別のクラスのプロパティを持ち、といった具合になります。トップクラスのコンストラクタを呼び出すと、その後にこれらのクラスすべてのコンストラクタが呼び出されます。 常に が必要です。

このやり方に対して、個人的な好み以外に異論はないのでしょうか?

UPDATE: この質問に関して多くの異なる意見を考慮し、私は自分の受け入れた答えを守ります。しかし、私はこのコンセプトをより良く理解し、いつ使用し、いつ使用しないかを決めることができるようになりました。

短所

  • スレッドセーフの問題
  • 値がNULLの場合、quot;setter"リクエストに従わない。
  • マイクロ最適化
  • 例外処理はコンストラクタで行う必要があります。
  • クラスのコードでNullをチェックする必要がある

長所

  • マイクロ最適化
  • プロパティがNULLを返すことはない
  • 重いオブジェクトのロードを遅延させるか回避する。

短所のほとんどは私の現在のライブラリには適用できませんが、quot;マイクロ最適化"が実際に何かを全く最適化しているかどうかはテストする必要があるでしょう。

最後の更新です。

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

どのように解決するのですか?

ここにあるのは、quot;遅延初期化" の - 素朴な - 実装です。

簡単な答え

遅延初期化の使用 無条件に は良いアイデアではありません。それはそれでよいのですが、この解決策がもたらす影響を考慮しなければなりません。

背景と説明

具体的な実装方法。

まず、あなたの具体的なサンプルと、その実装がなぜ素朴なものであると考えるかを見てみましょう。

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

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
    
  2. これは、かなりのスレッド問題を導入しています。の呼び出し元が2つある場合 foo.Bar の2つの異なるインスタンスを取得する可能性があります。 Bar との接続がなく、そのうちの1つは Foo インスタンスを作成します。このインスタンスに加えられたすべての変更は Bar インスタンスは静かに失われます。

    これもPOLSに違反するケースです。プロパティの格納された値のみにアクセスする場合、スレッドセーフであることが期待されます。プロパティのゲッターを含め、クラスが単にスレッドセーフでないと主張することはできますが、これは通常のケースではないため、適切に文書化する必要があります。さらに、この問題の導入は、まもなく見るように不要です。

一般的には

ここで、一般的な遅延初期化について見てみましょう。

遅延初期化は、通常、オブジェクトの構築を遅延させるために使用されます。 構築するのに時間がかかるもの、あるいは多くのメモリを消費するもの が完全に構築されると

これは、遅延初期化を使用する非常に正当な理由です。

しかし、このようなプロパティは通常、セッターを持たないので、上で指摘した最初の問題は解消されます。

さらに、スレッドセーフな実装が使用されるでしょう。 Lazy<T> - を使用することで、2つ目の問題を回避することができます。

遅延プロパティの実装において、この2点を考慮しても、このパターンの一般的な問題点として以下の点が挙げられます。

  1. オブジェクトの構築に失敗し、プロパティ・ゲッターから例外が発生する可能性があります。これはまた別の POLS 違反であるため、回避する必要があります。また プロパティのセクション の中で、プロパティ・ゲッターは例外を投げてはいけないと明記しています。

    <ブロッククオート

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

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

  2. コンパイラによる自動最適化、すなわちインライン化、分岐予測に問題があります。詳しくは ビル・Kの回答 に詳しい説明があります。

これらの点を踏まえての結論は以下の通りです。

怠惰に実装された1つのプロパティごとに、これらの点を考慮する必要がありました。

つまり、ケースバイケースの判断であり、一般的なベストプラクティスとして捉えることはできないということです。

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


このセクションでは、他の人が遅延初期化を無条件に使用する論拠として提唱しているいくつかの点について説明したいと思います。

  1. シリアライゼーション。

    EricJは1つのコメントでこう述べている。

    シリアライズされる可能性のあるオブジェクトは、デシリアライズされるときにそのコントラクターが呼び出されることはありません(シリアライザーによりますが、多くの一般的なものはこのような挙動をします)。初期化コードをコンストラクタに入れると、デシリアライズのための追加サポートを提供しなければならないことになります。このパターンでは、そのような特別なコーディングを避けることができます。

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

    1. ほとんどのオブジェクトはシリアライズされることはありません。必要でないときに何らかのサポートを追加することは、以下のことに違反します。 YAGNI .
    2. クラスがシリアライゼーションをサポートする必要がある場合、一見シリアライゼーションとは関係のない回避策を講じることなく、シリアライゼーションを可能にする方法が存在します。
  2. マイクロオプティマイゼーション。 あなたの主な主張は、誰かが実際にアクセスするときだけオブジェクトを構築したい、というものです。つまり、実際にはメモリ使用量の最適化について話しているわけです。

    私は以下の理由から、この主張には賛成できません。

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

      1. 怠惰な初期化は、潜在的にパフォーマンスを低下させる。ほんのわずかかもしれませんが、Billの回答が示すように、その影響は一見して思ったよりも大きいのです。つまり、このアプローチは基本的にパフォーマンスとメモリをトレードするものなのです。
      2. もし、クラスの一部だけを使用することが一般的なユースケースであるような設計があれば、それは設計自体に問題があることを示唆しています。問題のクラスは、複数の責任を負っている可能性が高いです。解決策としては、そのクラスをより焦点を絞ったいくつかのクラスに分割することです。