Jak podejść do opakowań w celu uzyskania właściwości Swift
Opublikowany: 2022-03-11Mówiąc prościej, opakowanie właściwości to ogólna struktura, która hermetyzuje dostęp do odczytu i zapisu do właściwości i dodaje do niej dodatkowe zachowanie. Używamy go, jeśli potrzebujemy ograniczyć dostępne wartości właściwości, dodać dodatkową logikę dostępu do odczytu/zapisu (np. przy użyciu baz danych lub wartości domyślnych użytkownika) lub dodać kilka dodatkowych metod.
Ten artykuł dotyczy nowego podejścia Swift 5.1 do zawijania właściwości, które wprowadza nową, czystszą składnię.
Stare podejście
Wyobraź sobie, że tworzysz aplikację i masz obiekt zawierający dane profilu użytkownika.
struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)
Chcesz dodać weryfikację adresu e-mail — jeśli adres e-mail użytkownika jest nieprawidłowy, właściwość email
musi wynosić nil
. Byłby to dobry przypadek, aby użyć otoki właściwości do hermetyzacji tej logiki.
struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }
Możemy użyć tego wrappera w strukturze Konta:
struct Account { var firstName: String var lastName: String var email: Email<String> }
Teraz jesteśmy pewni, że właściwość e-mail może zawierać tylko prawidłowy adres e-mail.
Wszystko wygląda dobrze, z wyjątkiem składni.
let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)
Dzięki opakowaniu właściwości składnia inicjowania, odczytywania i zapisywania takich właściwości staje się bardziej złożona. Czy można więc uniknąć tej komplikacji i używać opakowań właściwości bez zmiany składni? W przypadku Swift 5.1 odpowiedź brzmi: tak.
Nowy sposób: @propertyWrapper Adnotacja
Swift 5.1 zapewnia bardziej eleganckie rozwiązanie do tworzenia opakowań właściwości, gdzie dozwolone jest oznaczanie opakowania właściwości adnotacją @propertyWrapper
. Takie wrappery mają bardziej zwartą składnię w porównaniu z tradycyjnymi, co skutkuje bardziej zwartym i zrozumiałym kodem. Adnotacja @propertyWrapper
ma tylko jedno wymaganie: Twój obiekt opakowujący musi zawierać niestatyczną właściwość o nazwie wrappedValue
.
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
Aby zdefiniować tak opakowaną właściwość w kodzie, musimy użyć nowej składni.
@Email var email: String?
Tak więc oznaczyliśmy właściwość adnotacją @
email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil
Świetnie, teraz wygląda lepiej niż przy starym podejściu. Ale nasza implementacja opakowująca ma jedną wadę: nie zezwala na początkową wartość opakowanej wartości.
@Email var email: String? = "[email protected]" //compilation error.
Aby rozwiązać ten problem, musimy dodać następujący inicjator do opakowania:
init(wrappedValue value: Value?) { self.value = value }
I to wszystko.
@Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil
Ostateczny kod opakowania znajduje się poniżej:
@propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }
Konfigurowalne owijarki
Weźmy inny przykład. Piszesz grę i masz właściwość, w której przechowywane są wyniki użytkowników. Wymaganiem jest, aby ta wartość była większa lub równa 0 i mniejsza lub równa 100. Można to osiągnąć za pomocą opakowania właściwości.
@propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0
Ten kod działa, ale nie wydaje się ogólny. Nie możesz go ponownie użyć z różnymi ograniczeniami (nie 0 i 100). Co więcej, może ograniczać tylko wartości całkowite. Lepiej byłoby mieć jedno konfigurowalne opakowanie, które może ograniczać dowolny typ zgodny z protokołem Comparable. Aby nasz wrapper był konfigurowalny, musimy dodać wszystkie parametry konfiguracyjne za pomocą inicjatora. Jeśli inicjator zawiera atrybut wrappedValue
(wartość początkowa naszej właściwości), musi to być pierwszy parametr.

@propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }
Aby zainicjować opakowaną właściwość, definiujemy wszystkie atrybuty konfiguracji w nawiasach po adnotacji.
@Constrained(0...100) var scores: Int = 0
Liczba atrybutów konfiguracyjnych jest nieograniczona. Musisz je zdefiniować w nawiasach w tej samej kolejności, co w inicjatorze.
Uzyskiwanie dostępu do samego opakowania
Jeśli potrzebujesz dostępu do samego opakowania (nie do opakowanej wartości), musisz dodać podkreślenie przed nazwą właściwości. Weźmy na przykład naszą strukturę konta.
struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)
Potrzebujemy dostępu do samego wrappera, aby móc korzystać z dodatkowej funkcjonalności, którą do niego dodaliśmy. Na przykład chcemy, aby struktura konta była zgodna z protokołem Equatable. Dwa konta są równe, jeśli ich adresy e-mail są takie same, a w adresach e-mail nie można rozróżniać wielkości liter.
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }
Działa, ale nie jest to najlepsze rozwiązanie, ponieważ musimy pamiętać o dodaniu metody pisanej małymi literami (lowcased()) wszędzie tam, gdzie porównujemy e-maile. Lepszym sposobem byłoby ujednolicenie struktury wiadomości e-mail:
extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }
i porównaj wrappery zamiast opakowanych wartości:
extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }
Przewidywana wartość
Adnotacja @propertyWrapper
zawiera jeszcze jeden cukier składni — przewidywaną wartość. Ta właściwość może mieć dowolny typ. Aby uzyskać dostęp do tej właściwości, musisz dodać przedrostek $
do nazwy właściwości. Aby wyjaśnić, jak to działa, użyjemy przykładu z frameworka Combine.
@Published
tworzy wydawcę dla usługi i zwraca ją jako przewidywaną wartość.
@Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher
Jak widać, używamy wiadomości, aby uzyskać dostęp do opakowanej właściwości, oraz $message, aby uzyskać dostęp do wydawcy. Co należy zrobić, aby dodać przewidywaną wartość do opakowania? Nic specjalnego, po prostu to zadeklaruj.
@propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }
Jak wspomniano wcześniej, właściwość projectedValue
może mieć dowolny typ w zależności od potrzeb.
Ograniczenia
Składnia nowego opakowania właściwości wygląda dobrze, ale zawiera również kilka ograniczeń, z których najważniejsze to:
- Nie mogą uczestniczyć w obsłudze błędów. Opakowana wartość jest właściwością (nie metodą) i nie możemy oznaczyć metody pobierającej ani ustawiającej jako
throws
. Na przykład w naszym przykładzie wiadomościEmail
nie można zgłosić błędu, jeśli użytkownik spróbuje ustawić nieprawidłowy adres e-mail. Możemy zwrócićnil
lub zawiesić aplikację za pomocąfatalError()
, co w niektórych przypadkach może być nie do przyjęcia. - Stosowanie wielu opakowań do usługi jest niedozwolone. Na przykład lepiej byłoby mieć oddzielne opakowanie
@CaseInsensitive
i połączyć je z opakowaniem@Email
, zamiast nie uwzględniać wielkości liter w opakowaniu@Email
. Ale takie konstrukcje są zabronione i prowadzą do błędów kompilacji.
@CaseInsensitive @Email var email: String?
W ramach obejścia tego konkretnego przypadku możemy odziedziczyć opakowanie Email
z opakowania CaseInsensitive
. Dziedziczenie ma jednak również ograniczenia — tylko klasy obsługują dziedziczenie i dozwolona jest tylko jedna klasa bazowa.
Wniosek
Adnotacje @propertyWrapper
upraszczają składnię opakowań właściwości i możemy operować na opakowanych właściwościach w taki sam sposób, jak na zwykłych. Dzięki temu Twój kod jako programisty Swift jest bardziej kompaktowy i zrozumiały. Jednocześnie ma kilka ograniczeń, z którymi musimy się liczyć. Mam nadzieję, że niektóre z nich zostaną poprawione w przyszłych wersjach Swifta.
Jeśli chcesz dowiedzieć się więcej o właściwościach Swift, zapoznaj się z oficjalnymi dokumentami.