compactMap и опционалы
Вернёмся к повару у ленты. Теперь по ленте едут подарочные коробки: в одних что-то лежит, другие пусты. Карточка с рецептом говорит повару: открой коробку, выложи то, что внутри, на поднос, а пустую коробку выброси. В конце на подносе только продукты, без коробок и без пустых мест.
В Swift такие коробки появляются, когда замыкание возвращает опционал. Как вы уже знаете, Int("abc") возвращает опционал: в тексте может не оказаться числа. Если превращать строки в числа через map, получится массив опционалов — коробок, часть из которых пуста:
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Разберём по строкам:
map { Int($0) }даёт массив опционалов, его тип —[Int?]. Полное имя этого типа —Array<Optional<Int>>, его и напечаталtype(of:). Сложить такие элементы черезreduce(0, +)нельзя: компилятор потребует извлечь каждое значение.compactMapоткрывает каждую коробку, выбрасывает пустые и оставляет содержимое. Из"abc"и пустой строки чисел не вышло, поэтомуnumbers— это[10, 25]типа[Int].reduceскладывает оставшиеся числа:35.
Чем результат отличается от map
compactMap вызывает замыкание для каждого элемента, как map. Но пустой результат он пропускает, а из полного извлекает значение. Поэтому у его массива два отличия:
- он бывает короче исходного: из четырёх строк
inputsвышло два числа; - в нём нет опционалов: тип
numbers—[Int], и эти числа можно складывать и сортировать.
В названии compactMap слово compact значит «плотный»: пустые места выброшены, и значения стоят вплотную.
Здесь картинка с лентой неточна. В примере на ленте лежат не коробки, а обычные строки. Коробку делает само замыкание: Int($0) возвращает опционал, и compactMap тут же его открывает. Исходный массив inputs при этом не меняется, как и после map.
Не только числа из строк
Опционал возвращает не только Int(…). Поиск в словаре тоже даёт коробку: ключа может не оказаться. Так compactMap собирает цены заказа по меню и пропускает блюда, которых в меню нет:
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Давайте разберём пример:
menu[$0]ищет блюдо в меню и возвращаетInt?. Ключа"pizza"в меню нет, поэтому для пиццы выходитnil.compactMapвыбрасывает этотnilи оставляет цены чая и торта:[50, 120].compactMapвозвращает обычный массив, поэтомуreduceвызывают прямо у результата, как в цепочкеfilterиmap. Сумма заказа —170.
Выбросить или заменить
Иногда длину массива нужно сохранить: каждой строке ввода должно отвечать своё число. Тогда пустую коробку не выбрасывают, а заменяют запасным значением через ??. Запись inputs.map { Int($0) ?? 0 } даёт [10, 0, 25, 0]: четыре строки — четыре числа.
Этот вопрос часто задают на собеседовании: чем compactMap отличается от map? Ответ:
mapвозвращает массив той же длины; если замыкание возвращает опционал, получится массив опционалов[Int?];compactMapвыбрасываетnil, извлекает остальные значения и возвращает массив без опционалов[Int], иногда короче исходного.
Частая ошибка
Строки превратили в числа через map и сразу стали складывать:
let sum = inputs.map { Int($0) }.reduce(0, +) // ✗
print(sum)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бывает короче исходного; чтобы сохранить длину, пустое значение заменяют через??.