web-dev-qa-db-ja.com

相互に参照する不変オブジェクト?

今日、私はお互いを参照する不変のオブジェクトに頭を包み込もうとしていました。遅延評価を使用せずにそれを行うことはできないという結論に達しましたが、その過程で私はこの(私の意見では)興味深いコードを作成しました。

public class A
{
    public string Name { get; private set; }
    public B B { get; private set; }
    public A()
    {
        B = new B(this);
        Name = "test";
    }
}

public class B
{
    public A A { get; private set; }
    public B(A a)
    {
        //a.Name is null
        A = a;
    }
}

私が興味深いと思うのは、まだ完全に構築されておらず、スレッドを含む状態でタイプAのオブジェクトを観察する別の方法を考えることができないということです。なぜこれが有効なのですか?完全に構築されていないオブジェクトの状態を観察する他の方法はありますか?

76
Stilgar

なぜこれが有効なのですか?

なぜそれが無効であると期待するのですか?

コンストラクターは、外部コードがオブジェクトの状態を監視する前に、コンストラクターに含まれるコードが実行されることを保証することになっているためです。

正しい。ただし、コンパイラはその不変条件を維持する責任を負いません。あなたはです。その不変条件を破るコードを書き、それを行うと痛い場合は、それをやめる

完全に構築されていないオブジェクトの状態を観察する他の方法はありますか?

承知しました。参照型の場合、ストレージへの参照を保持する唯一のユーザーコードはコンストラクターであるため、これらはすべて、コンストラクターから「this」を渡す必要があります。コンストラクターが「これ」をリークする可能性のあるいくつかの方法は次のとおりです。

  • 「this」を静的フィールドに入れて、別のスレッドから参照します
  • メソッド呼び出しまたはコンストラクター呼び出しを行い、引数として「this」を渡します
  • 仮想呼び出しを行います。仮想メソッドが派生クラスによってオーバーライドされると、派生クラスのctor本体が実行される前に実行されるため、特に厄介です。

参照を保持する唯一のユーザーコードはctorであると言いましたが、もちろんガベージコレクターも参照を保持します。したがって、オブジェクトが半分構築された状態にあることを確認できるもう1つの興味深い方法は、オブジェクトにデストラクタがあり、コンストラクタが例外をスローする(またはスレッドアボートのような非同期例外を取得する、詳細は後で説明する)場合です。 )その場合、オブジェクトはもうすぐ死んでしまうため、ファイナライズする必要がありますが、ファイナライザスレッドはオブジェクトの半分初期化された状態を確認できます。そして今、私たちは半分構築されたオブジェクトを見ることができるユーザーコードに戻っています!

デストラクタは、このシナリオに直面して堅牢である必要があります。デストラクタは、維持されているコンストラクタによって設定されたオブジェクトの不変条件に依存してはなりません。破壊されるオブジェクトが完全に構築されていない可能性があるためです。

半分構築されたオブジェクトが外部コードによって観察される可能性がある別のクレイジーな方法は、もちろん、デストラクタが上記のシナリオで半分初期化されたオブジェクトを見て、参照をコピーする場合ですそのオブジェクトを静的フィールドに移動し、それによって、半分構築され、半分完成されたオブジェクトが死から救われることを保証します。 それをしないでください。私が言ったように、それが痛いなら、それをしないでください。

値型のコンストラクターを使用している場合、基本的には同じですが、メカニズムにいくつかの小さな違いがあります。この言語では、値型に対するコンストラクター呼び出しで、ctorのみがアクセスできる一時変数を作成し、その変数を変更してから、変更された値の構造体コピーを実際のストレージに実行する必要があります。これにより、コンストラクターがスローした場合、最終的なストレージが半変異状態にならないことが保証されます。

構造体のコピーはアトミックであることが保証されていないため、is別のスレッドがストレージを半変異状態で見ることができることに注意してください。そのような状況にある場合は、ロックを正しく使用してください。また、スレッドアボートなどの非同期例外が構造体コピーの途中でスローされる可能性もあります。これらの非原子性の問題は、コピーがctorの一時的なコピーであるか、「通常の」コピーであるかに関係なく発生します。また、一般に、非同期例外がある場合、維持される不変条件はごくわずかです。

実際には、C#コンパイラは、そのシナリオが発生する方法がないと判断できる場合、一時的な割り当てとコピーを最適化します。たとえば、新しい値がラムダによって閉じられておらず、イテレータブロック内にないローカルを初期化している場合、S s = new S(123);sを直接変更します。

値型コンストラクターの動作の詳細については、以下を参照してください。

値型に関する別の神話を暴く

また、C#言語のセマンティクスがどのように自分自身からあなたを救おうとするかについての詳細は、以下を参照してください。

イニシャライザーがコンストラクターとして逆の順序で実行されるのはなぜですか?パート1

イニシャライザーがコンストラクターとして逆の順序で実行されるのはなぜですか?パート2

目前の話題から外れたようです。もちろん、構造体では、同じ方法でオブジェクトが半分構築されるのを観察できます。半分構築されたオブジェクトを静的フィールドにコピーし、引数として「this」を指定してメソッドを呼び出すなどです。 (明らかに、より派生した型で仮想メソッドを呼び出すことは構造体の問題ではありません。)そして、私が言ったように、一時ストレージから最終ストレージへのコピーはアトミックではないため、別のスレッドが半分コピーされた構造体を監視できます。


ここで、質問の根本原因を考えてみましょう。相互に参照する不変オブジェクトをどのように作成しますか?

通常、あなたが発見したように、あなたはそうしません。相互に参照する2つの不変オブジェクトがある場合、論理的には有向非巡回グラフを形成します。不変の有向グラフを作成することを検討してください。そうすることは非常に簡単です。不変の有向グラフは、次のもので構成されます。

  • それぞれに値が含まれる不変ノードの不変リスト。
  • 不変ノードペアの不変リスト。各ペアには、グラフエッジの開始点と終了点があります。

ここで、ノードAとBを相互に「参照」させる方法は次のとおりです。

A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);

これで、AとBが相互に「参照」するグラフができました。

もちろん問題は、Gが手元にないとAからBに到達できないことです。その余分なレベルの間接参照を持つことは受け入れられないかもしれません。

105
Eric Lippert

はい、これは2つの不変オブジェクトが相互に参照する唯一の方法です。少なくとも一方は、完全に構築されていない方法で他方を見る必要があります。

それは 一般的にコンストラクターからthisをエスケープさせるのは悪い考えです しかし、両方のコンストラクターが何をするかについて自信があり、それが可変性の唯一の代替手段である場合、私はしませんあまりにも悪いと思います。

47
Jon Skeet

「完全に構築された」は、言語ではなくコードによって定義されます。

これは、コンストラクターから仮想メソッドを呼び出すバリエーションです。
一般的なガイドラインは次のとおりです:そうしないでください

「完全に構築された」という概念を正しく実装するには、コンストラクターからthisを渡さないでください。

22
Henk Holterman

実際、コンストラクター中にthis参照をリークすると、これを行うことができます。明らかに、不完全なオブジェクトでメソッドが呼び出されると、問題が発生する可能性があります。 「完全に構築されていないオブジェクトの状態を観察する他の方法」について:

  • コンストラクターでvirtualメソッドを呼び出します。サブクラスコンストラクターはまだ呼び出されていないため、overrideは不完全な状態(サブクラスで宣言または初期化されたフィールドなど)にアクセスしようとする可能性があります。
  • リフレクション、おそらくFormatterServices.GetUninitializedObjectを使用します(コンストラクターを呼び出さずにオブジェクトを作成しますまったく
8
Marc Gravell

初期化の順序を検討する場合

  • 派生静的フィールド
  • 派生静的コンストラクター
  • 派生インスタンスフィールド
  • 基本静的フィールド
  • 基本静的コンストラクター
  • ベースインスタンスフィールド
  • ベースインスタンスコンストラクタ
  • 派生インスタンスコンストラクタ

明らかにアップキャストにより、派生インスタンスコンストラクターが呼び出される前にクラスにアクセスできます(これが、コンストラクターから仮想メソッドを使用しない理由です。コンストラクター/派生クラスのコンストラクターによって初期化されていない派生フィールドに簡単にアクセスできます。派生クラスを「一貫性のある」状態にすることはできませんでした)

6
xanatos

コンストラクターの最後にBをインスタンス化することで、問題を回避できます。

 public A() 
    { 
        Name = "test"; 
        B = new B(this); 
    } 

あなたが提案することが不可能であるならば、Aは不変ではないでしょう。

編集:leppieのおかげで修正されました。

4
Nick

原則は、thisオブジェクトをコンストラクター本体からエスケープさせないことです。

このような問題を観察する別の方法は、コンストラクター内で仮想メソッドを呼び出すことです。

3

前述のように、コンパイラには、オブジェクトがどの時点で十分に構築されているかを知る手段がありません。したがって、コンストラクターからthisを渡すプログラマーは、オブジェクトが自分のニーズを満たすのに十分に構築されているかどうかを知っていると想定しています。

ただし、真に不変であることが意図されているオブジェクトの場合、最終値が割り当てられる前にフィールドの状態を調べるコードにthisを渡さないようにする必要があることを付け加えておきます。これは、thisが任意の外部コードに渡されないことを意味しますが、構築中のオブジェクトがそれ自体を別のオブジェクトに渡すことに問題があることを意味するものではありません。 最初のコンストラクターが完了するまで実際には使用されない後方参照を格納するため

不変オブジェクトの構築と使用を容易にする言語を設計している場合、メソッドを構築中のみ、構築後のみ、またはそのいずれかで使用可能であると宣言すると役立つ場合があります。フィールドは、構築中は逆参照不可であり、その後は読み取り専用であると宣言できます。同様に、パラメータをタグ付けして、参照解除できないようにする必要があることを示すことができます。このようなシステムでは、コンパイラーが相互に参照するデータ構造の構築を許可することは可能ですが、それが観察された後はプロパティが変更されることはありません。このような静的チェックの利点がコストを上回るかどうかについてはわかりませんが、興味深いかもしれません。

ちなみに、役立つ関連機能は、パラメーターと関数の戻り値を一時的、戻り可能、または(デフォルト)永続可能として宣言する機能です。パラメータまたは関数の戻り値が一時的であると宣言された場合、それをどのフィールドにもコピーすることも、永続的なパラメータとしてどのメソッドに渡すこともできませんでした。さらに、エフェメラル値または戻り可能値を戻り可能パラメーターとしてメソッドに渡すと、関数の戻り値はその値の制限を継承します(関数に2つの戻り可能パラメーターがある場合、その戻り値はより制限的な制約を継承しますパラメーター)。 Javaおよび.netの主な弱点は、すべてのオブジェクト参照が無差別であるということです。外部コードが手に入ると、誰がそれを使用するかはわかりません。パラメータを一時的に宣言できる場合、何かへの唯一の参照を保持しているコードが、それが唯一の参照を保持していることを知ることがより頻繁に可能であり、したがって、不必要な防御コピー操作を回避できます。さらに、コンパイラがそれらへの参照がないことを知ることができれば、クロージャなどをリサイクルできます彼らが戻った後に存在した。

1
supercat