Компромиссы ленивого и немедленного вычисления
Понимайте, когда ленивые вычисления помогают, а когда мешают.
«Компромиссы ленивого и немедленного вычисления» — бесплатный урок Swift Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.
Бесплатных обедов не бывает
Ленивое выполнение не всегда быстрее. Оно заменяет предварительное выделение памяти накладными расходами при каждом доступе и меняет момент выполнения побочных эффектов. Знание этих компромиссов позволяет сделать правильный выбор.
Ленивое выполнение выигрывает: частичное потребление
Нужна только часть огромного преобразования — ленивое выполнение пропускает остальное:
let data = Array(1...1_000_000)
let top = data.lazy.map { $0 * 2 }.prefix(3)
print(Array(top)) // [2, 4, 6] -- 999,997 transforms skippedПредварительное вычисление выигрывает: многократное использование
Если вы много раз перебираете результат, предварительное вычисление выполняется один раз, а ленивое выполнение пересчитывает его при каждом проходе:
let doubled = [1, 2, 3].map { $0 * 2 } // computed once
print(doubled.reduce(0, +)) // 12
print(doubled.max()!) // 6 -- no recomputeЛенивое выполнение пересчитывает данные
Одно и то же ленивое представление снова выполняет работу при каждой итерации:
var work = 0
let view = [1, 2, 3].lazy.map { (n: Int) -> Int in work += 1; return n }
_ = view.reduce(0, +)
_ = view.max()
print(work) // 6 -- transformed twiceНакладные расходы на каждый элемент
Каждый ленивый шаг оборачивает последовательность в ещё один тип и вызывает closures для каждого элемента. Для небольших массивов эти расходы могут превысить выигрыш от сэкономленного выделения памяти.
Для небольшого массива подойдёт предварительное вычисление
Если элементов немного и все они используются, выбирайте обычное предварительное вычисление — оно проще, а выделение памяти несущественно:
let nums = [3, 1, 2]
let sorted = nums.map { $0 + 1 }.sorted()
print(sorted) // [2, 3, 4]Момент побочных эффектов различается
При ленивом выполнении тело closure запускается позже строки, в которой оно определено. Не полагайтесь на побочные эффекты внутри ленивых преобразований:
var log: [Int] = []
let v = [1, 2].lazy.map { (n: Int) -> Int in log.append(n); return n }
print(log) // [] -- nothing ran yet
_ = Array(v)
print(log) // [1, 2]Операции, требующие предварительного вычисления
Некоторым операциям в любом случае приходится читать всю последовательность, поэтому ленивое выполнение здесь не даёт преимуществ:
let nums = Array(1...10)
print(nums.lazy.map { $0 * 2 }.sorted()) // sorted must see all -> [2,4,...,20]
print(nums.lazy.map { $0 }.count) // count walks everythingЛенивое выполнение и contains
Операции с ранним завершением, такие как contains(where:), особенно полезны в ленивом режиме для больших входных данных:
let big = Array(1...1_000_000)
let found = big.lazy.map { $0 * 2 }.contains { $0 == 8 }
print(found) // true -- stops at element 4Краткий список критериев выбора
Используйте ленивое выполнение, когда: источник большой, вы сохраняете только его часть (prefix / first / contains) и перебираете результат один раз. Используйте предварительное вычисление, когда: массив небольшой, результат используется повторно или удобство чтения важнее микрооптимизации.
let huge = Array(1...100_000)
let first = huge.lazy.filter { $0 % 9973 == 0 }.first
print(first!) // 9973Измеряйте, а не гадайте
Интуитивные оценки производительности часто ошибочны. Если это важно, сравните обе версии на реалистичных данных, прежде чем выбирать ленивое выполнение.
let nums = Array(1...50)
let eager = nums.filter { $0 > 25 }.count
let lazyC = nums.lazy.filter { $0 > 25 }.count
print(eager == lazyC) // true -- same answer, profile for speedБыстрая проверка
Когда eager лучше выбрать вместо ленивого выполнения?
Итоги
Вы узнали о компромиссах:
- Ленивое выполнение выигрывает для больших источников при частичном потреблении за один проход
- Предварительное вычисление выигрывает для небольших массивов и повторно используемых результатов (ленивое выполнение пересчитывает данные)
- Побочные эффекты в ленивых преобразованиях выполняются позже — не полагайтесь на них
- Перед оптимизацией проводите измерения
Далее: создание собственных ленивых последовательностей.
Часто задаваемые вопросы
Урок «Компромиссы ленивого и немедленного вычисления» бесплатный?
Да — полный текст урока «Компромиссы ленивого и немедленного вычисления» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.
Чему я научусь в уроке «Компромиссы ленивого и немедленного вычисления»?
Понимайте, когда ленивые вычисления помогают, а когда мешают. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Swift Academy?
Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Компромиссы ленивого и немедленного вычисления»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Swift Academy?
Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Свойство lazy
- Ленивые map и filter
- Компромиссы ленивого и немедленного вычисления
- Создание собственных ленивых последовательностей