Зачем нужны макросы
Иногда код, который вы пишете, состоит из повторяющегося шаблона: конструктор, который просто копирует значения параметров в свойства, реализация протокола, которая всегда выглядит одинаково, обёртка, которая логирует вызов функции. Компилятор мог бы дописать этот шаблон за вас сам, если бы умел смотреть на ваш код и по нему генерировать ещё код. Именно это и делает макрос: вы один раз описываете правило «если видишь такую-то конструкцию — разверни её вот в такой код», а дальше компилятор применяет это правило сам, ещё до того, как программа хоть раз запустится.
Как это называется в документации Apple
Макрос (macro) — это код, который выполняется на этапе компиляции и порождает
новый исходный код Swift, подставляемый на место вызова макроса. Макросы бывают двух видов:
freestanding («самостоятельный») — вызывается отдельно, как выражение или объявление,
и attached («присоединённый») — прикрепляется атрибутом к уже существующему
объявлению: типу, свойству или функции. Freestanding-макрос в коде начинается со знака решётки,
например #warning("текст"), attached-макрос — со знака собаки, например
@Observable.
Чем макрос отличается от функции
Функция — это код, который выполняется во время работы программы: вы вызываете её, процессор выполняет инструкции, вы получаете результат. Макрос работает совсем на другом этапе: он разворачивается компилятором ещё до финальной проверки типов и генерации машинного кода, заменяя вызов макроса на обычный текст Swift-кода, как будто вы напечатали его руками. После разворачивания макроса программа не запускается — она ещё только собирается. Из этого следует важное отличие: функция может только вернуть значение, а attached-макрос может добавить типу новые свойства, методы или даже заставить его соответствовать протоколу — то, что обычная функция сделать не в состоянии, потому что она не может менять форму окружающего кода.
Freestanding-макросы: решётка перед именем
Несколько freestanding-макросов уже встроены в стандартную библиотеку Swift. Они выглядят как вызов функции, но решаются на этапе компиляции:
func processOrder(id: Int) {
guard id > 0 else {
#warning("здесь стоит проверить и отрицательные id")
return
}
print("Обрабатываю заказ", #function, "с номером", id)
}
#warning("...") заставляет компилятор напечатать предупреждение прямо во время
сборки — полезно оставлять себе заметки о недоделанных местах. #function разворачивается
в строковый литерал с именем текущей функции — "processOrder(id:)" — сама эта строка
подставляется компилятором, программа во время выполнения никого не спрашивает, в какой функции
она находится. Похожим образом устроены #line (номер строки), #file и
#error("..."), который останавливает сборку с ошибкой, а не предупреждением.
Attached-макросы: собака перед объявлением
Attached-макрос ставится атрибутом перед объявлением и меняет то, что компилятор сгенерирует
для этого объявления. Пример из фреймворка Observation — макрос @Observable:
@Observable
final class ProfileViewModel {
var userName: String
var isLoading: Bool
init(userName: String, isLoading: Bool) {
self.userName = userName
self.isLoading = isLoading
}
}
Без макроса это была бы обычная разметка класса с двумя свойствами. Атрибут @Observable
разворачивается компилятором в дополнительный код: класс получает соответствие протоколу
Observable и вокруг каждого хранимого свойства появляется код отслеживания изменений,
чтобы экран, который читает userName или isLoading, узнавал об их
изменении и обновлялся. Сам этот курс не разбирает построение экранов подробно, но если вам
встретится такой код в чужом проекте на SwiftUI — теперь вы понимаете, откуда в классе с виду
берутся возможности, которых там не написано руками: их дописал макрос.
Готовые макросы, которые вы уже видели или ещё встретите
#expectи#requireиз библиотеки Swift Testing — freestanding-макросы, которые оборачивают проверку условия и при провале сами формируют понятное сообщение об ошибке с местом в коде, где что-то пошло не так.@Testиз той же библиотеки — attached-макрос на функции, который превращает обычную функцию в тест, который умеет находить и запускать тестовый раннер.@Observableиз фреймворкаObservation— attached-макрос на классе, разобранный выше.#Preview— freestanding-макрос, которым в SwiftUI-проектах помечают блок кода для предпросмотра экрана в Xcode без запуска всего приложения.
Все они устроены одинаково: вы пишете короткую пометку, а компилятор превращает её в куда больший объём обычного, ничем не примечательного Swift-кода.
Как посмотреть, во что разворачивается макрос
Проверять макрос вслепую неудобно, поэтому Xcode умеет показывать результат разворачивания.
Если щёлкнуть правой кнопкой мыши по вызову макроса в редакторе кода Xcode и выбрать в контекстном
меню пункт Expand Macro, редактор покажет ровно тот код, который компилятор подставил
на это место. Это лучший способ понять непонятный макрос из чужого проекта: не гадать по документации,
а посмотреть на развёрнутый код напрямую.
Почему свой макрос живёт в отдельном таргете пакета
Может показаться, что макрос — это просто ещё одна функция, которую можно дописать рядом с
остальным кодом. На деле реализация макроса — это отдельная программа: она получает код, который
нужно развернуть, в виде синтаксического дерева, разбирает его и строит на основе него новый код.
Эта программа должна быть собрана и запущена компилятором ещё до того, как он начнёт собирать код,
который этот макрос использует, — иначе разворачивать было бы попросту нечем. Поэтому в
Package.swift для макроса заводят отдельный таргет специального типа (наряду с уже
знакомыми типами таргетов вроде обычного или исполняемого), а таргет, где макрос используется,
подключает его как обычную зависимость. Смешать реализацию макроса и код, который его вызывает,
в одном таргете нельзя — это были бы одновременно и инструмент сборки, и то, что этим инструментом
собирается.
Частые ошибки
- Ждать от макроса поведения функции: пытаться передать в него значение, которое становится известно только во время выполнения программы. Макрос работает с исходным кодом на этапе сборки, а не со значениями во время работы программы.
- Путать freestanding и attached по написанию: решётка — это отдельный вызов сам по себе, собака — это всегда пометка перед уже существующим объявлением, а не самостоятельная конструкция.
- Считать любую пометку с собакой впереди макросом. Некоторые атрибуты Swift — часть языка изначально и не являются макросами; отличить их можно по документации конкретного атрибута.
- Пытаться написать свой макрос в том же файле или таргете, где он будет использоваться, и удивляться ошибке сборки — реализация макроса обязана жить в отдельном таргете.
Резюме
- Макрос разворачивается компилятором в обычный Swift-код на этапе сборки, а не выполняется во время работы программы, как функция.
- Freestanding-макрос (
#имя) вызывается сам по себе, как выражение или объявление; attached-макрос (@Имя) прикрепляется к уже существующему объявлению и может добавить ему новые свойства, методы или соответствие протоколу. - Готовые примеры из экосистемы:
#expect,#requireи@Testиз Swift Testing,@Observableиз Observation,#Previewв SwiftUI-проектах. - В Xcode пункт
Expand Macroв контекстном меню показывает, во что реально развернулся макрос. - Свой макрос реализуется как отдельная программа — плагин компилятора — и поэтому обязан жить в отдельном таргете пакета, а не рядом с кодом, который его использует.
Проверьте себя
3 вопроса