Целью книги является не только изучение синтаксиса и основ разработки на «яблочном» языке программирования. Мне бы хотелось, чтобы вы начали лучше и глубже понимать принципы разработки в Xcode на Swift. В этой главе приведены различные вспомогательные функциональные элементы, которыми так богат этот язык программирования. Вы также узнаете о важных функциональных элементах, которые смогут значительно облегчить практику программирования.
Описанные в главе методы помогут успешно работать с коллекциями различных типов. Если у вас уже есть опыт программирования, то, вероятно, вы привыкли заниматься обработкой этих типов с помощью циклов, однако сейчас это не всегда оправданно. Советую вам активно использовать описанные функции.
Метод map(_:) позволяет применить переданное в него замыкание для каждого элемента коллекции. В результате его выполнения возвращается новая последовательность, тип элементов которой может отличаться от типа исходных элементов (рис. 17.1).
Рассмотрим пример, описанный в листинге 17.1.
Листинг 17.1
var myArray = [2, 4, 5, 7]
var newArray = myArray.map{$0}
newArray // [2, 4, 5, 7]
Метод map(_:) принимает замыкание и применяет его к каждому элементу массива myArray. Переданное замыкание {$0} не производит каких-либо действий над элементами, поэтому результат, содержащийся в переменной newArray, не отличается от исходного.
Рис. 17.1. Принцип работы метода map
ПРИМЕЧАНИЕ В данном примере используется сокращенное имя параметра, если точнее, то $0. Данная тема была изучена в главе 16. Давайте повторим, каким образом функция map(_:) лишилась круглых скобок и приобрела вид map{$0}.
Метод map(_:) позволяет передать в него замыкание, которое имеет один входной аргумент того же типа, что и элементы обрабатываемой коллекции, и один выходной параметр. Если не использовать сокращенный синтаксис, то вызов метода будет выглядеть следующим образом:
var array = [2, 4, 5, 7]
var newArray = array.map({
(value: Int) -> Int in
return value
})
Замыкание никак не изменяет входной аргумент — просто возвращает его.
Оптимизируем замыкание.
Сократим код перед ключевым словом in, так как передаваемое замыкание имеет всего один входной аргумент.
Уберем круглые скобки, так как метод map(_:) имеет один входной аргумент.
Уберем оператор return, так как тело замыкания помещается в одно выражение.
В результате получим следующий код:
var newArray = array.map{value in value}
Теперь можно убрать ключевое слово in и заменить value на сокращенное имя $0:
var newArray = array.map{$0}
Изменим замыкание таким образом, чтобы map(_:) возводил каждый элемент в квадрат (листинг 17.2).
Листинг 17.2
newArray = newArray.map{$0*$0}
newArray // [4, 16, 25, 49]
Как говорилось ранее, тип значений результирующей последовательности может отличаться от типа элементов исходной последовательности. Так, например, в листинге 17.3 количество элементов массивов intArray и boolArray одинаково, но тип элементов различается (Int и Bool соответственно).
Листинг 17.3
var intArray = [1, 2, 3, 4]
var boolArray = intArray.map{$0 > 2}
boolArray // [false, false, true, true]
Каждый элемент последовательности сравнивается с двойкой, в результате чего возвращается логическое значение.
Вы можете обрабатывать элементы коллекции с помощью метода map(_:) произвольным образом. В листинге 17.4 показан пример создания многомерного массива на основе базового.
Листинг 17.4
let numbers = [1, 2, 3, 4]
let mapped = numbers.map { Array(repeating: $0, count: $0) }
mapped // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
Метод map(_:) позволяет обрабатывать элементы любой коллекции, в том числе и словаря. В листинге 17.5 показан пример перевода расстояния, указанного в милях, в километры.
Листинг 17.5
let milesToDest = ["Moscow":120.0,"Dubai":50.0,"Paris":70.0]
var kmToDest = milesToDest.map { name,miles in [name:miles * 1.6093] }
kmToDest // [["Dubai": 80.465], ["Paris": 112.651],
["Moscow": 193.116]]
Метод mapValues(_:) позволяет обработать значения каждого элемента словаря, при этом ключи элементов останутся в исходном состоянии (листинг 17.6).
Листинг 17.6
var mappedCloseStars = ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37]
var newCollection = mappedCloseStars.mapValues{ $0+1 }
newCollection // ["Proxima Centauri": "5.24", "Alpha Centauri A": "5.37]
В результате вы получаете все тот же словарь, но с обработанными значениями.
Метод filter(_:) используется, когда требуется отфильтровать элементы коллекции по определенному правилу (рис. 17.2).
В листинге 17.7 показана фильтрация всех целочисленных элементов исходного массива, которые делятся на 2 без остатка, то есть всех четных чисел.
Листинг 17.7
let numArray = [1, 4, 10, 15]
let even = numArray.filter{ $0 % 2 == 0 }
even // [4, 10]
Помимо массивов, можно производить фильтрацию других типов коллекций. В листинге 17.8 показана фильтрация элементов словаря starDistanceDict.
Листинг 17.8
var starDistanceDict = ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Barnard's Star": 5.96]
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // ["Alpha Centauri B": 4.37]
Рис. 17.2. Принцип работы метода filter
Метод reduce(_:_:) позволяет объединить все элементы коллекции в одно значение в соответствии с переданным замыканием. Помимо самих элементов, метод принимает первоначальное значение, которое служит для выполнения операции с первым элементом коллекции.
Предположим, необходимо определить общее количество имеющихся у вас денег. На вашей карте 210 рублей, а в кошельке 4 купюры разного достоинства. Эта задача легко решается с помощью метода reduce(_:_:) (рис. 17.3 и листинг 17.9).
Листинг 17.9
let cash = [10, 50, 100, 500]
let total = cash.reduce(210, +) // 870
Первый аргумент — это начальное значение, второй — замыкание, обрабатывающее каждую пару элементов. Первая операция сложения производится между начальным значением и первым элементом массива cash.
Рис. 17.3. Принцип работы метода reduce
Результат этой операции складывается со вторым элементом массива и т.д.
Замыкание, производящее операцию, может быть произвольным — главное, чтобы оно обрабатывало операцию для двух входящих аргументов (листинг 17.10).
Листинг 17.10
let multiTotal = cash.reduce(210, {$0*$1})
multiTotal // 5250000000
let totalThree = cash.reduce(210, {a,b in a-b})
totalThree // -450
Метод flatMap(_:) отличается от map(_:) тем, что всегда возвращает плоский одномерный массив. Так, пример, приведенный в листинге 17.4, но с использованием flatMap(_:), вернет одномерный массив (листинг 17.11).
Листинг 17.11
let numbersArray = [1, 2, 3, 4]
let flatMapped = numbersArray.flatMap { Array(repeating: $0,
count: $0) }
flatMapped // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
Вся мощь flatMap(_:) проявляется тогда, когда в многомерном массиве требуется найти все подпадающие под некоторое условие значения (листинг 17.12).
Листинг 17.12
let someArray = [[1, 2, 3, 4, 5], [11, 44, 1, 6], [16, 403, 321, 10]]
let filterSomeArray = someArray.flatMap{$0.filter{ $0 % 2 == 0}}
filterSomeArray // [2, 4, 44, 6, 16, 10]
Функция zip(_:_:) предназначена для формирования последовательности пар значений, каждая из которых составлена из элементов двух базовых последовательностей. Другими словами, если у вас есть две последовательности и вам нужно попарно брать их элементы, группировать и складывать в новую последовательность, то эта функция как раз то, что нужно. Сначала вы берете первые элементы каждой последовательности, группируете их, потом берете вторые элементы, и т.д.
Пример использования функции zip(_:_:) приведен в листинге 17.13.
Листинг 17.13
let collectionOne = [1, 2, 3]
let collectionTwo = [4, 5, 6]
var zipSequence = zip(collectionOne ,collectionTwo)
type(of: zipSequence) // Zip2Sequence<Array<Int>, Array<Int>>.Type
// генерация массива из сформированной последовательности
Array(zipSequence) // [(.0 1, .1 4), (.0 2, .1 5), (.0 3, .1 6)]
// генерация словаря на основе последовательности пар значений
let newDictionary = Dictionary(uniqueKeysWithValues: zipSequence)
newDictionary // [1: 4, 3: 6, 2: 5]
Обратите внимание на еще один новый для вас тип данных Zip2Sequence<Array<Int>, Array<Int>>.
Со многими новыми типами данных вы познакомитесь в будущих главах, а со временем даже научитесь создавать собственные. Но настоящая магия начнется тогда, когда вы перестанете бояться таких конструкций и поймете, что они значат и откуда появляются. Это неминуемо, если вы будете старательно продолжать обучение и пытаться делать чуть больше, чем сказано в книге.
Рассмотрим пример использования оператора guard при работе с опционалами.
Предположим, что название некоторой геометрической фигуры хранится в константе. Вам потребовалось реализовать механизм вывода на консоль сообщения, содержащего информацию о количестве сторон в данной фигуре. Для реализации задуманного напишем две функции:
• Первая — countSidesOfShape — возвращает количество сторон фигуры по ее названию.
• Вторая — maybePrintCountSides — выводит необходимое сообщение на консоль.
Почему лучше написать две функции вместо одной? Так как ваша программа предназначена для работы с геометрическими фигурами, то функция countSidesOfShape может потребоваться вам и для других целей. По этой причине имеет смысл разбить функционал.
Так как вы не можете заранее предусмотреть все варианты геометрических фигур, то в случае обработки неизвестной фигуры программа должна выводить сообщение о том, что ей неизвестно количество сторон для нее.
Реализуем функцию с именем countSidesOfShape (листинг 17.14).
Листинг 17.14
func countSidesOfShape(shape: String) -> Int? {
switch shape {
case "треугольник":
return 3;
case "квадрат":
return 4;
case "прямоугольник":
return 4;
default:
return nil;
}
}
Данная функция принимает имя фигуры на вход и возвращает количество ее сторон либо nil, если фигура неизвестна.
Далее реализуем функцию maybePrintCountSides(shape:), принимающую на вход название фигуры (листинг 7.15).
Листинг 17.15
func maybePrintCountSides(shape: String) {
if let sides = countSidesOfShape(shape: shape) {
print("Фигура \(shape) имеет \(sides) стороны")
}else{
print("Неизвестно количество сторон фигуры \(shape)")
}
}
Для получения количества сторон используется оператор условия if, осуществляющий проверку операции опционального связывания. Логика работы функции состоит в том, что, если фигура отсутствует в базе, не имеет смысла выполнять функцию, можно вывести информационное сообщение и досрочно завершить ее работу. Для этого можно использовать оператор раннего выхода guard (листинг 17.16).
Листинг 17.16
func maybePrintCountSides (shape: String) {
guard let sides = countSidesOfShape(shape: shape) else {
print("Неизвестно количество сторон фигуры \(shape)")
return
}
print("Фигура \(shape) имеет \(sides) стороны")
}
Оператор guard проверяет, возможно ли провести операцию опционального связывания, и в случае отрицательного результата выполняет код тела оператора, где с помощью return досрочно завершается работа функции.
Если опциональное связывание успешно завершается, то тело guard игнорируется и выполняется следующий за ним код.
С помощью guard код функции стал значительно более читабельным. Особенно это заметно, если код, следующий за оператором, будет занимать больше одной строки. С его помощью вы проверяете возможность получения всех необходимых параметров прежде, чем перейдете к выполнению кода функции.