En Swift-struktur kan dela muterbara objekt efter tilldelning. För att hålla kopior oberoende använder du egenskaper av värdetyp eller döljer klasslagring bakom copy-on-write. Den här guiden bygger en syntetisk beräknare för plattrader för en förhandsvisning på Mac och kontrollerar att ändringar i en kopia lämnar de andra oförändrade.

Varför ändras originalet ändå när strukturen kopieras?

Anta att en förhandsvisning på en dator beräknar bredden på en rad med plattor. Du kopierar inställningarna för att prova en annan plattbredd och förväntar dig att den ursprungliga förhandsvisningen ska förbli oförändrad. Den förväntningen slår fel när båda instansvärdena innehåller en referens till samma muterbara objekt.

Swift kopierar en strukturs lagrade värden när den tilldelas till en annan variabel. För en klassegenskap är det lagrade värdet en referens: det refererade objektet förblir delat. Omslaget är fortfarande en struktur, men dess observerbara beteende kan innefatta delad mutering. Dessa regler följer av Swifts dokumenterade åtskillnad mellan värde- och referenstyper.

Det här fullständiga exemplet visar problemet. Det använder bara Swifts standardbibliotek, så inga importer krävs.

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

Att deklarera egenskapen eller omslaget med let fryser inte objektets muterbara egenskaper. Det förhindrar att referensen eller strukturvärdet byts ut. Swifts kapitel om strukturer och klasser skiljer uttryckligen mellan konstanta referenser och oföränderligt objekttillstånd.

Om du lägger dessa objekt i en matris bevaras också deras delade identitet. Matriskopior hanterar sina element oberoende, men ett element av klasstyp är i sig en referens. Om du ersätter ett element påverkas en matris; om du ändrar ett objekt som refereras av båda matriserna märks ändringen genom båda. Apple visar den här skillnaden i dokumentationen för Array.

Vilken design ger beräkningen oberoende värden?

Definiera kontraktet innan du väljer en implementation: efter var edited = original ska varje stödd ändring av endera layouten lämna den andra layoutens beräknade bredd oförändrad. Den som läser typen ska inte behöva veta om dess lagring delas internt.

Följande beslutsstöd tillämpar kontraktet på en lokal förhandsvisningsmodell. Det är en designrekommendation som härletts från de dokumenterade kopieringsreglerna.

Situation Lämplig design Villkor för att bevara oberoendet
Inställningarna består av tal, strängar eller andra komponenter med värdesemantik Lagra värdena direkt i strukturen Kontrollera att kapslade komponenter också ger oberoende observerbara värden
Befintlig klassbaserad lagring måste behållas Använd privat lagring med copy-on-write Varje ändring måste först koppla loss det delade muterbara tillståndet
En separat ögonblicksbild behövs bara vid en namngiven operation Tillhandahåll en uttrycklig kopieringsoperation Dokumentera att vanlig tilldelning fortfarande delar objekt, såvida inte omslaget också upprätthåller oberoende
Flera delar av programmet ska avsiktligt kunna ändra ett objekt Behåll referenssemantiken Beskriv beteendet med delade objekt i API:t

För tre heltalsinställningar är direkta egenskaper i strukturen det enklaste valet. Den klassbaserade implementationen nedan är användbar när du anpassar en befintlig lagringsdesign eller vill förstå hur gränsen fungerar; den visar inte på någon prestandafördel för den här lilla beräkningen.

Copy-on-write gör att två värden kan dela lagring tills ett av dem ändras. Apple beskriver hur man kapslar in en klass i en struktur och använder copy-on-write för att kombinera separat allokerad lagring med värdesemantik i Explore Swift performance. Att bara kapsla in klassen implementerar inte det beteendet.

En uttrycklig metod copy() lämnar också en lucka: anropare kan fortfarande använda vanlig tilldelning utan att anropa den. Om tilldelning ingår i ditt värdekontrakt måste omslagets ändringsvägar upprätthålla oberoendet automatiskt.

Hur implementerar och kontrollerar du copy-on-write?

Använd ett privat lagringsobjekt, skapa det inuti omslaget och exponera bara beräknade värden och kontrollerade ändringsmetoder. Anropa isKnownUniquelyReferenced omedelbart före ändring av lagringen. Om unikhet inte kan fastställas ersätter du lagringen med en oberoende kopia. Apple dokumenterar den här sekvensen specifikt för implementering av copy-on-write.

Beräkningsexemplet använder illustrativa indata: tre plattor som vardera är 40 punkter breda, med ett mellanrum på 10 punkter mellan intilliggande plattor. Det finns två mellanrum, så radens bredd är 3 * 40 + 2 * 10 = 140 punkter. Om plattbredden ökas till 60 blir bredden 200 punkter.

Om du vill prova hela programmet på en Mac kör du först swift --version i Terminal för att bekräfta att en Swift-verktygskedja finns tillgänglig, enligt rekommendationen i Swifts kom igång-guide. Spara följande kod som Layout.swift och kör sedan swift Layout.swift från dess katalog. Programmet beräknar resultat i minnet och skriver ut dem.

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

Programmet kräver inga importer eller externa paket. De tillåtna intervallen är avsiktliga exempelbegränsningar: en till 1 000 kolumner och noll till 1 000 punkter för bredd och mellanrum. De håller heltalsberäkningen inom gränserna. Ett argument utanför intervallet utlöser ett förvillkor; värdet begränsas inte tyst. Om dessa sättare senare tar emot redigerbar text från ett gränssnitt på datorn ska du validera indata innan du anropar dem och ge användaren möjlighet att rätta till dem.

Kontrollerna täcker tre observerbara krav: att ändringar i den kopierade layouten bevarar originalet, att ändringar i originalet bevarar kopian och att en sparad konstant ögonblicksbild behåller sitt resultat under en senare ändring. De testar beteendet utan att kräva ett visst antal allokeringar. Swift var inte tillgängligt i forskningsmiljön, så exemplet granskades men kördes inte där; resultatet som visas är det förväntade.

Vad måste fortsätta gälla när omslaget byggs ut?

Exemplets oberoende beror på hela dess API-gräns. Lagringen är privat, ingen initierare tar emot ett externt lagringsobjekt som behålls, och ingen getter returnerar ett sådant. Båda ändringsmetoderna går genom samma frånkopplingsoperation. Gettern beräknar ett resultat utan att ändra lagringen.

När du lägger till funktioner ska du följa varje ny väg som kan ändra observerbart tillstånd. En ny sättare, subskription eller redigeringsåteranrop behöver behandlas på samma sätt som de befintliga metoderna. En getter som returnerar ett muterbart underordnat objekt låter anropare kringgå frånkopplingen, även om gettern själv är skrivskyddad.

Lagringskopian måste också omfatta allt muterbart tillstånd som är relevant för värdet. Här kopierar Storage.copy() tre heltal, vilket räcker. Om lagringen senare innehåller ett muterbart underordnat objekt av klasstyp skulle en tilldelning av det objektet till den nya lagringen bevara ytterligare en delad referens. Kopiera underobjektet på lämpligt sätt eller ersätt det med en komponent som redan har värdesemantik. Beteendet för klasselement som dokumenteras för Array illustrerar samma gräns.

Använd följande sekvens när du utökar beräknaren:

  1. Kopiera en layout innan du provar varje ny redigeringsoperation.
  2. Kontrollera att den oförändrade layouten behåller sitt tidigare resultat.
  3. Upprepa med ombytta roller.
  4. Spara en ögonblicksbild, gör flera senare ändringar och kontrollera den igen.
  5. Granska varje returnerad egenskap och varje initierarargument efter muterbara referenser som kan ta sig ut ur omslaget.

En lagringsklass final hindrar underklasser från att lägga till beteende, men den gör inte dess egenskaper oföränderliga. På samma sätt tillåter mutating på en operation att den uppdaterar strukturen; det är frånkopplingslogiken som bevarar oberoendet i det här exemplet.

Hur ska Foundation-objekt kopieras?

Om en befintlig modell lagrar Foundation-objekt som NSMutableString ska du använda den konkreta typens dokumenterade kopieringsbeteende när du implementerar frånkoppling av lagringen. Swift-tilldelning anropar inte automatiskt Foundations kopieringsmetoder.

Foundations protokoll NSCopying definierar funktionell kopiering, medan den exakta betydelsen beror på klassen. Dokumentationen tillåter också att det ursprungliga objektet returneras när både klassen och dess innehåll är oföränderliga. Därför är objektidentitet i sig inte ett allmängiltigt test på om en kopia uppfyller ditt värdekontrakt.

När du behöver ett muterbart resultat från en typ med separata muterbara och oföränderliga former ska du läsa dokumentationen för NSMutableCopying. Apple dokumenterar mutableCopy(with:) och bekvämlighetsmetoden mutableCopy() för det ändamålet. Klasser utan den uppdelningen bör i stället implementera NSCopying.

För plattberäknaren är den praktiska gränsen enkel: ett Foundation-objekt som tas emot som indata får inte förbli ett externt muterbart alias till dina inställningar som ska vara oberoende. Skapa en lämplig oberoende ögonblicksbild vid inläsningen, koppla loss lagringen på rätt sätt före ändringar och returnera värdeögonblicksbilder i stället för det interna muterbara objektet. Kontrollera hur den konkreta klassen hanterar eventuellt kapslat muterbart innehåll innan du förlitar dig på dess kopieringsoperation.

Gör copy-on-write typen Sendable?

Värdeoberoende och konformans för samtidighet kräver separata resonemang. Apples Sendable-dokumentation kräver att lagrade medlemmar i en kontrollerad sendable-struktur själva är sendable. En privat muterbar lagringsklass uppfyller inte det kravet bara för att den omslutande typen är en struktur.

Unikhetskontrollen är inte heller synkronisering. Apple varnar för att isKnownUniquelyReferenced kan returnera true vid samtidig åtkomst och kräver lämplig synkronisering runt sådan användning. Den kontrollerar starka referenser; svaga eller obundna alias kan fortfarande finnas. Därför är det viktigt att lagringen inte går att komma åt utanför omslaget, utöver att unikheten kontrolleras. Se standardbiblioteksfunktionens dokumenterade begränsningar.

Begränsa det här exemplet till en synkron beräkningskontext. Om en senare version behöver skicka inställningar mellan samtidighetsdomäner är det enkla alternativet en struktur med sendable-egenskaper av värdetyp och kontrollerad Sendable-konformans. Att lägga till @unchecked Sendable överför ansvaret för korrektheten till implementatören; det tillför ingen saknad samordning. Apple förklarar kraven i referensen för Sendable.

För en liten beräkning på datorn bör du föredra direkta värdeegenskaper när de räcker för uppgiften. När muterbar klasslagring måste behållas ska du bevara kontraktet genom privat ägarskap, fullständiga lagringskopior, kontrollerade ändringar och observerbara oberoendekontroller. Resultatet blir förutsägbart: ändringar i en förhandsvisnings arbetsinställningar lämnar varje sparat layoutvärde med sitt eget beräknade resultat.

Källor