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")
}
}
Читайте также:
- Map, CompactMap и FlatMap в Swift
- Как с With() улучшить написание кода на Swift
- Как создать пользовательскую поисковую панель SwiftUI с LazyVStack
Читайте нас в Telegram, VK и Дзен
Перевод статьи Alex Dremov: Top 7 Subtle Swift Features