Swift 5.1 представил обертки свойств через SE-0258, чтобы устранить повторяющийся шаблон доступа. Требование projectedValue было разработано для того, чтобы раскрыть вторичные API поверхности—такие как Binding или состояния валидации SwiftUI—вне самого обёрнутого значения. Эта функция позволяет разработчикам получать доступ к метаданным или проекциям с использованием синтаксиса с префиксом $.
Проблема возникает из-за того, что Swift должен преобразовывать декларативный синтаксис в валидный SIL (Swift Intermediate Language), не вводя конфликтов имен или не нарушая контроль доступа. Компилятор должен синтезировать хранилище, которое поддерживает семантику значений для обёрнутого свойства, при этом потенциально раскрывая ссылочную семантику через проекцию, всё это с обеспечением того, что идентификатор с префиксом $ не конфликтует с пользовательскими членами.
Решение включает в себя десугаривание на уровне исходного кода. Для свойства, объявленного как @Wrapper var property: T, компилятор генерирует три отдельных члена. Во-первых, приватная переменная хранилища _property типа Wrapper<T>. Во-вторых, вычисляемое свойство property, которое перенаправляет операции get/set к _property.wrappedValue. В-третьих, вычисляемое свойство $property, которое возвращает _property.projectedValue. Свойство с префиксом $ наследует контроль доступа оригинальной декларации, и компилятор обеспечивает, что projectedValue существует при использовании синтаксиса с $.
@propertyWrapper struct Validating<T> { var wrappedValue: T var projectedValue: ValidationState<T> init(wrappedValue: T) { self.wrappedValue = wrappedValue self.projectedValue = ValidationState(value: wrappedValue) } } // Десугаривание: struct Form { private var _username: Validating<String> var username: String { get { _username.wrappedValue } set { _username.wrappedValue = newValue } } var $username: ValidationState<String> { get { _username.projectedValue } } }
Мы проектировали приложение для ввода медицинских данных, где каждое поле должно было отслеживать как текущее значение, так и сложную историю валидации, включая предыдущие ошибки и временные метки исправлений. Задача заключалась в том, чтобы раскрыть два различных пути данных из одной абстракции свойства: необработанную строку для текстового поля пользовательского интерфейса и историю валидации для аналитики и отображения ошибок.
Первый рассматриваемый подход заключался в поддержании параллельного словаря, сопоставляющего имена свойств с объектами ValidationHistory. Это обеспечивало гибкость хранения, но вводило API с явно строковыми типами, которые ломались во время рефакторинга и требовали ручной синхронизации между словарем и фактическими значениями свойств. Риск десинхронизации, приводящий к устаревшим отображениям ошибок, был неприемлемо высоким для медицинских данных.
Второй подход заключался в создании обертки-структуры, которая содержала как значение, так и историю, а потом использование этого составного типа в качестве типа свойства. Хотя это было безопасно по типам, это загрязняло доменную модель вопросами валидации и заставляло каждую точку доступа обрабатывать распаковку, что противоречило цели чистого разделения архитектуры между пользовательским интерфейсом и бизнес-логикой.
Третий подход использовал пользовательскую обертку свойства @Validated с projectedValue, возвращающим ссылочный тип ValidationHistory. Это инкапсулировало синхронизацию внутри, при этом раскрывая $fieldName для доступа к истории. Мы выбрали этот подход, потому что он поддерживал семантику CoW (Copy-on-Write) для обёрнутого строкового значения, обеспечивая стабильную идентичность ссылки для истории валидации, что позволяло компонентам пользовательского интерфейса наблюдать за изменениями без накладных расходов на копирование.
Результат устранил целый класс ошибок синхронизации и сократил кодовую базу, связанную с валидацией, на 35%. Синтаксис $ обеспечивал интуитивную доступность для младших разработчиков, а принудительное соблюдение на этапе компиляции предотвращало случайное раскрытие деталей реализации за пределами модулей.
Почему изменения в значении projectedValue не сохраняются, когда доступ к нему осуществляется через префикс доллара?
Когда обертка свойства является структурой, геттер projectedValue возвращает копию значения. Если projectedValue возвращает структуру (например, Int или пользовательскую структуру состояния валидации), такие выражения, как $property.errorCount += 1, изменяют временную копию, которая немедленно отбрасывается. Чтобы обеспечить постоянные изменения, projectedValue должен возвращать ссылочный тип, или обертка должна реализовать CoW с поддержкой хранения на основе классов. Альтернативно, можно вернуть Binding или изменяемый указатель, который обеспечивает косвенность. Новички часто предполагают, что $property предоставляет изменяемый доступ к внутреннему состоянию обертки, не учитывая семантику значений Swift.
Как контроль доступа сгенерированного свойства с префиксом доллара взаимодействует с уровнем доступа оригинального свойства?
Компилятор синтезирует свойство с префиксом $ с идентичным контролем доступа к оригинальному свойству. Если вы объявляете public @Wrapper var name: String, как name, так и $name являются public. Напротив, private свойства генерируют private проекции. Кандидаты часто пытаются сделать обёрнутое значение публичным, сохраняя проекцию внутренней или приватной, что невозможно в текущих версиях Swift. Обходное решение требует сделать свойство private и раскрыть обёрнутое значение через явное вычисляемое свойство, в то время как проекция остается ограниченной.
Может ли одна обертка свойства раскрывать несколько различных проекций, и каковы эргономические последствия?
Swift строго разрешает только одно свойство projectedValue на обертку. Однако это свойство может возвращать кортеж, структуру или перечисление, содержащие несколько значений (например, projectedValue: (Binding<T>, ValidationError?, Bool)). Эргономическая компромисс заключается в том, что для доступа к компонентам требуется синтаксис с точкой для $property ($property.0, $property.isValid), что снижает читаемость. Некоторые кандидаты пытаются объявить несколько свойств projectedValue или применять несколько оберток свойств к одному свойству (цепочку). Хотя цепочка поддерживается, она создает сложные семантики и непрозрачные проблемы с выводом типов. Рекомендуемый подход для нескольких проекций состоит в возврате специальной структуры проекции с именованными свойствами, сохраняя безопасность типов, принимая синтаксические накладные расходы.