JavaRush /Курсы /Swift SELF /map / filter / compactMap / forEach

map / filter / compactMap / forEach

Swift SELF
16 уровень , 3 лекция
Открыта

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

Метод Что делает Что возвращает Тип замыкания (идея)
map
преобразует каждый элемент новый массив
(Element) -> NewElement

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 мин"]

Здесь важно не увлечься «цепочками на полэкрана». Пока цепочка из 23 шагов — читается отлично. Длиннее — мозгу становится тяжело, и лучше разнести на промежуточные переменные (это не «плохой стиль», это забота о будущем себе).

Мини-таблица: что делает filter

Метод Что делает Что возвращает Тип замыкания (идея)
filter
оставляет элементы, которые проходят условие новый массив
(Element) -> Bool

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

Метод Что делает Что возвращает Когда уместен
forEach
выполняет действие для каждого элемента
Void
печать, логирование, простые побочные эффекты (без
break
)

6. Как выбрать между for-in и map/filter/compactMap/forEach

Когда инструментов становится больше, появляется новый тип усталости: «я не знаю, какой выбрать». Это нормально. Тут помогает не запоминание «правильных» ответов, а привычка задавать себе один вопрос: я хочу получить новый массив или хочу сделать действие? А дальше уже почти всё решается автоматически.

Соберём мини-отчёт для нашего StudyLog:

  1. мы хотим взять «сырой» ввод минут (часть мусор),
  2. получить только валидные числа,
  3. оставить только те, что >= 20 минут,
  4. превратить их в строки,
  5. распечатать.
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, который честно показывает сценарий выполнения.

1
Задача
Swift SELF, 16 уровень, 3 лекция
Недоступна
Отчёт по минутам
Отчёт по минутам
1
Задача
Swift SELF, 16 уровень, 3 лекция
Недоступна
Фильтр полезного
Фильтр полезного
1
Задача
Swift SELF, 16 уровень, 3 лекция
Недоступна
Средний балл
Средний балл
1
Задача
Swift SELF, 16 уровень, 3 лекция
Недоступна
Отчёт сессий
Отчёт сессий
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ