1. ホーム
  2. c#

お互いを参照し合う不変のオブジェクト?

2023-10-12 21:42:23

質問

今日、私はお互いを参照するイミュータブルなオブジェクトについて、頭を整理しようとしていました。私は、遅延評価を使用せずにそれを行うことは不可能であるという結論に達しましたが、その過程で、私はこの(私の意見では)興味深いコードを書きました。

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のオブジェクトを観察する他の方法が思いつかないということです。なぜこれが有効なのでしょうか?完全に構築されていないオブジェクトの状態を観察する他の方法はあるのでしょうか?

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

<ブロッククオート

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

なぜ無効であることを期待しているのですか?

<ブロッククオート

コンストラクタは、外部のコードがオブジェクトの状態を観察する前に、それが含むコードが実行されることを保証することになっているからです。

その通りです。しかし コンパイラが はその不変性を維持する責任はありません。 あなたは . もし、その不変性を破るようなコードを書いて、それを実行すると痛い目にあうのであれば をやめてください。 .

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

参照型の場合、ストレージへの参照を保持する唯一のユーザーコードはコンストラクタであるため、明らかにコンストラクタから "this" を何らかの形で渡すことが必要です。コンストラクタが "this"を漏らすことができるいくつかの方法があります。

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

私は、唯一の ユーザー コード が参照を保持するのは ctor だけですが、もちろん ガベージコレクタ も参照を保持します。したがって、オブジェクトが半構築状態であることが観察できるもうひとつの興味深い方法は、オブジェクトがデストラクタを持ち、コンストラクタが例外を投げる場合です(またはスレッドアボートのような非同期例外を取得する場合。そして今、私たちは半分構築されたオブジェクトを見ることができるユーザー コードに戻りました!

デストラクタはこのシナリオに直面して堅牢であることが要求されます。

破壊されるオブジェクトは完全に構築されていないかもしれないので、デストラクタはコンストラクタによって設定されたオブジェクトの不変量に依存してはいけません。

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

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

構造体コピーはアトミックであることが保証されていないので、次のことに注意してください。 そのような状況に陥った場合は、ロックを正しく使用してください。また、構造体コピーの途中でスレッドのアボートのような非同期例外がスローされる可能性があります。これらの非原理性の問題は、コピーがCTOR一時的なものであるか、quot;通常のコピーであるかに関係なく発生します。また、一般に、非同期例外が発生した場合、不変量はほとんど維持されません。

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

値型コンストラクタの動作の詳細については

値型に関するもうひとつの神話を否定する

また、C#の言語セマンティクスがどのようにあなたを救おうとしているのかについては

なぜ初期化子はコンストラクタと逆の順序で実行されるのか?パート 1

なぜ初期化子はコンストラクタと逆の順序で実行されるのですか?パート 2

本題から外れてしまったようです。半構築オブジェクトを静的フィールドにコピーしたり、引数に "this"を指定してメソッドを呼び出すなどです。(もちろん、構造体の場合は、より派生した型の仮想メソッドを呼び出しても問題ありません)。 また、先ほども言ったように、一時記憶域から最終記憶域へのコピーはアトミックではないため、別のスレッドが半分コピーされた構造体を観察することができます。


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

一般的に、あなたが発見したように、そうではありません。もし、お互いを参照する 2 つの不変オブジェクトがあれば、論理的には、それらは 有向循環グラフ . このような場合、単純にイミュータブル有向グラフを作成することを考えるかもしれません。 これは非常に簡単である。不変の有向グラフは以下のような構成になっています。

  • 各ノードが値を含む、不変のリスト。
  • イミュータブルなノードペアのリストで、それぞれがグラフエッジの始点と終点を持つ。

さて、ノードAとBを"reference"させる方法は。

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

AとBが互いに"reference"するグラフが出来上がりましたね。

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