Полезные глобальные функции языка Swift

Swift

Глобальные функции (их можно вызвать отовсюду без привязки к области действия определённого типа) — это довольно старая концепция, которая была популярна в таких языках, как С и Objective-C. А вот в Swift её использовать не рекомендуется, тут лучше подходят аккуратно типизированные и разграниченные вещи типа Swifty.

Так исторически сложилось, что в стандартной библиотеке Swift достаточно публичных глобальных функций. Некоторые из них даже дожили до сегодняшнего дня и успешно применяются в работе. Посмотрим, что из себя представляют функции zip и dump.

zip()

Возможно, это самая известная глобальная функция. Она нужна, чтобы объединять два и более массива в единую последовательность кортежа. Такое очень сильно помогает, когда нужно проитерировать два объекта одновременно. Без zip надо было бы вручную создать цикл for и отдельно получить каждый индекс каждого массива. А с zip можно получить элементы из всех массивов самым удобным способом for-in.

Например, есть экранная форма регистрации пользователя и мы хотим обновить текстовые поля, чтобы показать список результатов валидации, полученных из бэкенда. Пишем:

func present(validationResults: [FieldValidationResult],
inTextFields textFields: [MyTextField]) {
for i in 0..<textFields.count {
let field = textFields[i]
let result = validationResults[i]
field.render(validationResult: result)
}
}

А с функцией zip можем убрать всё ручное индексирование:

func present(validationResults: [FieldValidationResult],
inTextFields textFields: [MyTextField]) {
for (field, result) in zip(textFields, validationResults) {
field.render(validationResult: result)
}
}

Для возврата типа функции zip есть объект Zip2Sequence, который соответствует Sequence. Так что все другие методы, связанные с последовательностью, можно применить и тут, включая её трансформацию в массив.

dump()

Функция dump — это изящная альтернатива выводу объектов. В то время как вывод объектов — чистый синтаксический “сахар” для свойств типа description или debugDescription, то функция dump — суперзаряженная версия Mirror(reflecting:). Она выводит содержимое объекта при помощи рефлексии, что часто даёт результат со значительно большей информацией, в т.ч. иерархию объектов.

class Foo: NSObject {
    let bar: String = "bar"
}
let foo = Foo()
print(foo)
// <SwiftRocks.Foo: 0x1030b9250>
dump(foo)
// ▿ <SwiftRocks.Foo: 0x1030b9250> #0
//     - super: NSObject
//    - bar: "bar"

sequence()

Глобальная функция sequence() в целом немного непонятная, но при этом классная. С ней можно писать рекурсивные функции с более приятным синтаксисом.

Давайте представим, что мы меняем цвет фона в представлении и во всём остальном, что ниже по иерархии. Возможно, вам на ум приходит такой вот цикл while:

var currentView: UIView? = self
while currentView != nil {
currentView?.backgroundColor = .green
currentView = currentView?.superview
}

Это лучший юзкейс для sequence(). Формат такой: задача этой функции дать вам Sequence, которая обращается к определённому замыканию снова и снова. Так как рекурсивная часть этого метода всегда одинаковая (currentView = currentView?.superview), то мы можем применять sequence(). Так функция трансформируется в простой цикл for:

for view in sequence(first: self, next: { $0.superview } ) {
view.backgroundColor = .green
}

Этот код работает таким образом, что sequence() возвращает пользовательский тип UnfoldFirstSequence. Это простой враппер для Sequence, который продолжает применять замыкание снова и снова в его функции next().

isKnownUniquelyReferenced()

Функция isKnownUniquelyReferenced получает объект class и возвращает логическое значение, которое указывает, что на объект ссылаются только один раз. Это нужно, чтобы вы смогли реализовать семантику значений для ссылочных типов. И хотя структуры сами по себе являются типами значений, их содержимое может не быть таковым. Запомните, что перемещение класса внутрь структуры не означает, что он будет скопирован по назначению: 

class Foo: NSObject {
    var bar: String = "bar"
}
struct FooHolder {
    let foo: Foo = Foo()
    var intValue: Int = 1
}
var fooHolder = FooHolder()
var fooHolder2 = fooHolder
fooHolder2.foo.bar = "bar2"
fooHolder2.intValue = 2
print(fooHolder.intValue)
// 1
print(fooHolder2.intValue)
// 2
print(fooHolder.foo.bar)
// bar2
print(fooHolder2.foo.bar)
// bar2

Разберём пример выше: хотя fooHolder2 и его основное число — это отдельные сущности по отношению к первоначальному холдеру, основной класс всё ещё связан с ними обоими. Чтобы это решить, можно применить isKnownUniquelyReferenced. Эта функция поможет определить, когда происходит обращение к этому свойству, и при необходимости создать новый инстанс класса:

struct FooHolder {
private var _foo: Foo = Foo()
var foo: Foo {
mutating get {
if isKnownUniquelyReferenced(&_foo) {
return _foo
} else {
let newFoo = Foo()
newFoo.bar = _foo.bar
_foo = newFoo
return _foo
}
} set {
_foo = newValue
}
}
var intValue: Int = 1
}

Интересно, что стандартная библиотека Swift позволяет так использовать семантику копирования при записи по отношению к массивам и строчкам. 

repeatElement()

Функция repeatElement() делает именно то, о чём говорит её название. Подав на вход объект и число, мы получим Sequence из объектов данного типа, которая может итерироваться. Количество объектов в последовательности равно данному числу.

let repeated: Repeated<String> = repeatElement("SwiftRocks", count: 3)
for value in repeated {
print(value)
}
//SwiftRocks
//SwiftRocks
//SwiftRocks

Повторение элементов — распространённая операция в Swift, особенно для заполнения пробелов в строчках и массивах. Фактически, у большинства этих типов даже есть специальный инициализатор для этого:

let array = [Int](repeating: 0, count: 10)

Так почему вам стоит использовать repeatElement? Причина в производительности. repeatElement() возвращает тип Repeated<T> Sequence, похожий на Zip2Sequence в том, что он ничего не делает, кроме того, что обеспечивает повторяющуюся функциональность. Давайте представим, что вы хотите заменить определённую секцию числового массива другим числом. Один способ для этого — применить replaceSubrange с другим массивом:

array.replaceSubrange(2...7, with: [Int](repeating: 1, count: 6))
print(array)
// [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]

Да, это срабатывает, но применение [Int](repeating:) приносит и дополнительные заботы: нужно инициализировать буферные массивы, которые тут без надобности. Если вам нужна только повторяющаяся функциональность, то пользуйтесь repeatElement. Это будет эффективным решением. 

array.replaceSubrange(2...7, with: repeatElement(1, count: 6))

stride()

Есть ещё кое-что довольно популярное — это функция stride(). Она появилась в арсенале Swift как способ создавать циклы for, которые могут пропускать определённые элементы. Она была добавлена потому, что исчез способ, эквивалентный стилю С. 

for (int i = 0; i < 10; i += 2) { ... }

Теперь применим stride(), чтобы получить аналогичное поведение. 

for i in stride(from: 0, to: 10, by: 2) {
    // от 0 до 9, пропустить нечетные числа.
}

Аргументы этой функции соответствуют протоколу Strideable, в котором представлены объекты, отражающие концепцию смещений. 

Привожу пример того, как мы можем добавить концепцию разницы между двумя днями в объектах Date так, чтобы их можно было применять вместе со stride():

extension Date: Strideable {
func advanced(by n: Int) -> Date {
return Calendar.current.date(byAdding: .day,
value: n,
to: self)!
}
func distance(to other: Date) -> Int {
return Calendar.current.dateComponents([.day],
from: other,
to: self).day!
}
}
let startDate = Date()
let finalDate = startDate.advanced(by: 5)
for date in stride(from: startDate, to: finalDate, by: 1) {
print(date)
}
// March 24th
// March 25th
// March 26th
// March 27th
// March 28th

Помните, что у Date уже есть реализация методов Strideable, которая срабатывает за секунды. Так что копирование её в проект не сработает. 

Другие полезные математические функции

Математические функции

max()— возвращает максимальное значение аргументов;

min()— возвращает минимальное значение аргументов;

abs()— возвращает абсолютное значение аргумента (полезно в вопросах так называемого спортивного программирования).

Значения

swap()— меняет значение двух объектов. Я не упоминал об этой функции выше, потому что, если вам нужно поменять местами элементы массива, корректно будет пользоваться Array.swapAt(). Вы, конечно, можете применять swap() в других ситуациях, когда вам понадобится создать фейковое свойство aux для содержания значения.  

Заключение

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

Читайте также:


Перевод статьи Bruno Rocha: Useful Global Swift Functions