1. Как читать методы массива
Когда вы впервые видите numbers.map { ... }, мозг может честно сказать: «Я вижу фигурные скобки и подозреваю магию». На самом деле магии нет: это всё те же циклы, просто «упакованные» в методы стандартной библиотеки. Важная идея сегодня — научиться читать такие вызовы как фразу: «возьми массив и примени к каждому элементу правило».
Начнём с основ. Цикл for-in — это универсальный инструмент: им можно сделать вообще всё, включая очень странные вещи. Но из-за этой универсальности код иногда получается «разговорчивым»: он рассказывает как мы идём по массиву, хотя нам важнее что мы хотим получить.
Методы map, filter, compactMap и forEach как раз помогают писать код на уровне намерения:
- map — «преобразуй каждый элемент»
- filter — «оставь только подходящие»
- compactMap — «попробуй преобразовать, а то, что не получилось — выкинь»
- forEach — «сделай действие для каждого элемента»
Чтобы было проще ощущать пользу, мы будем развивать маленькое консольное приложение StudyLog — журнал учебных сессий. Представьте, что вы записываете: что изучали и сколько минут. Никаких сложных структур (они будут позже), пока используем массивы и tuples, потому что они уже знакомы.
import Foundation
let sessions: [(topic: String, minutes: Int)] = [
(topic: "Swift", minutes: 25),
(topic: "Алгоритмы", minutes: 40),
(topic: "Английский", minutes: 15)
]
С этим массивом мы будем делать «отчёты»: красиво печатать, отбирать длинные занятия, считать только валидные числа из ввода.
2. map: преобразование элементов
Обычно map нравится людям после первой же удачной задачи, потому что он делает ровно одну вещь и делает её честно: берёт каждый элемент массива и превращает его в новый элемент, складывая результаты в новый массив. Это похоже на конвейер на фабрике: на входе детали одного типа, на выходе — другого (или того же, но в новом виде).
Формально map можно прочитать так: «для каждого элемента вызови замыкание-преобразователь и собери результаты в массив». Важно: исходный массив не меняется, вы получаете новый.
Пример 1: превратим сессии в строки отчёта
Мы хотим получить массив строк вида "- Swift: 25 мин".
import Foundation
let sessions: [(topic: String, minutes: Int)] = [
(topic: "Swift", minutes: 25),
(topic: "Алгоритмы", minutes: 40),
(topic: "Английский", minutes: 15)
]
let lines = sessions.map { session in
"- \(session.topic): \(session.minutes) мин"
}
print(lines)
// ["- Swift: 25 мин", "- Алгоритмы: 40 мин", "- Английский: 15 мин"]
Обратите внимание, как читается строчка sessions.map { ... }: «сессии… преобразовать… по правилу».
Пример 2: map как «вытащи поле»
Иногда map — это просто «достань один кусочек из каждого элемента». Например, получить список тем:
import Foundation
let sessions: [(topic: String, minutes: Int)] = [
(topic: "Swift", minutes: 25),
(topic: "Алгоритмы", minutes: 40)
]
let topics = sessions.map { $0.topic }
print(topics) // ["Swift", "Алгоритмы"]
Здесь $0 уместен: выражение короткое и очевидное. Если бы логика была сложнее — лучше дать параметру имя.
Мини-таблица: что делает map
| Метод | Что делает | Что возвращает | Тип замыкания (идея) |
|---|---|---|---|
|
преобразует каждый элемент | новый массив | |
3. filter: отбор по условию
filter — это «сито» для массива. Он берёт элементы и оставляет только те, для которых условие (замыкание) вернуло true. Это очень похоже на обычный цикл с if, но обычно читается быстрее, потому что слово filter сразу говорит: «тут отбор».
Важно помнить, что filter тоже не меняет исходный массив, а возвращает новый.
Пример 1: оставим только длинные сессии
Скажем, мы хотим показать только занятия от 30 минут и больше.
import Foundation
let sessions: [(topic: String, minutes: Int)] = [
(topic: "Swift", minutes: 25),
(topic: "Алгоритмы", minutes: 40),
(topic: "Английский", minutes: 15)
]
let longSessions = sessions.filter { session in
session.minutes >= 30
}
print(longSessions)
// [(topic: "Алгоритмы", minutes: 40)]
Пример 2: фильтр + map как мини-пайплайн
Часто вы фильтруете, а потом преобразуете.
Например: «вывести строки отчёта только для длинных занятий».
import Foundation
let sessions: [(topic: String, minutes: Int)] = [
(topic: "Swift", minutes: 25),
(topic: "Алгоритмы", minutes: 40),
(topic: "Английский", minutes: 15)
]
let report = sessions
.filter { $0.minutes >= 30 }
.map { "✅ \($0.topic): \($0.minutes) мин" }
print(report) // ["✅ Алгоритмы: 40 мин"]
Здесь важно не увлечься «цепочками на полэкрана». Пока цепочка из 2–3 шагов — читается отлично. Длиннее — мозгу становится тяжело, и лучше разнести на промежуточные переменные (это не «плохой стиль», это забота о будущем себе).
Мини-таблица: что делает filter
| Метод | Что делает | Что возвращает | Тип замыкания (идея) |
|---|---|---|---|
|
оставляет элементы, которые проходят условие | новый массив | |
4. compactMap: преобразование с отбрасыванием nil
compactMap обычно становится любимцем дня, потому что он красиво решает вечную проблему: «у меня есть данные, но часть из них мусор». В Swift мусор часто выглядит как nil: например, Int("abc") возвращает nil, потому что строка не число.
Идея compactMap такая: вы пытаетесь преобразовать каждый элемент в Optional, а затем выкидываете все nil и разворачиваете .some. То есть на выходе получается массив уже нормальных, не-optional значений.
Если коротко: compactMap — это как map, но с «аварийным люком» для неудачных преобразований.
Эта логика хорошо иллюстрируется тем, что compactMap часто можно мысленно представить как «внутри map + if let». В одной из дискуссий про compactMap показывают именно такой стиль: внутри замыкания вы проверяете условие и возвращаете nil, чтобы элемент был отброшен.
Пример 1: парсим минуты из строк
Представим, что пользователь ввёл строку с минутами через пробел:
"25 30 x 45" — тут есть лишнее "x".
import Foundation
let input = "25 30 x 45"
let parts = input.split(separator: " ")
let minutes = parts.compactMap { part in
Int(part) // Int(...) вернёт Int?; nil будет отброшен
}
print(minutes) // [25, 30, 45]
Тут происходит очень «по-человечески»: попробовали превратить каждый кусок в число — получилось не везде — ну и ладно, берём то, что получилось.
Пример 2: соберём StudyLog из ввода
Добавим в наш StudyLog ввод: пользователь вводит темы через запятую, а минуты — через пробел. Минуты могут быть мусором, и мы не хотим падать.
import Foundation
let rawMinutes = "10 20 nope 30"
let parts = rawMinutes.split(separator: " ")
let safeMinutes = parts.compactMap { Int($0) }
print(safeMinutes) // [10, 20, 30]
Это пока не полноценный ввод приложения (мы не строим сложный CLI-парсер сегодня), но уже видим практическую пользу: compactMap спасает от ручных проверок.
Небольшая схема: что делает compactMap
flowchart TD
A["Исходный массив"] --> B["transform: Element -> Optional<T>"]
B --> C{"Результат nil?"}
C -- да --> D["выкинуть элемент"]
C -- нет --> E["взять значение"]
D --> F["Новый массив T"]
E --> F["Новый массив T"]
5. forEach: действие для каждого элемента
forEach выглядит как «цикл, но модный». И из-за этого с ним часто случаются две беды: люди пытаются использовать его там, где нужен обычный for, а потом удивляются, почему нельзя сделать break или continue. Тут важно запомнить простое правило: forEach хорош, когда вам нужно выполнить действие для каждого элемента, и вам не нужен контроль потока.
У forEach есть принципиальные отличия от for-in: внутри его замыкания нельзя использовать break/continue, а return выходит только из замыкания, но не из внешней функции/области. Это прямо подчёркивают в описании различий forEach и for-in.
Пример 1: печать отчёта
У нас есть lines — массив строк отчёта. Мы хотим напечатать каждую строку.
import Foundation
let lines = ["- Swift: 25 мин", "- Алгоритмы: 40 мин"]
lines.forEach { line in
print(line)
}
// - Swift: 25 мин
// - Алгоритмы: 40 мин
Пример 2: когда forEach хуже for-in
Допустим, вы хотите печатать строки, но остановиться, если встретили тему "Swift" (условно: «дальше не интересно, я уже понял»).
С for-in это делается естественно:
import Foundation
let lines = ["Swift", "Алгоритмы", "Английский"]
for line in lines {
if line == "Swift" { break }
print(line)
}
С forEach так сделать нельзя, потому что break/continue к нему не применяются. И именно поэтому forEach — не замена циклу, а отдельный инструмент под отдельную задачу.
Мини-таблица: где место forEach
| Метод | Что делает | Что возвращает | Когда уместен |
|---|---|---|---|
|
выполняет действие для каждого элемента | |
печать, логирование, простые побочные эффекты (без ) |
6. Как выбрать между for-in и map/filter/compactMap/forEach
Когда инструментов становится больше, появляется новый тип усталости: «я не знаю, какой выбрать». Это нормально. Тут помогает не запоминание «правильных» ответов, а привычка задавать себе один вопрос: я хочу получить новый массив или хочу сделать действие? А дальше уже почти всё решается автоматически.
Соберём мини-отчёт для нашего StudyLog:
- мы хотим взять «сырой» ввод минут (часть мусор),
- получить только валидные числа,
- оставить только те, что >= 20 минут,
- превратить их в строки,
- распечатать.
import Foundation
let raw = "10 20 nope 30 5"
let minutes = raw
.split(separator: " ")
.compactMap { Int($0) }
.filter { $0 >= 20 }
.map { "• \($0) минут" }
minutes.forEach { print($0) }
// • 20 минут
// • 30 минут
Здесь каждый шаг «говорит» о намерении:
- compactMap говорит: «убери мусор»
- filter говорит: «оставь подходящее»
- map говорит: «преврати в нужный формат»
- forEach говорит: «сделай действие (печать)»
И вот здесь появляется главный критерий «когда читаемее цикла»:
Если у вас есть конвейер из независимых шагов, где каждый шаг можно назвать одним словом (преобразовать / отобрать / отбросить nil / выполнить действие), то цепочка из map/filter/compactMap/forEach обычно читается лучше, чем один длинный for, внутри которого пять if и три временные переменные.
Но есть честная граница. Если вам нужно сложное управление потоком, ранний выход, несколько веток логики и промежуточные накопления, то for-in часто будет проще. Не потому, что map «плохой», а потому что у задачи другой характер: там уже не «конвейер», а «мини-сценарий».
Ещё один полезный ориентир: map и filter почти всегда должны быть чистыми (без побочных эффектов), а forEach как раз наоборот — почти всегда про эффект (печать, запись в лог, изменение внешнего состояния). Если вы делаете map, чтобы внутри печатать, это выглядит подозрительно: вы просите «конвейер» заняться «громкоговорителем». Иногда можно, но обычно это означает, что вы выбрали не тот инструмент.
7. Типичные ошибки
Ошибка №1: ожидать, что map изменит исходный массив.
Очень частая ловушка выглядит так: вы сделали sessions.map { ... }, потом печатаете sessions и удивляетесь, почему он не поменялся. В этом нет бага: map возвращает новый массив, а старый остаётся как был. Если вы не сохранили результат в переменную, вы просто «посчитали в пустоту».
Ошибка №2: использовать map ради побочных эффектов.
Иногда пишут что-то вроде numbers.map { print($0) } и получают массив из Void, который им не нужен. Это похоже на ситуацию «я заказал доставку пиццы, чтобы узнать номер телефона ресторана». Если ваша цель — действие, берите forEach или for-in. Так код будет честнее и проще.
Ошибка №3: путать map и compactMap при преобразованиях, которые могут не получиться.
Если вы делаете parts.map { Int($0) }, вы получите [Int?], то есть массив optional’ов. Иногда это именно то, что нужно, но чаще вы хотите «всё, что распарсилось». Тогда нужен compactMap, потому что он выбрасывает nil и возвращает [Int].
Ошибка №4: пытаться сделать break/continue внутри forEach.
Это одна из тех ошибок, которые сначала раздражают, а потом дисциплинируют. forEach не поддерживает break и continue, а return внутри замыкания не выходит из внешнего кода — это отдельное поведение, отличающееся от for-in. Если вам нужен ранний выход или сложный контроль потока, выбирайте for-in и не спорьте с языком: он победит.
Ошибка №5: превращать цепочку в «простыню», которая не читается.
map/filter/compactMap прекрасно читаются, пока каждый шаг короткий и понятный. Но если внутри каждого шага по 10 строк с условиями и временными переменными, вы получаете код, который одновременно и не цикл, и не красивый конвейер. В таких случаях лучше либо вынести сложную логику в отдельную функцию, либо вернуться к обычному for-in, который честно показывает сценарий выполнения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ