Pourquoi est-ce encore valable?
Pourquoi vous attendez-vous à être invalide?
Parce qu'un constructeur doit garantir que le code qu'il contient est exécutée avant à l'extérieur de code peut observer l'état de l'objet.
Correct. Mais le compilateur n'est pas responsable pour le maintien de cet invariant. Vous êtes. Si vous écrivez du code qui rompt que invariant, et ça fait mal quand vous le faites, alors arrêter de le faire.
Existe-il d'autres façons d'observer l'état d'un objet qui n'est pas entièrement construit?
Assurez-vous. Pour les types référence, tous impliquent en quelque sorte de passage "ce" hors du constructeur, évidemment, puisque le seul code d'utilisateur qui contient la référence à l'espace de stockage est le constructeur. D'une certaine manière le constructeur peut fuir "ce" sont:
- Mettre en "ce" dans un champ statique et de référence à partir d'un autre thread
- faire un appel de méthode ou d'un constructeur d'appel et de passer de "ce" comme argument
- faire un appel virtuel -- particulièrement désagréable si la méthode virtuelle est remplacée par une classe dérivée, car alors il s'exécute avant de la classe dérivée ctor corps fonctionne.
J'ai dit que le seul code d'utilisateur qui contient une référence est le ctor, mais bien sûr, le garbage collector est également titulaire d'une référence. Donc, une autre façon intéressante, dans laquelle un objet peut être observé dans un demi-construit de l'état, si l'objet a un destructeur, et le constructeur lève une exception (ou asynchrone exception comme un abandon de thread; plus sur cela plus tard.) Dans ce cas, l'objet est sur le point d'être mort et, par conséquent, doit être finalisé, mais le finaliseur thread peut voir la demi-initialisé à l'état de l'objet. Et maintenant nous sommes de retour dans le code de l'utilisateur qui peut voir la demi-objet construit!
Les destructeurs sont nécessaires pour être robuste face de ce scénario. Un destructeur ne doit pas dépendre d'un invariant de l'objet mis en place par le constructeur de l'entretien, parce que l'objet est détruit pourriez ne jamais avoir été entièrement construite.
Un autre fou façon qu'un demi-objet construit peut être observé par un code externe est bien sûr, si le destructeur voit la demi-initialisé objet dans le scénario ci-dessus, puis copie d'une référence à cet objet à un champ statique, de sorte qu'à moitié construit, demi-finalisation de l'objet est sauvé de la mort. S'il vous plaît ne pas le faire. Comme je l'ai dit, si ça fait mal, ne le faites pas.
Si vous êtes dans le constructeur d'un type de valeur, alors les choses sont fondamentalement les mêmes, mais il y a quelques petites différences dans le mécanisme. La langue exige qu'un appel de constructeur sur une valeur de type crée une variable temporaire que seul le ctor a accès, de muter cette variable, puis une struct copie de la mutation de la valeur pour le stockage réelle. Qui assure que si le constructeur lève, puis le stockage final n'est pas dans un demi-état muté.
A noter que depuis struct copies ne sont pas garantis pour être atomique, il est possible qu'un autre thread pour voir le stockage dans un demi-état muté; utiliser des verrous correctement si vous êtes dans cette situation. Aussi, il est possible pour un asynchrones exception comme un abandon de thread pour être jeté à mi-chemin par le biais d'un struct copie. Ces non-atomicité des problèmes surviennent indépendamment de si la copie est à partir d'un ctor temporaire ou un "régulier" de la copie. Et, en général, très peu d'invariants sont maintenues que si il y a les exceptions asynchrones.
Dans la pratique, le compilateur C# permettra d'optimiser loin l'allocation temporaire et copie si l'on peut déterminer qu'il n'existe aucun moyen pour que le scénario se poser. Par exemple, si la nouvelle valeur est l'initialisation d'un local qui n'est pas fermée par une lambda et non pas dans un itérateur bloc, S s = new S(123);
seulement mute s
directement.
Pour plus d'informations sur la façon dont la valeur de type de constructeurs de travail, voir:
La réfutation d'un autre mythe au sujet des types de valeur
Et pour plus d'informations sur la façon de langage C# sémantique essayer de vous sauver de vous-même, voir:
Pourquoi Ne Initialiseurs D'Exécuter Dans L'Ordre Inverse Que Les Constructeurs? La Première Partie
Pourquoi Ne Initialiseurs D'Exécuter Dans L'Ordre Inverse Que Les Constructeurs? Partie Deux
Il me semble avoir égaré le sujet à la main. Dans une struct bien sûr, vous pouvez observer un objet à moitié construit de la même façon -- copie de la demi-objet construit à un champ statique, appeler une méthode avec "ce" comme argument, et ainsi de suite. (Évidemment, l'appel d'une méthode virtuelle sur un plus type dérivé n'est pas un problème avec les structures.) Et, comme je l'ai dit, la copie de la temporaire pour le stockage final n'est pas atomique et, par conséquent, un autre thread peut observer la demi-copié struct.
Maintenant, nous allons examiner la cause racine de votre question: comment faire pour que les objets immuables qui font référence les uns aux autres?
En général, comme vous l'avez découvert, vous n'avez pas. Si vous avez deux objets immuables qui font référence les uns aux autres, alors, logiquement, ils forment un dirigé cyclique graphique. Vous pourriez envisager de construire simplement un immuable graphe orienté! Cela est assez facile. Immuable graphe orienté est composé de:
- Une liste immuable du immuable nœuds, dont chacun contient une valeur.
- Une liste immuable du immuable du nœud paires, chacun de qui a, le début et la fin d'un graphique à bord.
Maintenant, la façon dont vous faites les nœuds A et B "référence" des uns et des autres est:
A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);
Et vous avez fait, vous avez un graphique où A et B "de référence" les uns les autres.
Le problème, bien sûr, est que vous ne pouvez pas obtenir de B à partir d'Un sans avoir G dans la main. Ayant ce niveau supplémentaire d'indirection pourrait être inacceptable.