Een Swift-struct kan na toewijzing veranderbare objecten delen. Gebruik waarde-type-eigenschappen of verberg klasseopslag achter copy-on-write om kopieën onafhankelijk te houden. Deze gids bouwt een synthetische berekening voor tegelrijen voor een desktopvoorbeeld op de Mac en controleert of het bewerken van één kopie de andere ongewijzigd laat.

Waarom verandert het origineel nog steeds wanneer je de struct kopieert?

Stel dat een desktopvoorbeeld de breedte van een rij tegels berekent. Je kopieert de instellingen om een andere tegelbreedte uit te proberen, in de verwachting dat het oorspronkelijke voorbeeld ongewijzigd blijft. Die verwachting komt niet uit als beide instellingenwaarden een verwijzing naar hetzelfde veranderbare object bevatten.

Swift kopieert de opgeslagen waarden van een struct wanneer je die aan een andere variabele toewijst. Bij een klasse-eigenschap is die opgeslagen waarde een verwijzing: het verwezen object blijft gedeeld. De wrapper is nog steeds een struct, maar het waarneembare gedrag kan gedeelde wijzigingen omvatten. Deze regels volgen uit het gedocumenteerde onderscheid in Swift tussen waarde- en referentietypen.

Dit complete voorbeeld laat het probleem zien. Het gebruikt alleen de Swift-standaardbibliotheek, dus er zijn geen imports nodig.

final class TileDimensions {
    var width: Int

    init(width: Int) {
        self.width = width
    }
}

struct SharedLayout {
    let dimensions: TileDimensions
}

let original = SharedLayout(
    dimensions: TileDimensions(width: 40)
)
let edited = original
edited.dimensions.width = 60

assert(original.dimensions === edited.dimensions)
assert(original.dimensions.width == 60)
print(original.dimensions.width) // 60

Als je de eigenschap of wrapper declareert met let, worden de veranderbare eigenschappen van het object niet bevroren. Het voorkomt dat die verwijzing of struct-waarde wordt vervangen. In het hoofdstuk Structures and Classes maakt Swift expliciet onderscheid tussen constante verwijzingen en onveranderlijke objecttoestand.

Wanneer je deze objecten in een array zet, blijft hun gedeelde identiteit behouden. Kopieën van arrays beheren hun elementen onafhankelijk, maar een element met een klasse als waarde is zelf een verwijzing. Een element vervangen heeft effect op één array; een object wijzigen waarnaar beide arrays verwijzen, is in beide zichtbaar. Apple licht dit onderscheid toe in de documentatie van Array.

Welk ontwerp geeft de berekening onafhankelijke waarden?

Bepaal eerst het contract voordat je een implementatie kiest: na var edited = original moet elke ondersteunde wijziging aan een van beide layouts de berekende breedte van de andere layout ongemoeid laten. Gebruikers van het type hoeven niet te weten of de opslag intern gedeeld wordt.

De volgende besliswijzer past dat contract toe op een lokaal voorbeeldmodel. Het is een ontwerpaanbeveling die is afgeleid van de gedocumenteerde kopieerregels.

Situatie Geschikt ontwerp Voorwaarde om onafhankelijkheid te behouden
De instellingen bestaan uit getallen, strings of andere componenten met waardesemantiek Sla die waarden rechtstreeks op in de struct Controleer of geneste componenten ook onafhankelijke waarneembare waarden bieden
Bestaande opslag op basis van een klasse moet behouden blijven Gebruik private copy-on-write-opslag Elke wijziging moet eerst de gedeelde veranderbare toestand loskoppelen
Alleen bij een benoemde bewerking is een afzonderlijke snapshot nodig Bied een expliciete kopieerbewerking aan Documenteer dat gewone toewijzing nog steeds objecten deelt, tenzij de wrapper ook onafhankelijkheid afdwingt
Verschillende delen van het programma wijzigen bewust één object Behoud referentiesemantiek Beschrijf het gedrag met gedeelde objecten in de API

Voor drie instellingen met gehele getallen zijn rechtstreekse struct-eigenschappen de eenvoudigste keuze. De klassegebaseerde implementatie hieronder is nuttig bij het aanpassen van een bestaand opslagontwerp of om te leren hoe de grens werkt; ze biedt geen prestatievoordeel voor deze kleine berekening.

Met copy-on-write kunnen twee waarden opslag delen totdat een ervan wordt gewijzigd. Apple beschrijft hoe je een klasse in een struct kunt wrappen en copy-on-write kunt toepassen om afzonderlijk toegewezen opslag te combineren met waardesemantiek in Explore Swift performance. Alleen de klasse wrappen implementeert dit gedrag niet.

Een expliciete methode copy() laat ook een hiaat: aanroepers kunnen nog steeds gewone toewijzing gebruiken zonder die methode aan te roepen. Als toewijzing onderdeel is van je waardecontract, moeten de wijzigingspaden van de wrapper onafhankelijkheid automatisch afdwingen.

Hoe implementeer en controleer je copy-on-write?

Gebruik één private opslagobject, maak het binnen de wrapper aan en geef alleen berekende waarden en gecontroleerde wijzigingsmethoden vrij. Roep isKnownUniquelyReferenced direct vóór het wijzigen van de opslag aan. Als de uniciteit niet vaststaat, vervang je de opslag door een onafhankelijke kopie. Apple documenteert deze reeks specifiek voor de implementatie van copy-on-write.

De uitgewerkte berekening gebruikt illustratieve invoer: drie tegels, elk 40 punten breed, met een tussenruimte van 10 punten tussen aangrenzende tegels. Er zijn twee tussenruimtes, dus de rijbreedte is 3 * 40 + 2 * 10 = 140 punten. Een verhoging van de tegelbreedte naar 60 levert 200 punten op.

Om het complete programma op een Mac uit te proberen, voer je eerst swift --version uit in Terminal om te controleren of er een Swift-toolchain beschikbaar is, zoals aanbevolen in de Getting Started-gids van Swift. Sla de volgende code op als Layout.swift en voer vervolgens swift Layout.swift uit vanuit de map waarin het bestand staat. Het programma berekent alles in het geheugen en toont de resultaten.

struct TileLayout {
    private final class Storage {
        let columns: Int
        var tileWidth: Int
        var gap: Int

        init(columns: Int, tileWidth: Int, gap: Int) {
            self.columns = columns
            self.tileWidth = tileWidth
            self.gap = gap
        }

        func copy() -> Storage {
            Storage(
                columns: columns,
                tileWidth: tileWidth,
                gap: gap
            )
        }
    }

    private var storage: Storage

    init(columns: Int, tileWidth: Int, gap: Int) {
        precondition((1...1000).contains(columns))
        precondition((0...1000).contains(tileWidth))
        precondition((0...1000).contains(gap))
        storage = Storage(
            columns: columns,
            tileWidth: tileWidth,
            gap: gap
        )
    }

    var rowWidth: Int {
        storage.columns * storage.tileWidth
            + (storage.columns - 1) * storage.gap
    }

    private mutating func ensureUniqueStorage() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = storage.copy()
        }
    }

    mutating func setTileWidth(_ width: Int) {
        precondition((0...1000).contains(width))
        ensureUniqueStorage()
        storage.tileWidth = width
    }

    mutating func setGap(_ gap: Int) {
        precondition((0...1000).contains(gap))
        ensureUniqueStorage()
        storage.gap = gap
    }
}

var original = TileLayout(columns: 3, tileWidth: 40, gap: 10)
var edited = original

edited.setTileWidth(60)
assert(original.rowWidth == 140)
assert(edited.rowWidth == 200)

original.setGap(5)
assert(original.rowWidth == 130)
assert(edited.rowWidth == 200)

let snapshot = edited
edited.setGap(20)
assert(snapshot.rowWidth == 200)
assert(edited.rowWidth == 220)

print(original.rowWidth, snapshot.rowWidth, edited.rowWidth)
// Expected output: 130 200 220

Het programma vereist geen imports of externe pakketten. De toegestane bereiken zijn bewust gekozen voorbeeldbeperkingen: één tot 1.000 kolommen en nul tot 1.000 punten voor breedte en tussenruimte. Zo blijft de berekening met gehele getallen begrensd. Een argument buiten het bereik laat een precondition mislukken; het wordt niet stilzwijgend begrensd. Als deze setters later bewerkbare tekst uit een desktopinterface accepteren, valideer die invoer dan voordat je ze aanroept en bied de gebruiker een manier om de invoer te corrigeren.

De controles bestrijken drie waarneembare vereisten: als je de gekopieerde layout wijzigt, blijft het origineel behouden; als je het origineel wijzigt, blijft de kopie behouden; en als je een constante snapshot bewaart, blijft het resultaat daarvan behouden tijdens een latere wijziging. Ze testen gedrag zonder een bepaald aantal allocaties te vereisen. Swift was niet beschikbaar in de onderzoeksomgeving, dus het voorbeeld is daar nagekeken maar niet uitgevoerd; de getoonde uitvoer is het verwachte resultaat.

Wat moet waar blijven wanneer de wrapper groeit?

De onafhankelijkheid van het voorbeeld komt voort uit de volledige API-grens. De opslag is private, geen initializer accepteert een extern bewaard opslagobject en geen getter geeft er een terug. Beide wijzigingsmethoden gebruiken dezelfde loskoppelbewerking. De getter berekent een resultaat zonder de opslag te wijzigen.

Volg bij het toevoegen van functies elk nieuw pad dat de waarneembare toestand kan veranderen. Een nieuwe setter, subscript of callback voor bewerkingen vereist dezelfde behandeling als de bestaande methoden. Een getter die een veranderbaar onderliggend object teruggeeft, laat aanroepers de loskoppeling omzeilen, ook als de getter zelf alleen-lezen is.

De opslagkopie moet ook alle veranderbare toestand omvatten die relevant is voor de waarde. Hier kopieert Storage.copy() drie gehele getallen, wat volstaat. Als de opslag later een veranderbaar kindobject van een klasse bevat, blijft die verwijzing gedeeld als je dat kindobject aan de nieuwe opslag toewijst. Kopieer dat kindobject op de juiste manier of vervang het door een component die al waardesemantiek biedt. Het gedrag van klasse-elementen dat voor Array is gedocumenteerd, illustreert dezelfde grens.

Gebruik deze stappen wanneer je de calculator uitbreidt:

  1. Kopieer een layout voordat je elke nieuwe bewerkingsbewerking uitprobeert.
  2. Controleer of de ongewijzigde layout het eerdere resultaat behoudt.
  3. Herhaal dit met de rollen omgedraaid.
  4. Bewaar een snapshot, voer daarna verschillende wijzigingen uit en controleer de snapshot opnieuw.
  5. Inspecteer elke geretourneerde eigenschap en elk initializerargument op veranderbare verwijzingen die uit de wrapper kunnen ontsnappen.

Een opslagklasse met final voorkomt dat subclasses gedrag toevoegen, maar maakt de eigenschappen ervan niet onveranderlijk. Op dezelfde manier staat mutating bij een bewerking toe dat die de struct wijzigt; het is de loskoppellogica die de onafhankelijkheid van dit voorbeeld behoudt.

Hoe moeten Foundation-objecten worden gekopieerd?

Als een bestaand model Foundation-objecten zoals NSMutableString opslaat, gebruik dan het gedocumenteerde kopieergedrag van het concrete type bij het loskoppelen van de opslag. Swift-toewijzing roept niet automatisch kopieermethoden van Foundation aan.

Het Foundation-protocol NSCopying definieert functioneel kopiëren, terwijl de precieze betekenis afhangt van de klasse. Volgens de documentatie mag ook het oorspronkelijke object worden teruggegeven als zowel de klasse als de inhoud ervan onveranderlijk zijn. Objectidentiteit alleen is daarom geen universele test om te bepalen of een kopie aan je waardecontract voldoet.

Als je een veranderbaar resultaat nodig hebt van een type met afzonderlijke veranderbare en onveranderlijke vormen, raadpleeg dan NSMutableCopying. Apple documenteert daarvoor mutableCopy(with:) en de gemaksfunctie mutableCopy(). Klassen zonder dat onderscheid moeten in plaats daarvan NSCopying implementeren.

Voor de tegelcalculator is de praktische grens eenvoudig: een Foundation-object dat als invoer wordt geaccepteerd, mag geen extern veranderbare alias blijven naar je zogenaamd onafhankelijke instellingen. Maak bij invoer een geschikte onafhankelijke snapshot, koppel de opslag vóór wijzigingen op de juiste manier los en retourneer snapshots met waardesemantiek in plaats van het interne veranderbare object. Controleer hoe de concrete klasse eventuele geneste veranderbare inhoud behandelt voordat je op de kopieerbewerking vertrouwt.

Maakt copy-on-write het type Sendable?

Waardonafhankelijkheid en conformiteit voor concurrency vereisen afzonderlijke overwegingen. Volgens Apples documentatie over Sendable moeten de opgeslagen leden van een gecontroleerde sendable-struct sendable zijn. Een private veranderbare opslagklasse voldoet niet aan die eis alleen doordat het omringende type een struct is.

De uniciteitscontrole is ook geen synchronisatie. Apple waarschuwt dat isKnownUniquelyReferenced true kan teruggeven bij gelijktijdige toegang en vereist passende synchronisatie rond dit gebruik. De controle kijkt naar sterke verwijzingen; zwakke of unowned-aliases kunnen nog steeds bestaan. Daarom is het belangrijk dat de opslag buiten de wrapper ontoegankelijk blijft, naast de uniciteitscontrole. Zie de gedocumenteerde beperkingen van de standaardbibliotheekfunctie.

Houd dit voorbeeld binnen één synchrone berekeningscontext. Als een latere versie instellingen tussen concurrency-domeinen moet doorgeven, is een struct met sendable waarde-eigenschappen en gecontroleerde Sendable-conformiteit het eenvoudige alternatief. @unchecked Sendable toevoegen legt de verantwoordelijkheid voor correctheid bij de implementator; het biedt geen ontbrekende coördinatie. Apple legt deze vereisten uit in de referentie over Sendable.

Kies voor een kleine desktopberekening bij voorkeur rechtstreekse waarde-eigenschappen als die aan de vereisten voldoen. Als veranderbare klasseopslag moet blijven, behoud je het contract met privaat eigenaarschap, volledige opslagkopieën, gecontroleerde wijzigingen en controles op waarneembare onafhankelijkheid. Het resultaat is voorspelbaar: als je de werkinstellingen van een voorbeeld wijzigt, behoudt elke bewaarde layoutwaarde zijn eigen berekende resultaat.

Bronnen