Swift 5.1 führte Property Wrappers durch SE-0258 ein, um sich wiederholenden Zugriffs-Boilerplate-Code zu eliminieren. Die Anforderung für projectedValue wurde entwickelt, um sekundäre API-Oberflächen – wie SwiftUI's Binding oder Validierungszustände – über den verpackten Wert selbst hinaus offenzulegen. Diese Funktion ermöglicht es Entwicklern, Metadaten oder Projektionen mit der $-Präfix-Syntax zuzugreifen.
Das Problem entsteht, weil Swift die deklarative Syntax in gültiges SIL (Swift Intermediate Language) transformieren muss, ohne Namenskonflikte einzuführen oder die Zugriffssteuerung zu brechen. Der Compiler muss Speicher synthetisieren, der die Wertsemantiken für die verpackte Eigenschaft beibehält, während er möglicherweise Referenzsemantiken über die Projektion offenbart und gleichzeitig sicherstellt, dass der mit $-Präfix bezeichnete Identifikator nicht mit benutzerdefinierten Mitgliedern in Konflikt steht.
Die Lösung umfasst eine Quell-zu-Quell-Desugaring. Für eine als @Wrapper var property: T deklarierte Eigenschaft generiert der Compiler drei verschiedene Mitglieder. Zuerst eine private Speicher-Variable _property vom Typ Wrapper<T>. Zweitens, eine berechnete Eigenschaft property, die get/set-Operationen an _property.wrappedValue weiterleitet. Drittens eine berechnete Eigenschaft $property, die _property.projectedValue zurückgibt. Die mit $-Präfix gekennzeichnete Eigenschaft erbt die Zugriffssteuerung der ursprünglichen Deklaration und der Compiler stellt sicher, dass projectedValue existiert, wenn die $-Syntax verwendet wird.
@propertyWrapper struct Validating<T> { var wrappedValue: T var projectedValue: ValidationState<T> init(wrappedValue: T) { self.wrappedValue = wrappedValue self.projectedValue = ValidationState(value: wrappedValue) } } // Wird zu: struct Form { private var _username: Validating<String> var username: String { get { _username.wrappedValue } set { _username.wrappedValue = newValue } } var $username: ValidationState<String> { get { _username.projectedValue } } }
Wir entwarfen eine Anwendung zur Eingabe medizinischer Daten, bei der jedes Feld sowohl seinen aktuellen Wert als auch eine komplexe Validierungshistorie, die frühere Fehler und Korrekturzeitstempel umfasste, verfolgen musste. Die Herausforderung bestand darin, zwei verschiedene Datenpfade von einer einzigen Eigenschaftenabstraktion offenzulegen: den Roh-String für das UI-Textfeld und die Validierungshistorie für Analysen und Fehlermeldungen.
Der erste Ansatz war die Pflege eines parallelen Wörterbuchs, das Eigenschaftsnamen auf ValidationHistory-Objekte abbildete. Dies bot Flexibilität in der Speicherung, führte jedoch zu schwach typisierten APIs, die während der Refaktorisierung kaputt gingen und eine manuelle Synchronisierung zwischen dem Wörterbuch und den tatsächlichen Eigenschaftswerten erforderten. Das Risiko einer Desynchronisierung, das zu veralteten Fehlermeldungen führte, war für medizinische Daten inakzeptabel hoch.
Der zweite Ansatz sah vor, eine Wrapper-Struktur zu erstellen, die sowohl den Wert als auch die Historie enthielt, und diesen zusammengesetzten Typ als Eigenschaftstyp zu verwenden. Während dies typsicher war, verschmutzte es das Domänenmodell mit Validierungsbelangen und zwang jeden Zugriffspunkt, das Entpacken zu handhaben, was den Zweck der sauberen Trennung von UI und Geschäftslogik untergrub.
Der dritte Ansatz nutzte einen benutzerdefinierten @Validated-Property-Wrapper mit einem projectedValue, der einen Referenztyp von ValidationHistory zurückgab. Dies kapselte die Synchronisation intern, während $fieldName für den Zugriff auf die Historie offengelegt wurde. Wir wählten dies, weil es CoW (Copy-on-Write)-Semantiken für den verpackten String-Wert beibehielt und gleichzeitig eine stabile Referenzidentität für die Validierungshistorie bot, sodass UI-Komponenten Änderungen beobachten konnten, ohne Kopieraufwand.
Das Ergebnis beseitigte eine ganze Klasse von Synchronisationsfehlern und reduzierte die validierungsbezogene Codebasis um 35 %. Die $-Syntax bot intutive Entdeckbarkeit für weniger erfahrene Entwickler, und die Kompilierungszeitdurchsetzung verhinderte das unbeabsichtigte Offenlegen von Implementierungsdetails über Modulgrenzen hinweg.
Warum bleiben Änderungen an einem werttypischen projectedValue nicht erhalten, wenn sie über das Dollarzeichen-Präfix zugegriffen werden?
Wenn der Property Wrapper eine Struktur ist, gibt der projectedValue-Getter eine Kopie des Wertes zurück. Wenn projectedValue eine Struktur (wie ein Int oder eine benutzerdefinierte Validierungsstatusstruktur) zurückgibt, verändern Anweisungen wie $property.errorCount += 1 eine temporäre Kopie, die sofort verworfen wird. Um persistente Veränderungen zu ermöglichen, muss projectedValue einen Referenztyp zurückgeben, oder der Wrapper muss CoW mit einem klassenbasierten Speicher implementieren. Alternativ kann ein Binding oder ein veränderlicher Zeiger zurückgegeben werden, der Indirektion bietet. Anfänger gehen oft davon aus, dass $property mutable Zugriffe auf den internen Zustand des Wrappers gewährt, ohne Swifts Wertsemantiken zu berücksichtigen.
Wie interagiert die Zugriffssteuerung der synthetisierten Dollarzeichen-Eigenschaft mit dem Zugriffslevel der ursprünglichen Eigenschaft?
Der Compiler synthetisiert die mit $-Präfix versehene Eigenschaft mit identischer Zugriffssteuerung zu der ursprünglichen Eigenschaft. Wenn Sie public @Wrapper var name: String deklarieren, sind sowohl name als auch $name public. Im Gegensatz dazu erzeugen private Eigenschaften private projizierte Werte. Kandidaten versuchen häufig, den verpackten Wert öffentlich zu machen, während sie den projizierten Wert intern oder privat halten möchten, was in den aktuellen Swift-Versionen unmöglich ist. Der Workaround erfordert, die Eigenschaft private zu machen und den verpackten Wert über eine explizite berechnete Eigenschaft offenzulegen, während der projizierte Wert eingeschränkt bleibt.
Kann ein einzelner Property Wrapper mehrere unterschiedliche Projektionen offenlegen, und was sind die ergonomischen Implikationen?
Swift erlaubt strikt nur eine projectedValue-Eigenschaft pro Wrapper. Diese Eigenschaft kann jedoch ein Tupel, eine Struktur oder ein Enum zurückgeben, das mehrere Werte enthält (z. B. projectedValue: (Binding<T>, ValidationError?, Bool)). Der ergonomische Kompromiss besteht darin, dass $property dann Punkt-Syntax benötigt, um auf Komponenten zuzugreifen ($property.0, $property.isValid), was die Lesbarkeit verringert. Einige Kandidaten versuchen, mehrere projectedValue-Eigenschaften zu deklarieren oder mehrere Property Wrappers auf derselben Eigenschaft anzuwenden (Kettenbildung). Während Kettenbildung unterstützt wird, schafft dies komplexe Initialisierungsemantiken und undurchsichtige Typinferenzprobleme. Der empfohlene Ansatz für mehrere Projektionen besteht darin, eine dedizierte Projektionstruktur mit benannten Eigenschaften zurückzugeben, um die Typsicherheit zu bewahren und den Syntaxaufwand zu akzeptieren.