Schnelle Eigenschaftenverpackungen

Wenn Sie SwiftUI verwendet haben, haben Sie wahrscheinlich auf Schlüsselwörter wie @ObservedObject, @EnvironmentObject, @FetchRequest usw. geachtet. Property Wrapper (im Folgenden als "Property Wrapper" bezeichnet) ist eine neue Funktion von Swift 5.1. In diesem Artikel erfahren Sie, woher alle Konstruktionen von @ stammen und wie Sie sie in SwiftUI und in Ihren Projekten verwenden.



Übersetzt von: Evgeny Zavozhansky, Entwickler von FunCorp.


Hinweis: Zum Zeitpunkt der Erstellung der Übersetzung hatte ein Teil des Quellcodes des Originalartikels aufgrund von Änderungen in der Sprache seine Relevanz verloren, sodass einige Codebeispiele absichtlich ersetzt wurden.


Property Wrapper wurden erstmals im März 2019 in Swift- Foren vorgestellt, einige Monate vor der Ankündigung von SwiftUI. In seinem ursprünglichen Vorschlag beschrieb Douglas Gregor, ein Mitglied des Swift Core-Teams, dieses Konstrukt (damals als Eigenschaftendelegierte bezeichnet) als "eine benutzerfreundliche Verallgemeinerung der Funktionalität, die derzeit von einem Sprachkonstrukt wie z. B. lazy bereitgestellt wird".


Wenn eine Eigenschaft mit dem Schlüsselwort lazy deklariert wird, bedeutet dies, dass sie beim ersten Zugriff initialisiert wird. Beispielsweise könnte eine verzögerte Eigenschaftsinitialisierung unter Verwendung einer privaten Eigenschaft implementiert werden, auf die über eine berechnete Eigenschaft zugegriffen wird. Die Verwendung des Schlüsselworts " lazy erleichtert dies jedoch erheblich.


 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 erklärt den Entwurf und die Implementierung von Property Wrappern perfekt. Anstatt zu versuchen, die Beschreibung in der offiziellen Dokumentation zu verbessern, sollten Sie einige Beispiele betrachten, die mit Eigenschaftenwrappern implementiert werden können:


  • Einschränkung von Eigenschaftswerten;
  • Umwandlung von Werten beim Ändern von Eigenschaften;
  • Gleichheitssemantik ändern und Eigenschaften vergleichen;
  • Eigenschaftenzugriffsprotokollierung.

Eigenschaftswerte begrenzen


SE-0258: Property Wrapper enthält mehrere praktische Beispiele, darunter @Clamping , @Copying , @Atomic , @ThreadSpecific , @Box , @UserDefault . Betrachten Sie den @Clamping Wrapper, mit dem Sie den Maximal- oder Minimalwert einer Eigenschaft begrenzen können.


 @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 kann beispielsweise der Säuregehalt einer Lösung modelliert werden, deren Wert einen Wert von 0 bis 14 annehmen kann.


 struct Solution {    @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68) 

Wenn Sie versuchen, den pH-Wert außerhalb des Bereichs von (0...14) einzustellen, nimmt die Eigenschaft den Wert an, der dem minimalen oder maximalen Intervall am nächsten liegt.


 let superDuperAcid = Solution(pH: -1) superDuperAcid.pH // 0 

Eigenschaftenwrapper können verwendet werden, um andere Eigenschaftenwrapper zu implementieren. Beispielsweise begrenzt der Wrapper @UnitInterval den Wert einer Eigenschaft mithilfe von @Clamping(0...1) auf das Intervall @Clamping(0...1) :


 @propertyWrapper struct UnitInterval<Value: FloatingPoint> {    @Clamping(0...1)    var wrappedValue: Value = .zero    init(initialValue value: Value) {        self.wrappedValue = value    } } 

Ähnliche Ideen


  • @Positive / @NonNegative gibt an, dass der Wert entweder eine positive oder eine negative Zahl sein kann.
  • @NonZero gibt an, dass der Wert der Eigenschaft nicht 0 sein kann.
  • @Validated oder @Whitelisted / @Blacklisted schränkt den Wert einer Eigenschaft auf bestimmte Werte ein.

Konvertieren von Werten beim Ändern von Eigenschaften


Das Validieren von Textfeldwerten bereitet Anwendungsentwicklern ständig Kopfzerbrechen. Es gibt so viele Dinge zu beachten: von Plattitüden wie der Codierung bis zu böswilligen Versuchen, einen Code über ein Textfeld einzugeben. Verwenden Sie einen Eigenschaftenwrapper, um Leerzeichen zu entfernen, die ein Benutzer am Anfang und Ende einer Zeile eingegeben hat.


 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 bietet die trimmingCharacters(in:) -Methode an, mit der Sie Leerzeichen am Anfang und Ende einer Zeile entfernen können. Sie können diese Methode immer dann aufrufen, wenn Sie die Richtigkeit der Eingabe gewährleisten müssen, dies ist jedoch nicht sehr praktisch. Hierfür können Sie den Property Wrapper verwenden.


 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" 

Ähnliche Ideen


  • @Transformed wendet die ICU-Konvertierung auf die @Transformed .
  • @Rounded / @Truncated rundet oder @Truncated einen Zeichenfolgenwert ab.

Ändern Sie die Semantik von Gleichheit und Eigenschaftsvergleich


In Swift sind zwei Saiten gleich, wenn sie kanonisch äquivalent sind , d. H. enthalten die gleichen Zeichen. Angenommen, wir möchten, dass die Zeichenfolgeneigenschaften gleich sind und nicht zwischen Groß- und Kleinschreibung unterscheiden.


@CaseInsensitive implementiert einen Wrapper für Eigenschaften vom Typ String oder 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 

Ähnliche Ideen


  • @Approximate für einen groben Vergleich von Eigenschaften des Typs Double oder Float.
  • @Ranked für Eigenschaften, deren Werte in der @Ranked Reihenfolge sind (z. B. der Rang der Spielkarten).

Protokollierung des Eigenschaftenzugriffs


@Versioned können Sie zugewiesene Werte abfangen und sich merken, wann sie festgelegt wurden.


 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    } } 

Mit der ExpenseReport Klasse können ExpenseReport Zeitstempel der Verarbeitungszustände der Spesenabrechnung speichern.


 class ExpenseReport {    enum State { case submitted, received, approved, denied }    @Versioned var state: State = .submitted } 

Das obige Beispiel zeigt jedoch eine schwerwiegende Einschränkung in der aktuellen Implementierung von Eigenschaftenwrappern, die sich aus der Swift-Einschränkung ergibt: Eigenschaften können keine Ausnahmen auslösen. Wenn wir @Versioned eine Einschränkung hinzufügen @Versioned , um zu verhindern, dass der Wert nach dem .approved des Werts .denied in .approved .denied , ist fatalError() die beste Option, die für echte Anwendungen nicht geeignet ist.


 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: «»   . 

Ähnliche Ideen


  • @Audited , @Audited Eigenschaftenzugriff zu @Audited .
  • @UserDefault , um den Mechanismus zum Lesen und Speichern von Daten in UserDefaults .

Einschränkungen


Eigenschaften können keine Ausnahmen auslösen


Wie bereits erwähnt, können Eigenschafts-Wrapper nur wenige Methoden zum Verarbeiten ungültiger Werte verwenden:


  • ignoriere sie;
  • Beenden Sie die Anwendung mit fatalError ().

Umbrochene Eigenschaften können nicht mit dem Attribut `typealias` gekennzeichnet werden


Das @UnitInterval Beispiel @UnitInterval , dessen Eigenschaft durch das Intervall (0...1) , kann nicht als deklariert werden


 typealias UnitInterval = Clamping(0...1) 

Beschränkung der Verwendung von Zusammensetzungen aus mehreren Eigenschaftenverpackungen


Das Erstellen von Eigenschaftenwrappern ist keine kommutative Operation: Die Reihenfolge der Deklaration wirkt sich auf das Verhalten aus. Stellen Sie sich ein Beispiel vor, in dem die Eigenschaft slug, die die URL eines Blogposts darstellt, normalisiert ist. In diesem Fall hängt das Ergebnis der Normalisierung davon ab, wann die Leerzeichen vor oder nach dem Entfernen von Leerzeichen durch Bindestriche ersetzt werden. Daher wird eine Komposition mehrerer Property Wrapper derzeit nicht unterstützt.


 @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 } 

Diese Einschränkung kann jedoch durch die Verwendung von Wrappern für verschachtelte Eigenschaften umgangen werden.


 @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 } 

Andere Einschränkungen für Eigenschaftenwrapper


  • Kann nicht innerhalb des Protokolls verwendet werden.
  • Eine Wrapper-Eigenschaftsinstanz kann nicht in enum deklariert werden.
  • Eine in einer Klasse deklarierte umschlossene Eigenschaft kann nicht von einer anderen Eigenschaft überschrieben werden.
  • Eine @NSCopying Eigenschaft kann nicht lazy , @NSCopying , @NSManaged , weak oder nicht im unowned .
  • Eine umschlossene Eigenschaft sollte die einzige in ihrer Definition sein (d. H. @Lazy var (x, y) = /* ... */ ).
  • Für eine umschlossene Eigenschaft kann kein getter und setter definiert werden.
  • Die Typen der wrappedValue Eigenschaft und der wrappedValue Variablen in init(wrappedValue:) müssen dieselbe Zugriffsebene haben wie der Wrapper-Typ der Eigenschaft.
  • Die type-Eigenschaft von projectedValue muss dieselbe Zugriffsebene wie der Wrapper-Typ der Eigenschaft haben.
  • init() muss dieselbe Zugriffsebene wie der Property-Wrapper-Typ haben.

Lassen Sie uns zusammenfassen. Eigenschaftenwrapper in Swift ermöglichen Bibliotheksautoren den Zugriff auf das allgemeine Verhalten, das zuvor für Sprachfunktionen reserviert war. Ihr Potenzial zur Verbesserung der Lesbarkeit und zur Reduzierung der Codekomplexität ist riesig, und wir haben die Funktionen dieses Tools nur oberflächlich untersucht.


Verwenden Sie Property Wrapper in Ihren Projekten? Schreibe in die Kommentare!

Source: https://habr.com/ru/post/de485008/


All Articles