Зачем нужны функции высшего порядка
Когда вы работаете с массивом, множеством или словарём, почти всегда хочется сделать что-то одно из трёх: превратить каждый элемент во что-то другое, оставить только нужные элементы или свернуть коллекцию в одно значение (сумму, максимум, строку). Раньше для этого вы писали for-in с пустым массивом-накопителем и добавляли туда результат вручную. Это работает, но в коде появляется лишняя переменная, лишний цикл и место, где легко ошибиться: забыть добавить элемент, перепутать условие, не обнулить накопитель.
Функции высшего порядка — это методы коллекций, которые принимают в качестве аргумента другую функцию (или замыкание) и сами делают перебор за вас. Вы описываете не «как перебрать», а «что должно получиться» — какое преобразование применить к каждому элементу, какое условие оставляет элемент, как свернуть коллекцию в одно значение. Код становится короче, а главное — в нём меньше мест, где можно случайно ошибиться в механике цикла.
Прежде чем перейти к примерам, — про короткую запись замыкания { $0 }. Когда вы передаёте в такой метод короткое выражение, Swift разрешает не писать имя параметра и in, а обращаться к первому аргументу как к $0, ко второму — как к $1, и так далее. Это просто сокращённая запись, не отдельный механизм — устройство замыканий подробно разберём отдельно, здесь достаточно понимать, что $0 — это «тот единственный элемент, который сейчас обрабатывается».
map — превратить каждый элемент
map применяет переданное замыкание к каждому элементу коллекции и возвращает новый массив того же размера, где на месте каждого элемента — результат замыкания. Исходная коллекция не меняется.
let prices = [100, 250, 400]
let withDiscount = prices.map { $0 - 50 }
print(withDiscount)
Построчно: prices.map { ... } берёт каждое число из prices по очереди, подставляет его вместо $0 в выражение $0 - 50 и кладёт результат в новый массив. На выходе — [50, 200, 350]: три элемента на входе, три на выходе, порядок сохранён.
filter — оставить только нужное
filter проверяет каждый элемент замыканием, которое возвращает Bool, и оставляет в новом массиве только те элементы, для которых замыкание вернуло true. Размер результата заранее не известен — он может быть от нуля до размера исходной коллекции.
let scores = [40, 95, 60, 88, 30]
let passed = scores.filter { $0 >= 60 }
print(passed)
scores.filter { $0 >= 60 } проходит по каждому числу и спрашивает: «оно не меньше 60?». Число остаётся в passed, только если ответ true. Результат — [95, 60, 88]: элементы, не прошедшие проверку, просто исчезли, порядок оставшихся не изменился.
reduce — свернуть коллекцию в одно значение
Это метод, в котором новички путаются чаще всего, поэтому разберём его подробно. reduce сворачивает всю коллекцию в единственное значение: сумму, произведение, самую длинную строку — что угодно, что можно вычислить, постепенно накапливая результат. У него два аргумента: начальное значение накопителя и замыкание, которое на каждом шаге получает текущий накопитель и очередной элемент, а возвращает новый накопитель.
let numbers = [1, 2, 3, 4]
let total = numbers.reduce(0) { accumulator, item in
accumulator + item
}
print(total)
Разберём по шагам. Начальное значение — 0, это то, с чего стартует накопитель ещё до первого элемента. Дальше reduce идёт по массиву: на первом шаге accumulator равен 0, item — 1, замыкание возвращает 1 — это новый accumulator. На втором шаге accumulator — 1, item — 2, результат 3. Так продолжается до конца массива, и последнее возвращённое значение и есть итог — 10. Начальное значение — это не «первый элемент массива», это стартовая точка накопителя, и её можно выбрать любой: если бы вы начали с 100, итог был бы 110.
Для сложения и умножения есть ещё более короткая запись — можно передать сам оператор вместо замыкания, потому что + — это тоже функция, принимающая два аргумента:
let total2 = numbers.reduce(0, +)
print(total2)
Это делает ровно то же самое: 0 — начальное значение, + — правило объединения накопителя с очередным элементом.
compactMap — преобразовать и сразу отбросить пустое
compactMap ведёт себя как map, но ожидает, что замыкание возвращает Optional. Он применяет преобразование к каждому элементу, разворачивает результат, если он не nil, и просто выбрасывает элемент из результата, если преобразование вернуло nil. Итоговый массив может быть короче исходного и содержит уже не опционалы, а развёрнутые значения.
Самый практичный пример — разбор строк, введённых пользователем, в числа: Int(строка) возвращает Optional(Int) и превращается в nil, если строка не является числом.
let input = ["12", "seven", "5", "", "9"]
let parsed = input.compactMap { Int($0) }
print(parsed)
Int($0) пытается превратить каждую строку в число. Для "12" получится Optional(12), для "seven" и пустой строки — nil. compactMap разворачивает успешные значения и молча пропускает nil, поэтому результат — [12, 5, 9]: три числа вместо пяти исходных строк, и это уже обычные Int, без Optional.
flatMap — развернуть вложенность
flatMap нужен, когда замыкание возвращает не одно значение, а целую коллекцию, и вы хотите получить не массив массивов, а один общий плоский массив.
let groups = [[1, 2], [3, 4, 5], [6]]
let flat = groups.flatMap { $0 }
print(flat)
groups — массив массивов. flatMap { $0 } для каждой внутренней группы возвращает её саму, но вместо того, чтобы сложить группы одну в другую (как сделал бы обычный map, получив снова массив массивов), flatMap «расплющивает» их в одну последовательность: [1, 2, 3, 4, 5, 6].
forEach — просто выполнить действие
forEach не строит новый массив и ничего не возвращает — он просто выполняет замыкание для каждого элемента по очереди, как обычный цикл. Он уместен, когда вам нужен только побочный эффект — например, печать:
["Аня", "Борис"].forEach { name in
print("Привет, \(name)!")
}
Здесь важно не путать forEach с map: map отвечает на вопрос «что должно получиться взамен», а forEach — «что нужно сделать с каждым элементом», не создавая ничего нового. Использовать map ради одной только печати, выбрасывая результат, — плохой стиль, для этого и существует forEach.
Цепочки вызовов
Методы можно соединять один за другим — результат одного сразу становится источником для следующего. Такие цепочки принято читать слева направо, как последовательность шагов обработки:
let bonuses = [10, -5, 20, 0, 15]
let total = bonuses
.filter { $0 > 0 }
.map { $0 * 2 }
.reduce(0, +)
print(total)
Читаем как рецепт: сначала filter оставляет только положительные бонусы — [10, 20, 15], затем map удваивает каждый — [20, 40, 30], затем reduce складывает их в 90. Каждый шаг получает на входе результат предыдущего, и цепочку легко читать и легко расширять — правда, чем она длиннее, тем полезнее иногда разбить её на промежуточные переменные с понятными именами, чтобы не терять нить.
Когда лучше остаться на for-in
Честно: обычный цикл for-in не хуже. Он ничем не проигрывает по скорости и иногда даже понятнее — особенно если внутри цикла несколько разных действий, есть ранний выход через break, или логика ветвится на несколько случаев. Функции высшего порядка хороши, когда задача укладывается в одну понятную операцию — преобразовать, отобрать, свернуть. Если ради «функционального стиля» код превращается в нечитаемую цепочку из пяти методов — лучше вернуться к простому циклу. Выбор — это вопрос читаемости, а не «правильного» и «неправильного» способа.
Частые ошибки
- Использовать
mapвместоforEach, когда результат никуда не сохраняется — компилятор не запретит, но это вводит в заблуждение того, кто читает код. - Путать начальное значение
reduceс первым элементом массива:numbers.reduce(0, +)начинает с нуля независимо от того, что лежит вnumbers. - Забывать, что
filterможет вернуть пустой массив — если потом сразу обращаться к первому элементу без проверки, программа упадёт. - Применять
flatMapтам, где нужен обычныйmap— если замыкание возвращает не коллекцию, а одно значение,flatMapне нужен. - Строить длинную цепочку из четырёх-пяти методов подряд там, где простой
for-inчитался бы понятнее.
Резюме
map— превращает каждый элемент во что-то другое, размер результата совпадает с исходным.filter— оставляет элементы, прошедшие условие, размер результата заранее не известен.reduce— сворачивает коллекцию в одно значение через начальное значение и накопитель.compactMap— какmap, но отбрасываетnilи разворачивает остальное; удобен для разбора строк в числа.flatMap— превращает коллекцию коллекций в одну плоскую коллекцию.forEach— выполняет действие для каждого элемента, ничего не возвращая.$0,$1— короткая запись для аргументов замыкания без явного имени.- Цепочки читаются слева направо; если цепочка стала длинной и запутанной, обычный
for-in— не хуже.
Проверьте себя
4 вопроса
Квадраты чисел
Напишите функцию squares, которая принимает массив целых чисел и возвращает новый массив, где каждое число заменено на его квадрат. Порядок элементов сохраняется, размер результата совпадает с размером входного массива.
Используйте map — она отлично подходит, когда каждому элементу нужно сопоставить ровно одно новое значение.
Сумма чётных чисел
Напишите функцию sumOfEvenNumbers, которая принимает массив целых чисел и возвращает сумму только чётных элементов. Если чётных чисел в массиве нет (или массив пуст), функция должна вернуть 0.
Отберите нужные элементы одним методом и сложите их другим — так вы опишете задачу в два понятных шага вместо ручного цикла со счётчиком.
Сумма чисел из строк
Напишите функцию sumValidNumbers, которая принимает массив строк и возвращает сумму тех строк, которые получается превратить в целое число. Строки, которые числом не являются (включая пустые и с лишними пробелами), нужно просто пропустить, не прерывая работу функции. Если ни одна строка не подошла, верните 0.
Вспомните, что Int(строка) возвращает Optional: он равен числу, если строка действительно им является, и nil иначе.