Если после создания ленивой цепочки над Array изменить исходный массив, какие элементы увидит последующий обход?
Последующий обход увидит состояние массива на момент создания ленивой цепочки, если после этого изменялась структура исходного Array. Это связано с семантикой значений и copy-on-write: цепочка хранит собственную копию значения Array, а физическое копирование буфера откладывается до мутации.
Если элементы массива являются ссылочными объектами, изменение состояния самого объекта может быть видно из ленивой цепочки: копируется контейнер, но не объекты, на которые он ссылается.
В Swift коллекции вроде Array имеют семантику значений: присваивание создаёт логически независимое значение. Чтобы не копировать весь буфер сразу, Swift использует механизм copy-on-write — физическое копирование выполняется только при изменении разделяемого хранилища.
Ленивые адаптеры Sequence и Collection решают другую задачу: они откладывают вычисление преобразований до фактического обхода и не создают промежуточные коллекции. Поэтому важно понимать, какое значение-источник хранит такая цепочка и как оно взаимодействует с copy-on-write.
Представим, что цепочка создана из массива, но обход выполняется позже. За это время исходный массив может получить новые элементы, удалить существующие или изменить значения по индексам.
Ошибочное предположение, что ленивое вычисление всегда читает текущий исходный массив, может привести к несогласованным результатам. В частности, разработчик может ожидать, что добавленный после создания цепочки элемент будет обработан, хотя структура источника уже отделена от сохранённого значения.
При создании ленивой цепочки над Array в неё передаётся значение массива. Сначала исходный массив и сохранённое в цепочке значение могут совместно использовать один буфер, но структурная мутация исходного массива запускает copy-on-write.
После такой мутации исходный массив получает новый буфер, а цепочка продолжает ссылаться на прежнее значение. Поэтому добавленные или удалённые элементы исходного массива не влияют на последующий обход цепочки.
Замыкание map всё равно выполняется лениво: выражения преобразования вызываются только во время Array(doubled). Однако ленивость не означает чтение изменённой переменной source; цепочка хранит переданное ей значение исходной коллекции.
Есть важное различие между изменением контейнера и изменением ссылочного элемента. Если массив содержит экземпляры класса, copy-on-write разделит массив, но не клонирует объекты. Поэтому изменение свойства объекта может быть видно через оба массива и через ленивую цепочку.
Следует также отличать создание цепочки от самого обхода. Если источник — не Array, а пользовательская Sequence с особой семантикой итератора, поведение может зависеть от того, как эта Sequence хранит состояние. Гарантия снимка в данном объяснении относится к сохранённому значению Array и его семантике copy-on-write.
Сервис формирует ленивую цепочку обработки большого массива записей, затем добавляет в исходный массив новые записи и ожидает, что они попадут в выгрузку.
Можно выполнять обход непосредственно исходного массива после всех изменений. Это наиболее очевидный вариант, но он не подходит, если цепочка уже передана в другой компонент и должна сохранять исходный набор данных.
Другой вариант — использовать ленивую цепочку, созданную до мутации. Он обеспечивает логический снимок исходного Array и избегает немедленного создания промежуточных массивов. Цена — новые элементы в исходном массиве не попадут в результат, поэтому момент создания цепочки становится частью контракта.
Практическое решение — явно выбрать требуемую семантику: сначала завершить все изменения, затем создать цепочку, если нужен актуальный набор; либо создать цепочку заранее, если нужен стабильный снимок. Для массивов ссылочных объектов дополнительно нужно решить, достаточно ли снимка контейнера или требуется независимое копирование самих объектов.
Дополнительный вопрос: изменится ли результат, если после создания цепочки изменить существующий элемент массива?
Если элемент — значимый тип, например Int или структура, структурное изменение исходного массива вызывает copy-on-write, и цепочка продолжит видеть старое значение элемента. Если элемент — экземпляр класса, изменение его свойства обычно будет видно через цепочку, потому что копируется массив ссылок, а не объекты.
Дополнительный вопрос: означает ли lazy, что результат всегда отражает самое позднее состояние источника?
Нет. lazy откладывает вычисление, но не отменяет семантику хранения базовой последовательности. Для Array ленивый адаптер хранит значение массива, поэтому структурные изменения исходной переменной после создания адаптера не становятся изменениями сохранённого значения.
Дополнительный вопрос: когда ленивую цепочку всё же следует материализовать в отдельную коллекцию?
Материализация оправдана, если результат нужно многократно обходить с предсказуемой стоимостью, сохранить как независимый набор данных или передать API, которому нужна конкретная коллекция. Ленивый вариант полезнее, когда нужен один проход, можно остановить обработку досрочно или важно избежать промежуточных массивов. Компромисс заключается в том, что ленивый обход переносит вычисления на момент потребления и может повторно выполнять замыкания при повторных обходах.