В коде ниже один lazy-результат обходят дважды. Сколько раз будет вызвано замыкание преобразования и почему?
let source = [1, 2, 3]
let doubled = source.lazy.map {
print("обработка \($0)")
return $0 * 2
}
print(Array(doubled))
print(Array(doubled))
Замыкание будет вызвано 6 раз: по одному разу для каждого элемента при каждом из двух обходов. Lazy-последовательности не кэшируют результаты преобразований; они выполняют замыкание заново при создании очередного итератора и чтении элементов.
Ленивые адаптеры последовательностей появились как способ строить цепочки преобразований без немедленного создания промежуточных коллекций. Это позволяет обрабатывать только реально запрошенные элементы и снижать лишние выделения памяти.
Обратная сторона такого подхода — результат не является сохранённым снимком. Повторный обход снова запускает вычисления, если базовая последовательность допускает повторный обход.
В примере doubled — это не готовый массив, а объект, описывающий источник и замыкание преобразования. Первый вызов Array(doubled) полностью обходит источник, а второй создаёт новый обход и повторяет работу.
Если замыкание дорогое или имеет побочные эффекты, ошибочное ожидание кэширования может привести к ухудшению производительности или неожиданному повторному выполнению действий. Например, сетевой запрос или запись в журнал внутри такого замыкания могут выполняться многократно.
source.lazy.map { ... } создаёт ленивую последовательность, которая хранит исходную последовательность и функцию преобразования. При запросе очередного элемента она получает элемент источника, применяет замыкание и возвращает результат.
У ленивого результата нет встроенного массива с уже вычисленными значениями. Поэтому каждый вызов Array(doubled) проходит по source и вызывает замыкание для 1, 2 и 3. Всего получается шесть вызовов.
Ленивость не означает однократное вычисление и не гарантирует кэширование. Она означает, что вычисление откладывается до потребления элемента.
Если нужен повторно используемый вычисленный результат, его следует материализовать один раз:
Здесь преобразование выполняется только при создании cached, а последующие обходы читают готовый массив. При этом нужно учитывать, что материализация требует памяти, пропорциональной числу сохранённых элементов.
Повторный обход также зависит от базовой последовательности. Array является многократно обходящейся коллекцией, но произвольный Sequence может быть одноразовым или зависеть от внешнего состояния. Ленивый адаптер не превращает такой источник в повторяемый и не добавляет ему кэширование.
Предположим, приложение фильтрует и преобразует большой массив данных, после чего результат нужен только для первых десяти элементов. Цепочка lazy.filter(...).map(...).prefix(10) позволяет не обрабатывать элементы после найденных первых десяти результатов и не создавать промежуточные массивы.
Вариант с обычными filter и map проще для чтения и удобен, если результат нужен многократно: вычисления выполняются один раз, но создаются промежуточные коллекции и обрабатываются все элементы. Ленивый вариант экономит память и может прекратить работу раньше, но при повторном обходе повторяет вычисления.
Практический выбор: для одноразового потребления большого источника использовать ленивую цепочку, а для многократного доступа к результату — один раз материализовать её в Array. Побочные эффекты в ленивых замыканиях лучше не размещать, поскольку момент и число их выполнений определяются обходами.
Изменится ли результат, если между двумя обходами изменить исходный массив?
Да, если ленивый объект ссылается на изменяемый источник, второй обход может увидеть новое состояние источника. Однако у Array действует семантика значений и copy-on-write: после присваивания и последующей мутации исходного массива ленивый адаптер обычно продолжает видеть тот экземпляр значения, который был захвачен им, а мутируемая переменная получает отдельное хранилище при необходимости.
Остановит ли prefix(2) вычисление всей ленивой цепочки?
Да, обход остановится после получения двух элементов результата prefix, если дальнейшие операции не требуют продолжения. Это одно из главных преимуществ ленивой композиции: потребитель запрашивает только нужное количество элементов, а предыдущие адаптеры вычисляют лишь необходимый префикс.
Гарантирует ли копирование ленивой последовательности независимое состояние обхода?
Нет. Копирование самого значения последовательности не является гарантией независимых уже выполняющихся итераторов. Поведение определяется типом базовой последовательности и её итератора. Для Array обычно можно создать независимые итераторы, но обобщённый код, принимающий только Sequence, не должен полагаться на повторяемость или независимость копий.