Чому це взагалі діє?
Чому ви вважаєте, що він буде недійсним?
Оскільки конструктор повинен гарантувати, що код, який він містить, виконується до того, як зовнішній код зможе спостерігати за станом об'єкта.
Правильно. Але компілятор не відповідає за підтримку цього інваріанта. Ви є . Якщо ви пишете код, який розбиває цей інваріант, і боляче, коли ви це робите, тоді припиніть це робити .
Чи існують інші способи спостереження за станом об’єкта, який не повністю побудований?
Звичайно. Що стосується типів посилань, усі вони передбачають якимось чином передавати "це" з конструктора, очевидно, оскільки єдиним кодом користувача, який містить посилання на сховище, є конструктор. Деякі способи конструктора можуть просочити "це":
- Помістіть "це" у статичне поле та посилайтеся на нього з іншого потоку
- зробити виклик методу або виклик конструктора і передати "це" як аргумент
- здійснити віртуальний виклик - особливо неприємно, якщо віртуальний метод замінено похідним класом, оскільки тоді він виконується до того, як запуститься тіло ctor похідного класу.
Я сказав, що єдиним кодом користувача, який містить посилання, є ctor, але, звичайно, збирач сміття також містить посилання. Отже, ще один цікавий спосіб, за яким можна спостерігати, що об’єкт перебуває в напівконструйованому стані, це якщо об’єкт має деструктор, і конструктор видає виняток (або отримує асинхронний виняток, наприклад, переривання потоку; про це пізніше. ) У такому випадку об’єкт повинен бути мертвим, і тому його потрібно доопрацювати, але потік фіналізатора може бачити напівініціалізований стан об’єкта. І ось ми знову в користувацькому коді, який може бачити напівконструйований об’єкт!
Деструктори повинні бути надійними в умовах цього сценарію.Деструктор не повинен залежати від будь-якого інваріанта об'єкта, встановленого підтримуваним конструктором, оскільки об'єкт, який руйнується, ніколи не був повністю побудований.
Ще один божевільний спосіб того, як напівконструйований об'єкт може спостерігатися зовнішнім кодом, звичайно, якщо деструктор бачить напівініціалізований об'єкт у сценарії вище, а потім копіює посилання на цей об'єкт у статичне поле, забезпечуючи тим самим, що половина -побудований, напівзавершений об'єкт рятується від смерті. Будь ласка, не роби цього. Як я вже сказав, якщо боляче, не роби цього.
Якщо ви знаходитесь у конструкторі типу значення, то в основному все однаково, але в механізмі є деякі невеликі відмінності. Мова вимагає, щоб виклик конструктора для типу значення створював тимчасову змінну, до якої має доступ лише ctor, мутував цю змінну, а потім виконував структурну копію мутованого значення у фактичне сховище. Це гарантує, що якщо конструктор кидає, остаточне сховище не знаходиться в напівмутованому стані.
Зверніть увагу , що оскільки Struct копія не гарантована атомарним, є можливо для іншого потоку , щоб побачити зберігання в половинному мутували стан; правильно використовуйте замки, якщо ви потрапили в таку ситуацію. Крім того, можливо, щоб асинхронний виняток, як переривання потоку, був перекинутий наполовину через структурну копію. Ці проблеми з неатомністю виникають незалежно від того, чи є копія тимчасовою чи "звичайною" копією. І взагалі, дуже мало інваріантів зберігається, якщо є асинхронні винятки.
На практиці компілятор C # оптимізує тимчасове розподіл і скопіює, якщо зможе визначити, що такий сценарій не може виникнути. Наприклад, якщо нове значення ініціалізує локальний, який не закритий лямбда-ланцюгом, а не в блоці ітератора, тоді S s = new S(123);
просто мутує s
безпосередньо.
Для отримання додаткової інформації про те, як працюють конструктори типу значення, див .:
Розвінчання чергового міфу про типи цінностей
Докладніше про те, як семантика мови C # намагається врятувати вас від себе, див .:
Чому ініціалізатори працюють у протилежному порядку як конструктори? Частина перша
Чому ініціалізатори працюють у протилежному порядку як конструктори? Частина друга
Здається, я відхилився від розглянутої теми. У структурі ви, звичайно, можете спостерігати за тим, як об'єкт, який повинен бути побудований наполовину, однаково - скопіюйте напівконструйований об'єкт у статичне поле, викликайте метод з аргументом "this" тощо. (Очевидно, що виклик віртуального методу для більш похідного типу не є проблемою зі структурами.) І, як я вже сказав, копія з тимчасового до кінцевого сховища не є атомною, і тому інший потік може спостерігати напівкопійовану структуру.
А тепер давайте розглянемо першопричину вашого запитання: як ви робите незмінні об’єкти, які посилаються один на одного?
Як правило, як ви виявили, ні. Якщо у вас є два незмінні об'єкти, які посилаються один на одного, то логічно вони утворюють спрямований циклічний графік . Ви можете подумати просто створити незмінний спрямований графік! Зробити це досить просто. Незмінний спрямований графік складається з:
- Незмінний список незмінних вузлів, кожен з яких містить значення.
- Незмінний список незмінних пар вузлів, кожна з яких має початкову та кінцеву точки ребра графіка.
Тепер, як ви робите вузли А і В "посиланнями" один на одного:
A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);
І все готово, у вас є графік, де А і В "посилаються" одне на одного.
Проблема, звичайно, полягає в тому, що ви не можете дістатися до точки В з точки А, не маючи в руках G. Наявність такого додаткового рівня опосередкованості може бути неприйнятною.