Swift 5.1 introduceerde property wrappers via SE-0258 om repetitieve toegangscode te elimineren. De projectedValue vereiste was ontworpen om secundaire API-oppervlakken - zoals SwiftUI's Binding of validatietoestanden - bloot te leggen, buiten de verpakte waarde zelf. Deze functie stelt ontwikkelaars in staat om metadata of projecties te benaderen met behulp van de $ prefixsyntax.
Het probleem doet zich voor omdat Swift declaratieve syntax moet transformeren in geldige SIL (Swift Intermediate Language) zonder naamconflicten of het doorbreken van toegangsbepalingen. De compiler moet opslag synthetiseren die waarde-semantiek voor de verpakte eigenschap behoudt, terwijl mogelijk referentiesemantiek via de projectie wordt blootgesteld, en dit alles terwijl ervoor wordt gezorgd dat de $ prefix geïdentificeerde naam niet in conflict komt met door de gebruiker gedefinieerde leden.
De oplossing omvat source-to-source desugaring. Voor een eigenschap die is gedeclareerd als @Wrapper var property: T, genereert de compiler drie verschillende leden. Ten eerste, een private opslagvariabele _property van het type Wrapper<T>. Ten tweede, een berekende eigenschap property die get/set-operaties doorstuurt naar _property.wrappedValue. Ten derde, een berekende eigenschap $property die _property.projectedValue retourneert. De $ prefix-eigenschap erft de toegangsbepaling van de oorspronkelijke verklaring, en de compiler handhaaft dat projectedValue bestaat wanneer de $ syntax wordt gebruikt.
@propertyWrapper struct Validating<T> { var wrappedValue: T var projectedValue: ValidationState<T> init(wrappedValue: T) { self.wrappedValue = wrappedValue self.projectedValue = ValidationState(value: wrappedValue) } } // Desugars naar: struct Form { private var _username: Validating<String> var username: String { get { _username.wrappedValue } set { _username.wrappedValue = newValue } } var $username: ValidationState<String> { get { _username.projectedValue } } }
We waren bezig met het architecteren van een medische gegevensinvoerapplicatie waarbij elk veld zowel de huidige waarde als een complexe validatiegeschiedenis, inclusief eerdere fouten en correctietijdstempels, moest bijhouden. De uitdaging vereiste het blootleggen van twee verschillende gegevenspaden vanuit een enkele eigenschapsabstractie: de ruwe tekenreeks voor het UI-tekstveld en de validatiegeschiedenis voor analytics en foutweergave.
De eerste benadering was het bijhouden van een parallelle woordenboek die eigenschapsnamen aan ValidationHistory-objecten koppelde. Dit bood flexibiliteit in opslag, maar introduceerde string-typed API's die tijdens refactoring faalden en handmatige synchronisatie tussen het woordenboek en de werkelijke eigenschapswaarden vereisten. Het risico van desynchronisatie leidend tot verouderde foutweergaven was onaanvaardbaar hoog voor medische gegevens.
De tweede benadering bestond uit het creëren van een wrapper-struct die zowel de waarde als de geschiedenis bevatte, en vervolgens dat samengestelde type als de eigenschapstype gebruikte. Hoewel typeveilig, verstoorde dit het domeinmodel met validatieproblemen en dwong elke toegangssite om het uitpakken te behandelen, waardoor het doel van schone architecturale scheiding tussen UI en bedrijfslogica werd ondermijnd.
De derde benadering gebruikte een aangepaste @Validated property wrapper met een projectedValue dat een referentietype voor ValidationHistory retourneert. Dit omvatte de synchronisatie intern terwijl $fieldName werd blootgesteld voor geschiedenis toegang. We kozen dit omdat het CoW (Copy-on-Write) semantiek voor de verpakte tekenreekswaarde behield terwijl het een stabiele referentie identiteit voor de validatiegeschiedenis bood, wat ervoor zorgde dat UI-componenten wijzigingen konden observeren zonder kopieer-overhead.
Het resultaat was de eliminatie van een hele klasse synchronisatiefouten en een vermindering van de validatiegerelateerde codebasis met 35%. De $ syntax bood intuïtieve ontdekbaarheid voor junior ontwikkelaars, en de compilatietijd handhaving voorkwam accidentele blootstelling van implementatiedetails over modulegrenzen.
Waarom blijven mutaties aan een waarde-type projectedValue niet bestaan wanneer ze via de dollar-tekenprefix worden benaderd?
Wanneer de property wrapper een struct is, retourneert de projectedValue getter een kopie van de waarde. Als projectedValue een struct retourneert (zoals een Int of een aangepaste validatiestatus struct), muteren uitspraken zoals $property.errorCount += 1 een tijdelijke kopie die onmiddellijk wordt weggegooid. Om persistente mutaties mogelijk te maken, moet de projectedValue een referentietype retourneren, of de wrapper moet CoW implementeren met een class-gebaseerde opslag. Alternatief kan een Binding of mutabele pointer worden geretourneerd die indirection biedt. Beginners gaan vaak ervan uit dat $property mutabele toegang biedt tot de interne staat van de wrapper zonder rekening te houden met Swift's waarde-semantiek.
Hoe interageert de toegangsbepaling van de gesynthetiseerde dollar-teken eigenschap met het toegangsniveau van de originele eigenschap?
De compiler synthetiseert de $ prefix-eigenschap met dezelfde toegangsbepaling als de oorspronkelijke eigenschap. Als je public @Wrapper var name: String declareert, zijn zowel name als $name public. Omgekeerd genereert private eigenschappen private geprojecteerde waarden. Kandidaten proberen vaak de verpakte waarde openbaar te maken terwijl ze de geprojecteerde waarde intern of privé houden, wat onmogelijk is in de huidige Swift versies. De oplossing vereist het maken van de eigenschap private en het blootstellen van de verpakte waarde via een expliciete berekende eigenschap, terwijl de geprojecteerde waarde beperkt blijft.
Kan een enkele property wrapper meerdere verschillende projecties blootstellen, en wat zijn de ergonomische implicaties?
Swift staat strikt slechts één projectedValue eigenschap per wrapper toe. Die eigenschap kan echter een tuple, struct of enum retourneren die meerdere waarden bevat (bijv. projectedValue: (Binding<T>, ValidationError?, Bool)). De ergonomische afweging is dat $property vervolgens punt-syntax vereist om componenten te benaderen ($property.0, $property.isValid), wat de leesbaarheid vermindert. Sommige kandidaten proberen meerdere projectedValue eigenschappen te declareren of meerdere property wrappers op dezelfde eigenschap toe te passen (chainen). Hoewel chainen wordt ondersteund, creëert het complexe initialisatie semantiek en ondoorzichtige type-inferentieproblemen. De aanbevolen benadering voor meerdere projecties is het retourneren van een toegewijde projectie-struct met benoemde eigenschappen, zodat typeveiligheid wordt behouden terwijl de syntax-overhead wordt geaccepteerd.