Если вы использовали SwiftUI, то наверняка обращали внимание на такие ключевые слова, как @ObservedObject, @EnvironmentObject, @FetchRequest и так далее. Property Wrappers (далее «обёртки свойств») — новая возможность языка Swift 5.1. Эта статья поможет вам понять, откуда же взялись все конструкции с @, как использовать их в SwiftUI и в своих проектах.
Автор перевода: Евгений Заволжанский, разработчик FunCorp.
Прим.пер.: К моменту подготовки перевода часть исходного кода оригинальной статьи потеряла свою актуальность из-за изменений в языке, поэтому некоторые примеры кода намеренно заменены.
Обёртки свойств впервые были представлены на форумах Swift ещё в марте 2019 года, за несколько месяцев до объявления SwiftUI. В своём первоначальном предложении Дуглас Грегор ( Douglas Gregor), член команды Swift Core, описал эту конструкцию (тогда она называлась property delegates) как «доступное пользователю обобщение функциональности, в настоящее время предоставляемой такой языковой конструкцией, как, например, lazy».
Если свойство объявлено с ключевым словом lazy, это значит, что оно будет инициализировано при первом обращении к нему. Например, отложенную инициализацию свойства можно было бы реализовать с помощью закрытого свойства, доступ к которому осуществляется через вычисляемое свойство. Но с помощью ключевого слова lazy это сделать гораздо легче.
struct Structure { // Отложенная инициализация свойства с помощью lazy lazy var deferred = … // Аналогичная реализация с помощью закрытого и вычисляемого свойства private var _deferred: Type? var deferred: Type { get { if let value = _deferred { return value } let initialValue = … _deferred = initialValue return initialValue } set { _deferred = newValue } } }
В SE-0258: Property Wrapper отлично объясняется дизайн и реализация обёрток свойств. Поэтому, вместо того чтобы пытаться улучшить описание в официальной документации, рассмотрим несколько примеров, которые можно реализовать с помощью обёрток свойств:
- ограничение значений свойств;
- преобразование значений при изменении свойств;
- изменение семантики равенства и сравнения свойств;
- логирование доступа к свойству.
Ограничение значений свойств
SE-0258: Property Wrapper даёт несколько практических примеров, включая @Clamping, @Copying, @Atomic, @ThreadSpecific, @Box, @UserDefault. Рассмотрим обёртку @Clamping, которая позволяет ограничить максимальное или минимальное значение свойства.
@propertyWrapper struct Clamping<Value: Comparable> { var value: Value let range: ClosedRange<Value> init(initialValue value: Value, _ range: ClosedRange<Value>) { precondition(range.contains(value)) self.value = value self.range = range } var wrappedValue: Value { get { value } set { value = min(max(range.lowerBound, newValue), range.upperBound) } } }
@Clamping можно использовать, например, для моделирования кислотности раствора, величина которой может принимать значение от 0 до 14.
struct Solution { @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68)
Попытка установить значение pH, выходящее за диапазон от (0...14), приведёт к тому, что свойство примет значение, ближайшее к минимуму или максимуму интервала.
let superDuperAcid = Solution(pH: -1) superDuperAcid.pH // 0
Обёртки свойств могут использоваться при реализации других обёрток свойств. Например, обёртка @UnitInterval ограничивает значение свойства интервалом (0...1), используя @Clamping(0...1):
@propertyWrapper struct UnitInterval<Value: FloatingPoint> { @Clamping(0...1) var wrappedValue: Value = .zero init(initialValue value: Value) { self.wrappedValue = value } }
Похожие идеи
@Positive/@NonNegativeуказывает, что значение может быть либо положительным, либо отрицательным числом.@NonZeroуказывает, что значение свойства не может быть равно 0.@Validatedили@Whitelisted/@Blacklistedограничивает значение свойства определёнными значениями.
Преобразование значений при изменении свойств
Валидация значений текстовых полей — постоянная головная боль разработчиков приложений. Существует очень много вещей, которые нужно отслеживать: от банальностей типа кодировки до злонамеренных попыток ввести код через текстовое поле. Рассмотрим применение обёртки свойства для удаления пробелов, которые ввёл пользователь в начале и в конце строки.
import Foundation let url = URL(string: " https://habrahabr.ru") // nil let date = ISO8601DateFormatter().date(from: " 2019-06-24") // nil let words = " Hello, world!".components(separatedBy: .whitespaces) words.count // 3
Foundation предлагает метод trimmingCharacters(in:), с помощью которого можно удалить пробелы в начале и в конце строки. Можно вызывать этот метод всегда, когда нужно гарантировать правильность ввода, но это не очень удобно. Для этого можно использовать обёртку свойства.
import Foundation @propertyWrapper struct Trimmed { private(set) var value: String = "" var wrappedValue: String { get { return value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } }
struct Post { @Trimmed var title: String @Trimmed var body: String } let quine = Post(title: " Swift Property Wrappers ", body: "…") quine.title // "Swift Property Wrappers" — без пробелов в начале и в конце quine.title = " @propertyWrapper " // "@propertyWrapper"
Похожие идеи
@Transformedприменяет ICU-преобразование к введённой строке.@Rounded/@Truncatedокругляет или урезает значение строки.
Изменение семантики равенства и сравнения свойств
В Swift две строки равны, если они канонично эквивалентны, т.е. содержат одинаковые символы. Но допустим, мы хотим, чтобы строковые свойства были равны без учёта регистра символов, которые они содержат.
@CaseInsensitive реализует оболочку для свойств, имеющих тип String или SubString.
import Foundation @propertyWrapper struct CaseInsensitive<Value: StringProtocol> { var wrappedValue: Value } extension CaseInsensitive: Comparable { private func compare(_ other: CaseInsensitive) -> ComparisonResult { wrappedValue.caseInsensitiveCompare(other.wrappedValue) } static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedSame } static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedAscending } static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool { lhs.compare(rhs) == .orderedDescending } }
let hello: String = "hello" let HELLO: String = "HELLO" hello == HELLO // false CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true
Похожие идеи
@Approximateдля приблизительного сравнения свойств, имеющих тип Double или Float.@Rankedдля свойств, значения которых имеют порядок (например, ранг игральных карт).
Логирование доступа к свойству
@Versioned позволит перехватывать присвоенные значения и запоминать, когда они были установлены.
import Foundation @propertyWrapper struct Versioned<Value> { private var value: Value private(set) var timestampedValues: [(Date, Value)] = [] var wrappedValue: Value { get { value } set { defer { timestampedValues.append((Date(), value)) } value = newValue } } init(initialValue value: Value) { self.wrappedValue = value } }
Класс ExpenseReport позволяет сохранить временные метки состояний обработки отчёта о расходах.
class ExpenseReport { enum State { case submitted, received, approved, denied } @Versioned var state: State = .submitted }
Но пример выше демонстрирует серьёзное ограничение в текущей реализации обёрток свойств, которое вытекает из ограничения Swift: свойства не могут генерировать исключения. Если бы мы хотели добавить в @Versioned ограничение для предотвращения изменения значения на .approved после того, как оно приняло значения .denied, то наилучший вариант — fatalError(), который плохо подходит для реальных приложений.
class ExpenseReport { @Versioned var state: State = .submitted { willSet { if newValue == .approved, $state.timestampedValues.map { $0.1 }.contains(.denied) { fatalError("Ошибка") } } } } var tripExpenses = ExpenseReport() tripExpenses.state = .denied tripExpenses.state = .approved // Fatal error: «ошибка» и краш приложения.
Похожие идеи
@Auditedдля логирования доступа к свойству.@UserDefaultдля инкапсулирования механизма чтения и сохранения данных вUserDefaults.
Ограничения
Свойства не могут генерировать исключения
Как уже было сказано, обёртки свойств могут использовать лишь несколько методов обработки недопустимых значений:
- игнорировать их;
- завершить работу приложения при помощи fatalError().
Свойства, имеющие обёртку, не могут быть помечены атрибутом `typealias`
Пример @UnitInterval выше, свойство которого ограничено интервалом (0...1), не может быть объявлен как
typealias UnitInterval = Clamping(0...1)
Ограничение на использование композиции из нескольких обёрток свойств
Композиция обёрток свойств — не коммутативная операция: на поведение будет влиять порядок объявления. Рассмотрим пример, в котором свойство slug, представляющее собой url поста в блоге, нормализуется. В этом случае результат нормализации будет различаться в зависимости от того, когда пробелы будут заменены тире, до или после удаления пробелов. Поэтому на данный момент композиция из нескольких обёрток свойств не поддерживается.
@propertyWrapper struct Dasherized { private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.replacingOccurrences(of: " ", with: "-") } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { … @Dasherized @Trimmed var slug: String // error: multiple property wrappers are not supported }
Однако это ограничение можно обойти, если использовать вложенные обёртки свойств.
@propertyWrapper struct TrimmedAndDasherized { @Dasherized private(set) var value: String = "" var wrappedValue: String { get { value } set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) } } init(initialValue: String) { self.wrappedValue = initialValue } } struct Post { … @TrimmedAndDasherized var slug: String }
Другие ограничения обёрток свойств
- Нельзя использовать внутри протокола.
- Экземпляр свойства с обёрткой не может быть объявлен в
enum. - Свойство с обёрткой, объявленное внутри класса, не может быть переопределено другим свойством.
- Свойство с обёрткой не может быть
lazy,@NSCopying,@NSManaged,weakилиunowned. - Свойство с обёрткой должно быть единственным в рамках своего определения (т.е. нельзя
@Lazy var (x, y) = /* ... */). - У свойства с обёрткой нельзя определить
getterиsetter. - Типы у свойства
wrappedValueи у переменнойwrappedValueвinit(wrappedValue:)должны иметь тот же уровень доступа, что и тип обёртки свойства. - Тип свойство
projectedValueдолжен иметь тот же уровень доступа, что и тип обёртки свойства. init()должен иметь тот же уровень доступа, что и тип обёртки свойства.
Давайте подытожим. Обёртки свойств в Swift предоставляют авторам библиотек доступ к высокоуровневому поведению, ранее зарезервированному для языковых функций. Их потенциал для улучшения читаемости и уменьшения сложности кода огромен, и мы только поверхностно рассмотрели возможности этого инструмента.
Используете ли вы обёртки свойств в своих проектах? Пишите в комментариях!
ссылка на оригинал статьи https://habr.com/ru/company/funcorp/blog/485008/
Добавить комментарий