Swift: 7 секретов оптимизации

1. Ключевое слово indirect

Ключевое слово indirect применяется только с перечислениями enum. Как известно, они являются типами значений и хранятся в стеке. Поэтому компилятору необходимо знать, сколько памяти занимает каждое перечисление. 

Поскольку в любой момент времени возможен только один вариант, то enum занимает память самого большого кейса перечисления (англ. case) вкупе с текущими рабочими данными.

// Ничего необычного, просто общее перечисление 
enum Foo {
case bizz(String)
case fizz(Int)
}

А что если сделать enum рекурсивным? 

// Бесконечный размер??
enum Foo {
case bizz(Foo)
case fizz
}

Это определение порождает ошибку компилятора. 

Рекурсивное перечисление Foo не отмечено ключевым словом indirect.  

Суть ошибки в том, что компилятор не может вычислить размер Foo, так как он стремится к бесконечности. В этом случае требуется indirect

// Теперь другое дело 
enum Foo {
indirect case bizz(Foo)
case fizz
}

Этот прием: 

  • простой  —  видоизменяет структуру памяти enum для решения проблемы рекурсии;
  • конкретный  —  .bizz(Foo) больше не хранится как вложение в памяти. С модификатором indirect данные хранятся в указателе (косвенно). 

Проблема решена! Кроме того, мы можем преобразовать все кейсы перечисления в indirect

// Теперь каждый кейс - indirect 
indirect enum Foo {
case bizz(Foo?)
case fizz(Foo?)
}

2. Атрибут @autoclosure

Атрибут Swift @autoclosure определяет аргумент, который автоматически оборачивается в замыкание. Как правило, он необходим для того, чтобы отложить выполнение выражения до нужного момента. 

func calculate(_ expression: @autoclosure () -> Int,
zero: Bool) -> Int {
guard !zero else {
return 0
}
return expression()
}

Вызов вычисления происходит следующим образом: 

calculate(1 + 2, zero: false) // 3
calculate([Int](repeating: 5, count: 10000000).reduce(0, +),
zero: false) // 50000000
calculate([Int](repeating: 5, count: 1000).reduce(0, +),
zero: true) // 0

В этом примере при условии zero: true вызов calculate не вычисляет выражение, тем самым улучшая производительность кода. 

3. Свойства Lazy

Свойство отложенного хранения lazy  —  это свойство, начальное значение которого не вычисляется до первого применения. “Ленивые” свойства всегда объявляются как переменные. Обратите внимание, что при указании lazy в struct следует обозначить функцию, задействующую эту структуру, как mutating.

class Foo {
lazy var bonk = DBConnection()
func send() {
bonk.sendMessage()
}
}

Мы уже познакомились с атрибутом @autoclosure, который позволяет отложить вычисление выражения. Он также может использоваться с lazy! Рассмотрим распространенный случай внедрения зависимости: 

class Foo {
let bonkProvider: () -> DBConnection
lazy var bonk: DBConnection = bonkProvider()
init(_ expression: @escaping @autoclosure () -> DBConnection) {
self.bonkProvider = expression
}
func send() {
// Вызов bonkProvider()
// Только для первого вызова send()
bonk.sendMessage()
}
}

4. Перечисления как пространства имен 

В Swift нет пространств имен, что может обернуться сложностями при работе с большими проектами. Но эта проблема легко решается с помощью перечислений. 

enum API {}
extension API {
static let token = "…"
struct CatsCounter {

}
}
let a = API.CatsCounter()
print(API.token)

5. Атрибут @dynamicMemberLookup

Данный раздел посвящен атрибуту @dynamicMemberLookup (динамический поиск элементов). Он пригодится для работы со структурами и классами. 

Если мы просто добавим @dynamicMemberLookup в определение, то получим ошибку. 

Атрибут @dynamicMemberLookup требует наличия у класса Foo метода subscript(dynamicMember:), который принимает либо ExpressibleByStringLiteral, либо key path

Следовательно, необходимо определить метод subscript

@dynamicMemberLookup
class Foo {
subscript(dynamicMember string: String) -> String {
return string
}
}
let a = Foo()
print(a.helloWorld)

В subscript есть возможность реализовать намного более сложную логику для извлечения данных. При этом видно, что данная реализация ограничена только строками и не гарантирует полную безопасность. Исправим это с помощью key path:

class Bob {
let age = 22
let name = "Bob"
}
@dynamicMemberLookup
class Foo {
let himself = Bob()
subscript<T>(dynamicMember keyPath: KeyPath<Bob, T>) -> T {
return himself[keyPath: keyPath]
}
}
let a = Foo()
print(a.age)

Знание этой функциональности не обязывает использовать ее повсеместно. Вы сами определяете, какой из двух вариантов отличается лучшей читаемостью и выразительностью: a.himself.age или a.age.

6. Атрибут @dynamicCallable

Речь идет о функциональности компилятора для вызова объектов, которая применяется со struct, enum и class.

Добавление атрибута приводит к ошибке: 

Атрибут @dynamicCallable требует наличия у структуры RangeGenerator либо метода dynamicallyCall(withArguments:), либо dynamicallyCall(withKeywordArguments:).

Сигнатура метода аналогична сигнатуре @dynamicMemberLookup:

@dynamicCallable
struct RangeGenerator {
var range: Range<Int>
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> [Int] {
if args.count > 1 || args.first?.key != "count" {
fatalError("Unknown arguments \(args)")
}
let count = args.first!.value
return (0..<count).map{ _ in Int.random(in: range) }
}
}
let gen = RangeGenerator(range: 0..<100)
print(gen(count: 13))
// [2, 89, 4, 17, 65, 26, 73, 86, 93, 13, 25, 96, 96]

7. Встраивание кода 

Иногда требуется предоставить дополнительную информацию об оптимизациях, которые может задействовать компилятор. Встраиваемый код  —  одна из важнейших функциональностей оптимизации. Рассмотрим, как работать с атрибутами ‌@inlinable, @inline(__always) и @usableFromInline

Атрибут @inlinable экспортирует тело функции как часть интерфейса модуля, делая его доступным для оптимизатора при обращении к нему из других модулей. 

В результате @inlinable обеспечивает общедоступность реализации метода и возможность ее встраивания в вызывающую программу. Кроме того, все, что вызывается, в обязательном порядке становится @usableFromInline.

@inline(__always) указывает компилятору игнорировать эвристику встраивания и (почти) всегда встраивать функцию. 

Функция с @inline(__always) в отличии от функции с @inlinable не может использоваться для встраивания вне своего модуля из-за недоступности ее кода. 

@inline(__always) может как улучшить производительность, так и негативно сказаться на производительности макросов из-за увеличения размера кода. 

struct Foo {
@inlinable
@inline(__always)
func simpleComputation(_ a: Int, _ b: Int) -> Int {
duplicate(a) + duplicate(b)
}
@usableFromInline
func duplicate(_ c: Int) -> Int {
c * 2
}
func general() {
print("Hello world")
}
}

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

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Alex Dremov: Top 7 Subtle Swift Features

Предыдущая статьяКак писать функции: 8 советов от опытного разработчика
Следующая статьяТоповые пакеты Python для очистки данных