Разберите вызов изменяющего метода через existential-значение. Какое число напечатает код и почему изменение сохраняется?
Код напечатает 1. Переменная counter имеет тип any Counter, но внутри хранит конкретное значение Score; вызов mutating-метода изменяет это значение внутри existential-контейнера, поэтому последующее чтение value видит обновлённое состояние.
Existential-типы нужны, когда код должен работать со значением неизвестного конкретного типа, но известного по набору требований протокола. Это позволяет хранить разные conforming-типы в одной переменной и обращаться к их общему интерфейсу.
Проблема особенно заметна для структур: они имеют value semantics, поэтому изменение метода должно происходить над самим хранимым значением, а не над временной копией, которая сразу потеряется.
any Counter не сообщает компилятору конкретный тип значения: это может быть Score или другая структура, реализующая Counter. При этом протокол разрешает вызов mutating-метода, который потенциально заменяет или изменяет хранимое значение.
Неверное предположение состоит в том, что existential всегда передаёт значение методу как независимую копию. Если бы это было так, counter.increment() не имел бы наблюдаемого эффекта при последующем чтении counter.value.
Existential-контейнер хранит конкретное значение вместе с информацией, необходимой для вызова реализаций протокола. Представление может использовать встроенное хранилище или косвенное размещение; полагаться на конкретный способ хранения нельзя.
При вызове counter.increment() Swift предоставляет реализации доступ к изменяемому содержимому existential-контейнера. Метод Score.increment() увеличивает поле value, после чего обновлённое значение остаётся внутри counter.
Ключевое различие — между var и let. Для var counter контейнер можно изменить, поэтому через него разрешён вызов mutating. Если existential объявить через let, вызов такого метода будет запрещён: изменение значения потребовало бы изменить неизменяемую переменную.
Existential не раскрывает конкретный тип наружу. Например, нельзя напрямую обратиться к свойству, которого нет в Counter, или использовать конкретные API Score, пока значение явно не приведено к известному типу. При этом вызов требований протокола остаётся доступным через таблицу соответствия протоколу.
Предположим, приложение хранит разные счётчики: локальный struct, счётчик с ограничением и счётчик, загруженный из конфигурации. Массив [any Counter] позволяет обрабатывать их единообразно, но конкретный тип каждого элемента заранее неизвестен.
Вариант с конкретным обобщённым типом сохраняет больше статической информации и обычно лучше подходит для алгоритма, работающего с одним типом. Его недостаток — нельзя без дополнительной архитектуры поместить разнотипные реализации в одну коллекцию.
Вариант с классом-обёрткой даёт ссылочную семантику и удобное совместное изменение состояния, но добавляет уровень косвенности и меняет правила копирования. any Counter выбран, когда нужна гетерогенная коллекция и достаточно только требований протокола; изменения mutating-методов сохраняются в каждом existential-элементе.
Вопрос: Можно ли вызвать mutating-метод через let counter: any Counter?
Ответ: Нет. mutating означает, что метод может изменить значение, на котором он вызван. Переменная, объявленная через let, неизменяема, поэтому компилятор запрещает такой вызов независимо от того, хранит existential структуру или другое значение с value semantics.
Вопрос: Станут ли два existential-значения общими по состоянию, если оба содержат одну и ту же структуру?
Ответ: Нет, простое присваивание значения, реализующего протокол, сохраняет value semantics структуры. Например, var second = counter создаёт независимое значение на уровне семантики Swift; изменение second не обязано изменять counter. Общим состояние может быть только через ссылочное свойство внутри структуры или если конкретная реализация использует ссылочный тип.
Вопрос: Почему нельзя объявить mutating-метод в протоколе и затем вызвать его у значения, полученного как let copy = counter?
Ответ: После присваивания copy становится отдельной неизменяемой переменной, даже если исходный counter был объявлен через var. Вызов метода рассматривается относительно copy, а не исходной переменной, поэтому изменение existential-контейнера запрещено. Нужно объявить copy через var, если изменение значения через требования протокола допустимо.