map, filter, reduce
Представьте повара у ленты с продуктами. Вы отдаёте ему карточку с рецептом, и он применяет её к каждому продукту по очереди. Как идти вдоль ленты, повар знает сам, а вы решаете только, что сделать с одним продуктом.
С массивами чаще всего решают три задачи:
- превратить каждый элемент во что-то другое;
- оставить только подходящие элементы;
- собрать все элементы в одно значение.
Каждую можно решить циклом for-in, но для каждой в Swift есть готовый метод. Метод сам обходит массив, а что делать с элементом, вы говорите ему замыканием:
let prices = [120, 45, 300, 80]
let doubled = prices.map { $0 * 2 }
let cheap = prices.filter { $0 < 100 }
let total = prices.reduce(0, +)
print(doubled) // [240, 90, 600, 160]
print(cheap) // [45, 80]
print(total) // 545Все три метода — функции высшего порядка: они принимают функцию. Разберём каждый.
Три метода
map — превратить каждый элемент
map вызывает замыкание для каждого элемента и собирает результаты в новый массив той же длины. prices.map { $0 * 2 } даёт каждую цену, умноженную на два.
Тип элементов при этом может поменяться. Замыкание { "\($0) ₽" } превратило бы массив чисел в массив строк.
filter — оставить подходящие
Замыкание для filter возвращает Bool. Элемент попадает в новый массив, если замыкание вернуло true, и пропускается, если вернуло false. prices.filter { $0 < 100 } оставляет цены меньше ста — [45, 80]. Порядок элементов сохраняется.
reduce — собрать в одно значение
reduce(0, +) получает два аргумента:
- начальное значение
0; - функцию, которая соединяет накопленное значение со следующим элементом; здесь это оператор
+.
Давайте посмотрим, как reduce проходит по prices:
- Начало —
0. 0 + 120даёт120.120 + 45даёт165.165 + 300даёт465.465 + 80даёт545— это и есть результат.
Запись prices.reduce(0) { $0 + $1 } делает то же самое: в замыкании $0 — накопленное значение, а $1 — очередной элемент.
Здесь картинка с поваром неточна. Повар режет те самые продукты, что лежат на ленте. А map, filter и reduce исходный массив не трогают: prices после них остаётся прежним.
Цепочка вызовов
map и filter возвращают новый массив, поэтому следующий метод можно вызвать прямо у результата. Порядок в цепочке важен:
let a = prices.filter { $0 < 100 }.map { $0 * 2 }
let b = prices.map { $0 * 2 }.filter { $0 < 100 }
print(a, b) // [90, 160] [90]Разберём обе цепочки:
- В
aсначала работаетfilterи оставляет[45, 80], а потомmapудваивает их:[90, 160]. - В
bсначалаmapудваивает все цены:[240, 90, 600, 160]. Потомfilterоставляет из них числа меньше ста:[90].
Цепочку читают слева направо. Каждый следующий метод работает с результатом предыдущего, а не с исходным массивом.
Этот вопрос часто задают на собеседовании: чем отличаются map, filter и reduce? Ответ:
mapвозвращает массив той же длины, где каждый элемент преобразован;filterвозвращает массив из тех элементов, для которых замыкание вернулоtrue;reduceвозвращает одно значение, собранное из всех элементов.
Частая ошибка
Хотели убрать из массива отрицательные температуры и написали в filter условие для них:
let temps = [3, -2, 7, -5]
let positive = temps.filter { $0 < 0 } // ✗
print(positive) // [-2, -5]Ошибки нет ни при компиляции, ни при запуске, но в positive оказались как раз отрицательные числа. filter оставляет элементы, для которых замыкание вернуло true, а не убирает их. Пишите в замыкании условие для тех элементов, которые должны остаться: temps.filter { $0 > 0 } даёт [3, 7].
Главное
mapвызывает замыкание для каждого элемента и возвращает новый массив той же длины.filterоставляет элементы, для которых замыкание вернулоtrue.reduce(0, +)собирает массив в одно значение, начиная с0.- Исходный массив ни один из этих методов не меняет, а в цепочке каждый метод работает с результатом предыдущего.