Глобальные функции (их можно вызвать отовсюду без привязки к области действия определённого типа) — это довольно старая концепция, которая была популярна в таких языках, как С и 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
для содержания значения.
Заключение
Итак, очевидно, что никакой из этих методов не является обязательным, но с их помощью можно написать легко поддерживаемый код с лучшей производительностью, чего нельзя добиться, используя устаревшие решения.
Читайте также:
- Понимание врапперов в Swift
- Дизайн-системы в 2020 году: где источник истины?
- Я разработал и запустил MVP продукт за 5 дней
Перевод статьи Bruno Rocha: Useful Global Swift Functions