Мы уже встречались с понятием ленивых вычислений и даже немного «пощупали» их руками. В этой главе пойдем дальше и углубим свои знания в данной теме.
«Ленивый» в Swift звучит как lazy. Можно сказать, что lazy — синоним производительности. Хорошо оптимизированные программы практически всегда используют ленивые вычисления. Возможно, вы работали с ними и в других языках. В любом случае, внимательно изучите приведенный далее материал.
В программировании ленивыми называются такие элементы, вычисление значений которых откладывается до момента обращения к ним. Таким образом, пока значение не потребуется и не будет использовано, оно будет храниться в виде сырых исходных данных. С помощью ленивых вычислений достигается экономия процессорного времени, то есть компьютер не занимается ненужными в данный момент вычислениями.
Существует два типа ленивых элементов:
• lazy-by-name — значение элемента вычисляется при каждом доступе к нему;
• lazy-by-need — элемент вычисляется один раз при первом обращении к нему, после чего фиксируется и больше не изменяется.
Swift позволяет работать с обоими типами ленивых элементов, но в строгом соответствии с правилами.
С помощью замыканий мы можем создавать ленивые конструкции типа lazy-by-name, значение которых высчитывается при каждом обращении к ним.
Рассмотрим пример из листинга 18.1.
Листинг 18.1
var arrayOfNames = ["Helga", "Bazil", "Alex"]
print(arrayOfNames.count)
let nextName = { arrayOfNames.remove(at: 0) }
arrayOfNames.count // 3
nextName()
arrayOfNames.count // 2
В константе nextName хранится замыкание, удаляющее первый элемент массива arrayOfNames. Несмотря на то что константа объявлена, а ее значение проинициализировано, количество элементов массива не уменьшается до тех пор, пока не произойдет обращение к хранящемуся в ней замыканию.
Если пойти дальше, то можно сказать, что любая функция или метод являются lazy-by-name, так как их значение высчитывается при каждом обращении.
Некоторые конструкции языка Swift (например, массивы и словари) имеют свойство lazy, позволяющее преобразовать их в ленивые. Наиболее часто это происходит, когда существуют цепочки вызова свойств или методов и выделение памяти и вычисление промежуточных значений является бесполезной тратой ресурсов, так как эти значения никогда не будут использованы.
Рассмотрим следующий пример: существует массив целых чисел, значения которого непосредственно не используются в работе программы. Вам требуется лишь результат его обработки методом map(_:), и то не в данный момент, а позже (листинг 18.2).
Листинг 18.2
var baseCollection = [1,2,3,4,5,6]
var myLazyCollection = baseCollection.lazy
type(of:myLazyCollection) // LazyCollection<Array<Int>>.Type
var collection = myLazyCollection.map{$0 + 1}
type(of:collection) // LazyMapCollection<Array<Int>, Int>.Type
В результате выполнения возвращается ленивая коллекция. При этом память под отдельный массив целочисленных значений не выделяется, а вычисления метода map(_:) не производятся до тех пор, пока не произойдет обращение к переменной collection.
Вся прелесть такого подхода в том, что вы можете увеличивать цепочки вызовов, но при этом лишнего расхода ресурсов не будет (листинг 18.3).
Листинг 18.3
var resultCollection = [1,2,3,4,5,6].lazy.map{$0 + 1}.filter{$0 % 2
== 0}
Array(resultCollection) // [2, 4, 6]