Один из моих любимых синтаксических сахаров
Одна из моих любимых особенностей Swift заключается в том, что почти все функции компилятора построены на основе реальных классов и протоколов языка.
Это означает, что если вы видите собственный тип, который имеет особое магическое свойство (например, декларативный синтаксис SwiftUI), вполне вероятно, что вы сможете воспроизвести это поведение в своих пользовательских типах.
В этой статье я расскажу об одном из моих любимых синтаксических сахаров языка - протоколах и внутреннем поведении компилятора, которые позволяют писать for
циклы.
Возможно, вы уже слышали о семействе протоколов Sequence
, и здесь мы увидим, как компилятор использует их в качестве строительных блоков для for
циклов.
for i in 0..<10 {
print("Wait, this is not a hardcoded language thing?")
}
В большинстве языков возможность повторения простых типов данных, таких как целые числа, в операторах for
часто жестко встроена в компилятор. Это не отличалось от первых нескольких версий Swift, где стандартом было использование старых циклов в стиле C.
// Swift 1.2
for var i = 0; i < 10; i++ {
print(i)
}
Циклы стиля for in
пришли вместе с типом Range
, и выпуск Swift 3 официально удалил старые циклы стиля C из языка и заставил всех использовать for in
циклы, к которым мы все привыкли.
Теперь for
циклы в Swift основаны на фактических протоколах, которые вы можете унаследовать, чтобы добавить for in
возможностей к всему, которое вы создаете, что может быть представлено как таковое.
Как и ожидалось, это означает, что возможность использовать диапазоны в качестве параметра цикла не потому, что диапазоны обрабатываются по-разному в компиляторе, а потому, что диапазоны соответствуют определенным общедоступным протоколам.
IteratorProtocol
Основой того, как итерация в целом работает в Swift, является тип IteratorProtocol
. Это очень простой протокол, который принимает только два компонента: associatedtype
, который представляет, что выполняется итерация, и свойство next
, которое возвращает «следующий» элемент в итерации (или nil
, если он завершен).
Базовым примером чего-то, что может быть реализовано как IteratorProtocol
, является таймер обратного отсчета, который продолжает уменьшать значение до тех пор, пока оно не достигнет нуля:
struct CountdownIterator: IteratorProtocol {
typealias Element = Int
var count: Int
mutating func next() -> Int? {
if count == 0 {
return nil
} else {
self.count -= 1
return count
}
}
init(count: Int) {
self.count = count
}
}
Если мы будем обращаться к этому итератору постоянно, мы будем получать убывающие значения до тех пор, пока итерация не закончится, что обозначено nil
:
var iterator = CountdownIterator(count: 3)
iterator.next() // 2
iterator.next() // 1
iterator.next() // 0
iterator.next() // nil
iterator.next() // nil
Итераторы называются деструктивными, потому что вы не можете получить доступ к определенному значению более одного раза - вам нужно идти от начала до конца линейно, и вы можете сделать это только один раз. Если вам снова нужно получить доступ к значению, вам нужно создать еще один итератор и повторить процесс.
Последовательность
Что касается итераций, протокол Sequence
- это то, с чем мы взаимодействуем больше всего.
Короче говоря, Sequence
- это просто высокоуровневая абстракция типа IteratorProtocol
, которая обеспечивает дополнительную функциональность поверх обычного процесса итерации.
Хотя Swift позволяет вам создавать Sequences
, которые также IteratorProtocols
, существование этого протокола позволяет отделить определение самого типа от логики, которая обрабатывает его итерацию.
Чтобы использовать его, все, что вам нужно сделать, это создать тип makeIterator()
, который возвращает IteratorProtocol
:
struct Countdown: Sequence {
typealias Iterator = CountdownIterator
let count: Int
init(count: Int) {
self.count = count
}
func makeIterator() -> CountdownIterator {
return CountdownIterator(count: count)
}
}
Требования реализации протокола Sequence
могут сделать его бесполезным, но не судите книгу по ее обложке - на самом деле она содержит массу необходимых методов, но все они реализованы для вас в стандартной библиотеке.
Sequence
содержит предварительные определения для множества часто используемых алгоритмов на основе последовательностей, включая map
, filter
и reduce
!
Хотя IteratorProtocol
- это тип, определяющий, как происходит итерация, Sequence
- это тип, который предоставляет дополнительные свойства и методы, которые делают эти итерации полезными в первую очередь:
let countdown = Countdown(count: 3)
countdown.map { $0 * $0 }
countdown.allSatisfy { $0.isMultiple(of: 2) }
countdown.max()
countdown.sorted()
countdown.forEach { print($0) }
Как и в IteratorProtocol
, мы можем создать итератор и линейно извлекать из него значения. Вот как работают все эти свойства и методы. Например, вот как мы могли бы реализовать клон map()
:
extension Sequence {
func map<T>(_ transform: (Iterator.Element) throws -> T) rethrows -> [T] {
var iterator = makeIterator()
var result = [T]()
while let next = iterator.next() {
result.append(try transform(next))
}
return result
}
}
Но это еще не все - величайшее свойство Sequences
и причина, по которой была задумана эта статья, заключается в том, что Sequences
глубоко привязаны к компилятору Swift в форме for
циклов.
Как упоминалось ранее, причина, по которой вы можете использовать диапазоны в циклах, заключается не в том, что диапазоны являются особенными, а просто в том, что диапазоны равны Sequences
. Поскольку в нашем примере тип Countdown
- это Sequence
, его также можно использовать внутри цикла for
!
for duration in Countdown(count: 3) {
print(duration)
}
Как и следовало ожидать, это будет работать с любым типом, соответствующим Sequence
.
Как циклы работают внутри Swift
Интересно, что такое поведение не ново для iOS - старожилы могут помнить, что вы могли разблокировать for-each
циклы стиля в Objective-C аналогичным образом, сделав объекты наследованными от NSFastEnumerator
:
for (id object in objectThatConformsToFastEnumerator) {
NSLog(@"%@", object);
}
Но, в отличие от того времени, со Swift мы действительно можем видеть, что происходит под капотом.
for
существует внутри компилятора с целью правильного анализа вашего кода, но окончательный код на самом деле отличается. Чтобы проверить это, мне нравится скомпилировать файл примера и дать Swift дамп его окончательного представления на промежуточном языке Swift:
swiftc -emit-sil forLoopTest.swift
Если я запущу эту команду с примером forLoopTest.swift
файла, содержащего for
цикл для массива, такого как приведенный ниже, мы увидим такие вещи вокруг области, где должен был быть определен цикл:
let array = [1,2,3]
for num in array {
print(num)
}
// function_ref Collection<>.makeIterator()
%40 = function_ref ...
// function_ref IndexingIterator.next()
%40 = function_ref ...
switch_enum %43 : $Optional<Int>, case #some!enumelt.1: bb3, case #none!enumelt: bb2
Это показывает нам, что for
циклов на самом деле не существует, это просто форма синтаксического сахара, позволяющая вам легко итерировать IteratorProtocol
без необходимости извлекать итератор!
Мы можем получить более подробную информацию, просмотрев исходный код Swift, в частности классы, которые обрабатывают семантический анализ кода.
В этом случае волшебство происходит после того, как компилятор проверяет тип цикла: Если мы проанализируем код из средства проверки типов, мы увидим, что, если цикл представляет действительный код, который повторяет Sequence
, он вводит новое свойство называется ${loopName}$generator
.
Это свойство выбирает итератор последовательности, затем выбирает тип Element
итератора и затем, наконец, перемещает наше исходное замыкание в новый цикл, который повторяет свойство итератора next()
.
Например, если мы скомпилируем предыдущий пример цикла, окончательный результат будет чем-то сопоставимым (но не совсем) с этим:
let array = [1,2,3]
var $num$generator = array.makeIterator()
while let num = $num$generator.next() {
print(num)
}
Компилятор Swift использует префикс $
для внутренних свойств, синтезируемых компилятором. Поскольку ему необходимо ввести реальный код, он использует символ, который невозможно использовать в обычном коде, чтобы гарантировать, что у нас не будет проблем с интервалом между именами.
Интересно, что вы можете взаимодействовать с некоторыми из этих внутренних свойств как часть новой функции оболочки свойств.
А как насчет коллекций?
Изучая больше о типах коллекций Swift, IteratorProtocol
, Sequence
и Collection
часто упоминаются вместе, и вы могли заметить, что сгенерированный SIL для нашего примера упоминает Collections
, поскольку он использует массив.
Если синтаксический сахар связан только с Sequences
, как работают коллекции?
Можно сказать, что Collection
- это просто более навороченная версия Sequence
. Фактически, все Collections
тоже Sequences
:
public protocol Collection: Sequence { ... }
В отличие от обычного Sequences
, Collections
позволяет вам обращаться к элементам несколько раз, перебирать их в обратном порядке (в случае BidirectionalCollections
) и даже обращаться к определенным элементам напрямую через индексы (в случае RandomAccessCollections
, например Array<>
).
В отличие от Sequences
, Collections
не нужно предоставлять IteratorProtocol
. По умолчанию Swift предоставляет IndexingIterator
структуру: IteratorProtocol
, которая принимает коллекцию и просто просматривает индексы, определенные как часть протокола Collection
.
Поскольку все Collections
являются Sequences
, им также может быть полезно иметь синтаксические сахарные петли.
Если вы хотите видеть больше подобной информации о Swift и iOS, подпишитесь на меня в моем Twitter (@rockthebruno) и дайте мне знать обо всех отзывах, предложениях и исправлениях, которыми вы хотите поделиться.