В типичном приложении работа с массивами — вполне обыденное дело. Нам нередко приходится перебирать все элементы, применяя к ним необходимые изменения.
Для этой задачи многие начинающие и продвинутые разработчики прибегают к классической итерации с использованием, к примеру forEach
, не зная, что для того во фреймворке Foundation есть более удачные функции.
Map
Map
— это стандартная применяемая к коллекции функция Swift, которая на входе получает замыкание, применяя его к каждому элементу и возвращая новую коллекцию. Иными словами — у вас есть начальный массив, элементы которого вы поочередно перебираете, применяя к каждому трансформацию, определяемую замыканием, и добавляете получаемое значение в новый массив.
Для наглядности разберем пример. Предположим, что нужно создать игру, где игроку даются анаграммы имен героев Marvel, и он должен угадать по ним персонажа. У нас есть список имен, к каждому элементу которого нужно применить следующие шаги.
- Перевести имена в нижний регистр.
- Удалить пробелы.
- Перемешать буквы.
Если не использовать map
, а попытаться обойтись forEach
, то придется для каждого шага создавать пустой массив, применять действие к каждому элементу и добавлять результат. Три массива, слишком много всего, что усложнит чтение и понимание кода. Посмотрим, насколько этот подход оказывается многословен:
let heroes = ["Iron Man", "Spiderman", "Star Lord", "Black Widow"]
var noWhitespaceHeroes = [String]()
heroes.forEach { hero in
let noWhitespaceHero = hero.replacingOccurrences(of: " ", with: "")
noWhitespaceHeroes.append(noWhitespaceHero)
}
// noWhitespaceHeroes = ["IronMan", "Spiderman", "StarLord", "BlackWidow"]
var lowercasedHeroes = [String]()
noWhitespaceHeroes.forEach { hero in
let lowercasedHero = hero.lowercased()
lowercasedHeroes.append(lowercasedHero)
}
// lowercasedHeroes = ["ironman", "spiderman", "starlord", "blackwidow"]
var heroAnagrams = [String]()
lowercasedHeroes.forEach { hero in
let heroAnagram = String(Array(hero).shuffled())
heroAnagrams.append(heroAnagram)
}
// пример heroAnagrams: ["oranimn", "arisendmp", "sraotlrd", "wlcwabiokd"]
Если же задействовать map
, то код получится намного проще и понятнее — подобно магии!
let heroes = ["Iron Man", "Spiderman", "Star Lord", "Black Widow"]
let heroAnagrams = heroes.map {
String(Array($0.replacingOccurrences(of: " ", with: "")
.lowercased()
.shuffled())
)
}
Как видите, читать его чрезвычайно легко, даже не зная все внутренние шаги.
CompactMap
CompactMap
работает аналогично map
, но имеет дополнительный бонус — удаляет все элементы nil
, создаваемые трансформацией.
Рассмотрим пример:
let urlStrings = [
"https://www.marvel.com",
"https://www.😈.com",
"https://www.dccomics.com"
]
let urls = urlStrings.compactMap { URL(string: $0) }
// urls = [https://www.marvel.com, https://www.dccomics.com]
Очевидно, что иконка не способна создать валидный url. С помощью compactMap
можно без проблем отфильтровать, не прибегая к дополнительным проверкам.
CompactMap
также предоставляет отличный способ для фильтрации списка с целью удаления всех значений nil
. Я недавно работал над приложением, в котором все метки UI в одном из представлений управлялись данными, получаемыми с серверами. Если значение присутствовало, нужно было показывать метку, если нет — метка скрывалась, не оставляя пустого места.
Все работало динамически в UITableView
, так что мне нужно было лишь добавлять или не добавлять элемент в источник данных.
В общем, фильтрация всего с помощью compacMap
оказалась очень быстрой и понятной для чтения. В приложении же подобный прием выглядит немного сложнее:
let fields = ["John", "Appleseed", nil, "Cupertino"]
let items = fields.compactMap { $0 }
// items = ["John", "Appleseed", "Cupertino"]
Обратите внимание на одну деталь в этом примере. Массив fields
имеет тип [String?]
, а массив items
— [String]
, так что возвращаемый тип compacMap
никогда не будет optional
. Изумительно!
FlatMap
Последний тип карты — это функция, которая уплощает массив массивов. Она одним действием преобразует набор массивов в один. Как-то так:
[[1, 1, 1], [2, 2, 2], [3, 3, 3]] -> [1, 1, 1, 2, 2, 2, 3, 3, 3]
Разберем пример.
Представьте, что пишете игру, в которой отряды героев сражаются с множеством противников. Вашей команде доступна группа персонажей, из которых вы выбираете участников сражения.
В ходе каждого раунда количество пораженных врагов прибавляется в статистику соответствующего героя. Теперь нужно подсчитать среднее число побежденных вашим отрядом врагов в каждом раунде. То есть создать один массив из всех статистик всех героев и вычислить по нему среднее значение.
Рассмотрим код:
struct HeroTeam {
let name: String
let heroes: [Hero]
}
struct Hero {
let name: String
let enemiesDefeated: [Int]
}
extension Array where Element == Int {
var mean: Double {
return Double(self.reduce(0, +)) / Double(self.count)
}
}
let hero01 = Hero(name: "Iron Man", enemiesDefeated: [12, 14, 8, 19])
let hero02 = Hero(name: "Captain America", enemiesDefeated: [8, 20, 21])
let hero03 = Hero(name: "The Hulk", enemiesDefeated: [12, 25, 19, 28, 32])
let team = HeroTeam(name: "Super Team", heroes: [hero01, hero02, hero03])
let averageDefeated = team.heroes
.compactMap { $0 }
.flatMap { $0.enemiesDefeated }
.mean
// averageDefeated = 18.166666666666668
// Массив массивов до применения flatMap = [[12, 14, 8, 19], [8, 20, 21], [12, 25, 19, 28, 32]]
Вся магия происходит на строке 25. Заметьте, что я для вычисления среднего использовал расширение (строка 26), а не функцию vDSP.mean
из фреймворка Accelerate
, чтобы сохранить аккуратный стиль функционального программирования.
И еще кое что
Если вы внимательно читали начало статьи, то могли заметить, что я писал не о простых массивах, а о коллекциях. Как думаете, почему? Потому что, функции map
также применимы и к словарям! Хотя для этого нужно быть более внимательным:
let heroInfo = [
"firstName":"Anthony Edward",
"lastName" : "Stark",
"nickname": "Iron Man",
"superPowers": "Genius, billionaire, playboy, philanthropist"
]
let infos = heroInfo.map { key, value in
[key: value]
}
// infos = [["lastName": "Stark"], ["nickname": "Iron Man"], ["firstName": "Anthony Edward"], ["superPowers": "Genius, billionaire, playboy, philanthropist"]]
Как видите, исходный словарь разделяется на массив словарей. Теперь у нас есть возможность применить трансформацию только к значениям словаря, но не его ключам.
Также в Swift для всех трех вышеприведенных примеров есть разные версии карт, с помощью которых можно реализовать эту задачу. Просто используйте вариант mapValues
:
let heroInfo = [
"firstName":"Anthony Edward",
"lastName" : "Stark",
"nickname": "Iron Man",
"superPowers": "Genius, billionaire, playboy, philanthropist"
]
let infoUppercased = heroInfo.mapValues{ $0.uppercased() }
// infoUppercased = ["nickname": "IRON MAN", "superPowers": "GENIUS, BILLIONAIRE, PLAYBOY, PHILANTHROPIST", "lastName": "STARK", "firstName": "ANTHONY EDWARD"]
Теперь мы получили [String: String]
, а не [[String: String}}
, как в предыдущем примере.
Успешного вам написания кода!
Читайте также:
- Реализация ViewPager в Swift 5
- Как с With() улучшить написание кода на Swift
- Новый API форматировщика дат в Swift
Перевод статьи Alessandro Manilii: Map, CompactMap, and FlatMap in Swift