Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 16. Замыкания (closure)
Дальше: 18. Ленивые вычисления

17. Дополнительные возможности

Целью книги является не только изучение синтаксиса и основ разработки на «яблочном» языке программирования. Мне бы хотелось, чтобы вы начали лучше и глубже понимать принципы разработки в Xcode на Swift. В этой главе приведены различные вспомогательные функциональные элементы, которыми так богат этот язык программирования. Вы также узнаете о важных функциональных элементах, которые смогут значительно облегчить практику программирования.

Описанные в главе методы помогут успешно работать с коллекциями различных типов. Если у вас уже есть опыт программирования, то, вероятно, вы привыкли заниматься обработкой этих типов с помощью циклов, однако сейчас это не всегда оправданно. Советую вам активно использовать описанные функции.

17.1. Метод map(_:)

Метод 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]]

17.2. Метод mapValues(_:)

Метод 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]

В результате вы получаете все тот же словарь, но с обработанными значениями.

17.3. Метод filter(_:)

Метод 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

17.4. Метод reduce(_:_:)

Метод 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

17.5. Метод flatMap(_:)

Метод 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]

17.6. Метод zip(_:_:)

Функция 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>>.

Со многими новыми типами данных вы познакомитесь в будущих главах, а со временем даже научитесь создавать собственные. Но настоящая магия начнется тогда, когда вы перестанете бояться таких конструкций и поймете, что они значат и откуда появляются. Это неминуемо, если вы будете старательно продолжать обучение и пытаться делать чуть больше, чем сказано в книге.

17.7. Оператор guard для опционалов

Рассмотрим пример использования оператора 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 код функции стал значительно более читабельным. Особенно это заметно, если код, следующий за оператором, будет занимать больше одной строки. С его помощью вы проверяете возможность получения всех необходимых параметров прежде, чем перейдете к выполнению кода функции.

Назад: 16. Замыкания (closure)
Дальше: 18. Ленивые вычисления