Привет.
Когда мы говорили о коллекциях, мы пропустили трансформацию коллекций.
Сегодня мы разберемся с тем, что же это такое.
Для начала просто создадим массив.
Он будет содержать целые числа, которые мы и будем трансформировать.
Самый простой пример,
это пробежаться по массиву и преобразовать все значения в другой массив.
В данном случае мы создаем переменную с пустым массивом,
итерируемся по нашей первоначальной коллекции и добавляем в новый
массив каждый из элементов, умноженный на 3.
Получаем ожидаемый результат, каждое значение из исходной коллекции,
умноженное на 3.
Но данный подход не очень swifty.
Для таких трансформаций лучше всего использовать метод map.
Этот метод объявлен для всех последовательностей.
Он получает на вход замыкание, которое преобразует каждый элемент по очереди.
Получившийся результат возвращается из функции map.
В данном случае мы передаем closure,
который каждый получаемый элемент умножает на 3 и возвращает.
Как видите, полученный результат точно такой же, как и в предыдущем варианте,
но выглядит куда лаконичнее.
Исходный же массив при этом остаётся неизменным.
Попробуем то же самое преобразование, но с массивом из optional integer значений.
В этом случае мы проверяем, является ли значение nil.
Если оно таковым не является, то возвращаем его, умноженное на 3.
Если же значения нет, то возвращаем nil.
Проблема в том, что данный код не может быть скомпилирован,
так как нет возможности точно определить,
возвращает ли у нас этот метод integer или optional integer.
Поэтому нам нужно всего лишь немного помочь компилятору и явно указать
возвращаемый тип, который в данном случае равен OptionalInt.
[БЕЗ_ЗВУКА] Как видите, всё ожидаемо.
Все элементы, которые являлись какими-то числами,
умножены на 3, а nil так и остался nil.
А теперь давайте попробуем трансформировать массив массивов.
Для начала объявим двумерный массив из чисел.
[БЕЗ_ЗВУКА] Добавим map,
в данном случае каждый элемент будет сам по себе являться массивом,
здесь мы это можем видеть.
А теперь добавим еще один map,
чтобы изменить значение элементов вложенного массива.
[БЕЗ_ЗВУКА] В
результате мы получаем ожидаемое значение: двумерный массив целых чисел,
в котором все элементы на 2 больше, чем в изначальном.
Пойдем еще дальше, используем map на массиве optional массивов integer.
[БЕЗ_ЗВУКА] В этом случае мы
можем вместо условия использовать optional chaining, когда выполнение дойдёт до nil,
то map на нём не вызовется, а результатом выполнения этого замыкания будет nil.
Как видите, мы получаем вполне ожидаемый результат.
Там, где в массивах были числа, они остались числами,
но увеличенными на 2, а где массива не было, мы точно так же получаем nil.
Однако нам не всегда нужно такое поведение.
Допустим, мы хотим убрать из итогового массива все nil.
Для этого существует специальный метод flatmap.
Всё, что нам необходимо сделать, это заменить map на flatmap.
[БЕЗ_ЗВУКА] Он получает на вход замыкание,
возвращающее в данном случае optional integer,
но в результате мы получим массив integer, так как все nil просто будут отброшены.
Если же мы применим flatmap к ArrayOfInts, в котором замыкание не может вернуть nil,
то результат будет точно таким же, как и с map.
[БЕЗ_ЗВУКА] Но интереснее ситуация будет,
если использовать flatmap на массиве массивов.
Обратите внимание,
что в результате мы получим просто массив integer, а не массив массивов.
То есть flatmap развернул вложенные массивы.
Это вторая версия данного метода.
Она принимает на вход замыкание, которое возвращает не optional значения,
а sequence.
Затем flatmap элементы из данных последовательностей добавляют в
один результат.
Но у нас нет еще одного перегруженного flatmap,
который одновременно раскрывал бы массивы и отбрасывал nil.
Если мы попробуем
использовать flatmap на массиве,
который содержит в себе optional массивы integer, то в результате получим просто
массив массивов integer, так как наше замыкание возвращает опциональный тип,
и отсутствующие массивы просто будут отброшены.
Если же мы будем возвращать из замыкания не опциональный массив,
то снова заработает вторая версия flatmap, в результате все
элементы объединятся в один массив.
Что делать с nil, нам придется решать самим.
В данном случае, как вы видите, мы просто возвращаем пустой массив,
который никак не повлияет при конкатенации массивов в завершение выполнения flatmap.
Помимо трансформирования элементов, в Swift есть возможность изменить
саму последовательность или получать
измененную копию.
Рассмотрим очень частую операцию — фильтрацию.
Простейший пример — это взять из массива только четные числа.
Для этого мы используем метод filter, а из closure, который мы передаем в данный
метод, возвращаем true, если деление очередного элемента на 2 не имеет остатка.
Конечно же, у нас есть возможность и отсортировать массив.
В Swift есть несколько методов для сортировки.
Sorted by вернет отсортированный массив как копию, а sort by изменит сам массив.
Вообще такое разделение часто встречается в Swift.
Название для методов, изменяющих сам объект, обычно в повелительной форме,
а для методов, возвращающих результат как копию,
обычно он заканчивается на -ed или -ing.
Давайте отсортируем массив в порядке убывания.
Для этого используем метод sorted.
Из замыкания мы должны вернуть true,
если данные элементы расположены по убыванию, то есть уже в той
последовательности, в которой они должны находиться в результирующем массиве.
Вместо передачи closure мы можем передавать самую функцию для сравнения,
как параметр, а так как в Swift операторы — это тоже функция,
то вызов сокращается для передачи одного символа.
Как видите, мы получаем тот же самый результат,
а вызов метода значительно сокращен.
У sort и sorted есть ещё по одной перегруженной версии.
Она не принимает никаких параметров,
по умолчанию используется метод для сравнения меньше,
то есть коллекция будет отсортирована по возрастанию.
Перейдем к более интересному методу reduce.
Он принимает два параметра: начальное значение и замыкание.
Далее каждый элемент и промежуточные значения передают по очереди в данное
замыкание, она производит какие-то вычисления над ними и возвращает новое
промежуточное значение.
То есть в результате мы получим один элемент вместо целой коллекции.
Reduce можно использовать, например, чтобы сложить все элементы в массиве,
что мы и продемонстрировали в данном случае.
Начальное значение у нас 0, а при каждом попадании в переданный нами
closure с новым элементом из коллекции, мы добавляем его к промежуточному результату.
Соответственно, на первый элемент из коллекции мы получим 0,
а когда данный closure будет вызван на втором элементе из коллекции, там уже
будет сумма всех элементов до него, то есть значение первого из элементов.
Так же, как в примере с sorted, мы можем просто передать
функцию в reduce.
В данном случае мы передаем плюс.
Как видите, получаем тот же самый результат, что и написав целый closure.
Reduce — это очень гибкий метод.
Вовсе не обязательно, чтобы результат его выполнения был такого же типа,
как и элемент исходного массива.
Он может быть, например, сам массивом.
Давайте напишем фильтрацию, используя reduce.
В качестве начального значения у нас будет пустой массив.
Далее мы проверяем, четный ли очередной элемент.
Если это так, то добавляем его с помощью конкатенации массивов.
Если он нечетный, то возвращаем полученный промежуточный массив.
У данной реализации есть один недостаток.
Каждый раз при добавлении нового элемента создается новый массив,
и это очень замедляет выполнение метода.
В Swift 4 добавлена новая версия reduce,
а именно reduce into.
Она передает промежуточное значение по ссылке.
Это позволяет избежать ненужного копирования.
Среди изменения в Swift 4 также есть измененный метод swapAt.
Раньше он принимал сами объекты для обмена по указателям,
а теперь же принимает и их индексы.
[БЕЗ_ЗВУКА] Это
сделано ради предотвращения конфликтов при одновременном доступе к контейнеру.
Можно подробнее об этом прочитать в Ownership Manifesto.
Но лучше всего методы для трансформации раскрывают себя при использовании подряд
сразу нескольких таких трансформаций.
Их можно дополнительно совмещать с методами для трансформации
последовательностей, возвращающихся в sequence,
таких как drop(while:) или prefix.
[БЕЗ_ЗВУКА] В данном примере
воспользуемся одним из вариантов функции sequence, который еще называется unfold.
Это заимствование из функциональных языков программирования.
Называют его так потому,
что один элемент разворачивается в целую последовательность.
В противовес есть метод reduce, который также называется fold.
Он сворачивает последовательность нескольких элементов в один.
Создадим последовательность возрастающих случайных чисел.
Обратите внимание, что это не массив, а последовательность.
При выполнении данной строки ещё ни одного числа не сгенерировано.
Это произойдёт только при обращении к данной последовательности.
Возьмем первые 100 элементов последовательности и осуществим над ними
несколько операций.
Далее же проитерируемся по всем элементам сгенерированной
последовательности и распечатаем их в консоль.
Какой-то определенной задачи данный алгоритм не решает, но позволяет увидеть,
какие возможности нам предоставляют методы для трансформации коллекций.
На этом всё, и не забывайте один из важнейших принципов Swift: Clarity
is more important than brevity.
Несмотря на то, что код, написанный на Swift, может быть кратким,
это не является самоцелью.
В первую очередь код должен быть понятным для человека, читающего его.