Перейти к содержимому
Теория · Замыкания · урок 5 из 8

compactMap и опционалы

Вернёмся к повару у ленты. Теперь по ленте едут подарочные коробки: в одних что-то лежит, другие пусты. Карточка с рецептом говорит повару: открой коробку, выложи то, что внутри, на поднос, а пустую коробку выброси. В конце на подносе только продукты, без коробок и без пустых мест.

В Swift такие коробки появляются, когда замыкание возвращает опционал. Как вы уже знаете, Int("abc") возвращает опционал: в тексте может не оказаться числа. Если превращать строки в числа через map, получится массив опционалов — коробок, часть из которых пуста:

SWIFT
let inputs = ["10", "abc", "25", ""]
print(type(of: inputs.map { Int($0) })) // Array<Optional<Int>>
let numbers = inputs.compactMap { Int($0) }
print(numbers) // [10, 25]
print(numbers.reduce(0, +)) // 35

Разберём по строкам:

  1. map { Int($0) } даёт массив опционалов, его тип — [Int?]. Полное имя этого типа — Array<Optional<Int>>, его и напечатал type(of:). Сложить такие элементы через reduce(0, +) нельзя: компилятор потребует извлечь каждое значение.
  2. compactMap открывает каждую коробку, выбрасывает пустые и оставляет содержимое. Из "abc" и пустой строки чисел не вышло, поэтому numbers — это [10, 25] типа [Int].
  3. reduce складывает оставшиеся числа: 35.

Чем результат отличается от map

compactMap вызывает замыкание для каждого элемента, как map. Но пустой результат он пропускает, а из полного извлекает значение. Поэтому у его массива два отличия:

  • он бывает короче исходного: из четырёх строк inputs вышло два числа;
  • в нём нет опционалов: тип numbers — [Int], и эти числа можно складывать и сортировать.

В названии compactMap слово compact значит «плотный»: пустые места выброшены, и значения стоят вплотную.

Здесь картинка с лентой неточна. В примере на ленте лежат не коробки, а обычные строки. Коробку делает само замыкание: Int($0) возвращает опционал, и compactMap тут же его открывает. Исходный массив inputs при этом не меняется, как и после map.

Не только числа из строк

Опционал возвращает не только Int(…). Поиск в словаре тоже даёт коробку: ключа может не оказаться. Так compactMap собирает цены заказа по меню и пропускает блюда, которых в меню нет:

SWIFT
let menu = ["tea": 50, "cake": 120]
let order = ["tea", "pizza", "cake"]
print(order.compactMap { menu[$0] }) // [50, 120]
print(order.compactMap { menu[$0] }.reduce(0, +)) // 170

Давайте разберём пример:

  1. menu[$0] ищет блюдо в меню и возвращает Int?. Ключа "pizza" в меню нет, поэтому для пиццы выходит nil.
  2. compactMap выбрасывает этот nil и оставляет цены чая и торта: [50, 120].
  3. compactMap возвращает обычный массив, поэтому reduce вызывают прямо у результата, как в цепочке filter и map. Сумма заказа — 170.

Выбросить или заменить

Иногда длину массива нужно сохранить: каждой строке ввода должно отвечать своё число. Тогда пустую коробку не выбрасывают, а заменяют запасным значением через ??. Запись inputs.map { Int($0) ?? 0 } даёт [10, 0, 25, 0]: четыре строки — четыре числа.

Этот вопрос часто задают на собеседовании: чем compactMap отличается от map? Ответ:

  • map возвращает массив той же длины; если замыкание возвращает опционал, получится массив опционалов [Int?];
  • compactMap выбрасывает nil, извлекает остальные значения и возвращает массив без опционалов [Int], иногда короче исходного.

Частая ошибка

Строки превратили в числа через map и сразу стали складывать:

SWIFT
let sum = inputs.map { Int($0) }.reduce(0, +) // ✗
print(sum)
TEXT
error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'

Программа не компилируется: map дал массив [Int?], и + не может сложить закрытые коробки. Под ошибкой компилятор советует ?? или !. С ?? 0 сумма выйдет верной, а с ! программа аварийно завершится на строке "abc". Замените map на compactMap, и reduce сложит только числа: 35.

Главное

  • compactMap вызывает замыкание для каждого элемента, выбрасывает nil и извлекает остальные значения.
  • map с замыканием, которое возвращает опционал, даёт массив [Int?], а compactMap — массив [Int], который можно сложить через reduce.
  • Массив после compactMap бывает короче исходного; чтобы сохранить длину, пустое значение заменяют через ??.