Why is this even true?
Why do you expect it to be invalid?
Because the constructor must ensure that the code that it contains is executed before the external code can observe the state of the object.
Right. But the compiler is not responsible for maintaining this invariant. You. If you write code that breaks this invariant, and it hurts when you do this, stop doing it.
Are there other ways to monitor the state of an object that is not fully constructed?
Sure. For reference types, they are all related to the fact that they pass "this" from the constructor, obviously, since the only user code that contains a reference to the repository is the constructor. Some of the ways that the constructor of this can proceed are:
- Put "this" in a static field and access it from another thread
- make a method call or constructor call and pass "this" as an argument
- making a virtual call is especially frustrating if the virtual method is overridden by the derived class, because it is executed before the derived ctor body is executed.
I said that the only user code that contains the link is ctor, but of course the garbage collector also contains the link. Thus, another interesting way of observing an object in a semi-constructed state is that the object has a destructor, and the constructor throws an exception (or receives an asynchronous exception, for example, interrupting a stream, more on this later). ) In this case, the object must be dead and, therefore, must be completed, but the stream of the finalizer can see the semi-initialized state of the object. And now we are back in the user code that can see the semi-designed object!
Destructors must be reliable in the face of this scenario. The destructor should not depend on any invariant of the object set by the supported constructor, since the destroyed object may never have been completely built.
In another crazy way that a semi-constructed object can be detected by external code, of course, if the destructor sees a semi-initialized object in the above scenario, and then copies the link to this object to a static field, thereby ensuring that the semi-constructed, semi-finished object is saved from death. Please, do not do that. As I said, if it hurts, don’t do it.
If you are in a value type constructor, then everything is basically the same, but there are slight differences in the mechanism. The language requires that calling the constructor on the value type creates a temporary variable that only ctor has access to, mutates that variable and then makes a structural copy of the changed value in the actual storage. This ensures that if the constructor throws, then the final repository is not in a semi-mutated state.
Note that since copies of the structure are not guaranteed to be atomic, it is possible that another thread might see the store in a half-mutated state; Use locks correctly if you are in this situation. In addition, an asynchronous exception, such as a thread interrupt, can be selected half through a copy of the structure. These atomicity problems occur whether the copy is a temporary or temporary copy. In general, very few invariants are supported if there are asynchronous exceptions.
In practice, the C # compiler optimizes time distribution and copy if it can determine that there is no way for this scenario to occur. For example, if a new value initializes a local one that is not closed by a lambda, and not in an iterator block, then S s = new S(123); mutates directly s .
For more information on how value type constructors work, see:
Destruction of another myth about value types
And for more information on how C # language semantics tries to save you from you, see:
Why do initializers work in the opposite order as constructors? Part one
Why do initializers work in the opposite order as constructors? Part two
I seem to be off topic. In the structure, you can, of course, observe that the object must be semi-constructed in the same way - copy the semi-constructed object into a static field, call the method with "this" as an argument, and so on. (Obviously, invoking a virtual method for a more derived type is not a problem for structures.) And, as I said, a copy from temporary to final storage is not atomic, and therefore another thread can observe a semi-copied structure.
Now consider the reason for your question: how do you create immutable objects that reference each other?
Usually, as you find out, you do not. If you have two immutable objects that reference each other, then logically they form a directed cyclic graph. You can simply build an immutable directed graph! This is pretty easy to do. A continuous directed graph consists of:
- An optional list of immutable nodes, each of which contains a value.
- An optional list of immutable node pairs, each of which has a start and end point on a graph edge.
Now the way that you create nodes A and B "reference" each other:
A = new Node("A"); B = new Node("B"); G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);
And you are done, you have a graph where A and B "link" to each other.
The problem, of course, is that you cannot get to B from A without G in your hand. The presence of this additional level of indirection may not be acceptable.