Håll egenskaperna som används för likhet och hashning stabila medan ett objekt används som nyckel i en ordbok. För en lokal Swift-beräkning på din Mac väljer du objektidentitet, ett oföränderligt ID eller en värdeögonblicksbild beroende på vad uppslagningen ska innebära.
Den här genomgången använder syntetiska rektanglar och små areaberäkningar som helt och hållet hålls i minnet. Den praktiska uppgiften är att låta en rektangels etikett eller mått ändras utan att den tillhörande ordboksposten går förlorad – och utan att förväxla en lyckad uppslagning med en uppdaterad beräkning.
Varför kan en ändring av ett objekt göra att en uppslagning i en ordbok misslyckas?
Swifts Dictionary är en hashtabell vars nycklar följer protokollet Hashable. En uppslagning med en nyckel returnerar ett valfritt värde eftersom den efterfrågade nyckeln kanske saknas. Hashning hjälper till att hitta en post; likhet avgör vilka nycklar som räknas som likvärdiga. Se Apples dokumentation för Dictionary.
Kontraktet för Hashable kopplar samman dessa operationer: lika instanser måste mata in samma väsentliga komponenter i Hasher, i samma ordning. När du implementerar en anpassad nyckel använder du egenskaperna som definierar likhet i hash(into:). Apple beskriver detta krav i Hashable.
En nyckel av referenstyp innebär ytterligare en sak att tänka på. När en klassinstans tilldelas skickas en referens till det befintliga objektet i stället för att ett oberoende objekt skapas. Att deklarera referensen med let låser inte dess ändringsbara egenskaper. Det här är språkregler som dokumenteras i Structures and Classes.
Anta att en rektangelklass jämför och hashberäknar sin redigerbara etikett. Du lägger in objektet när etiketten är \"Tile A\" och ändrar sedan etiketten till \"Tile B\". Den lagrade referensen visar nu ett annat nyckeldefinierande tillstånd. Ordbokens ursprungliga placering och senare uppslagningar stämmer inte längre stabilt överens.
Designregeln följer av hashtabellens och likhetskontraktets krav: håll nyckelns likhetsdefinierande tillstånd stabilt så länge den finns i ordboken. Apple anger motsvarande krav uttryckligen för Foundation-objekt: ett objekts hash får inte ändras medan objektet tillhör en samling som använder hashen för placeringen. Se NSObjectProtocol.hash.
Bygg inte ett test kring antagandet att varje ogiltig ändring omedelbart returnerar nil. När nyckelkontraktet har brutits räcker en till synes lyckad uppslagning inte som belägg för att designen fungerar. Testa i stället de avsedda likhetsreglerna och det stabila beteendet.
Vilken nyckel passar uppgiften?
Bestäm vad en ordbokspost ska betyda innan du implementerar protokollöverensstämmelsen. För ett verktyg som beräknar rektangelareor leder de här tre frågorna till olika designval:
| Vad ska en post representera? | Lämplig nyckel | Vad händer när rektangeln ändras? |
|---|---|---|
| Just det här rektangelobjektet | Klassnyckel baserad på identitet | Posten förblir kopplad till den instansen |
| En logisk rektangel som identifieras oberoende av sitt aktuella objekt | Oföränderligt ID-värde | Posten förblir kopplad till ID:t |
| En beräkning för vissa mått | Oföränderlig ögonblicksbild av måtten | Ändrade mått ger en annan uppslagningsnyckel |
Tabellen är ett designstöd som bygger på Swifts regler för identitet, värden och hashning. Den skiljer också åt två skyldigheter: att hålla en koppling åtkomlig och att hålla ett beräknat resultat aktuellt. En stabil identitetsnyckel löser det första; omberäkning eller en fullständig ögonblicksbild av indata hanterar det andra.
Använd objektidentitet när posten hör till en viss instans
För en koppling i minnet som ska följa ett objekt när det redigeras definierar du likhet med === och hashberäknar dess ObjectIdentifier. Swifts identitetsoperator testar om referenser pekar på samma instans. Apple dokumenterar ObjectIdentifier som en hashbar identifierare vars jämförelser är giltiga under instansens livstid. Se Structures and Classes och ObjectIdentifier.
Följande är ett fullständigt synkront program. Det kräver en Mac med Swifts kommandoradsverktyg tillgängliga och använder endast Swifts standardbibliotek, som importeras automatiskt. Spara det som IdentityKeys.swift och kör swift IdentityKeys.swift i Terminal. Exemplen granskades, men kunde inte köras i forskningsmiljön eftersom Swift inte fanns tillgängligt.
final class Rectangle: Hashable {
var label: String
var width: Int
var height: Int
init(label: String, width: Int, height: Int) {
self.label = label
self.width = width
self.height = height
}
static func == (lhs: Rectangle, rhs: Rectangle) -> Bool {
lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
func demonstrateIdentityKeys() {
let rectangle = Rectangle(label: "Tile A", width: 4, height: 3)
let alias = rectangle
let separate = Rectangle(label: "Tile A", width: 4, height: 3)
var areas: [Rectangle: Int] = [
rectangle: rectangle.width * rectangle.height
]
rectangle.label = "Tile B"
assert(areas[alias] == 12)
assert(areas[separate] == nil)
rectangle.width = 5
assert(areas[rectangle] == 12) // Still the previous calculation.
areas[rectangle] = rectangle.width * rectangle.height
assert(areas[rectangle] == 15)
assert(areas.count == 1)
print("Renamed lookup: 12")
print("Recalculated area: \(areas[rectangle]!)")
}
demonstrateIdentityKeys()
Förväntad utdata är Renamed lookup: 12, följt av Recalculated area: 15. Påståendena uttrycker det användbara kontraktet: ett alias hittar den befintliga posten, en separat skapad rektangel gör det inte, och redigering av det ursprungliga objektet skapar ingen ny nyckel.
Breddändringen lämnar avsiktligt den lagrade arean på 12 tills programmet tilldelar 15. Identiteten förblir stabil, men ordboken känner inte av aritmetiska beroenden och räknar inte om värden. Bestäm om programmet ska uppdatera värdet direkt efter en redigering eller ta bort det fram till nästa beräkning.
Likhet baserad på identitet är ett beslut som gäller hela typen. Varje jämförelse med == för den här klassen betyder nu ”samma instans”. Om modellen redan definierar likhet utifrån mått eller ett annat värde använder du i stället en separat nyckeldesign, i stället för att ändra betydelsen bara för en viss ordbok.
Du kan också använda ObjectIdentifier direkt som ordboksnyckel, men då innehåller identifieraren ensam inte rektangeln som behövs för senare åtgärder. Håll det relevanta objektet vid liv så länge dess identifierare används för jämförelser och ta bort inaktuella poster. Den dokumenterade livstidsbegränsningen gör detta till en identitetsmekanism för användning i minnet med begränsad räckvidd. Se ObjectIdentifier.
Använd en värdeögonblicksbild när posten representerar en beräkning
För återanvändbara arearesultat är själva måtten ofta den lämpliga nyckeln. Två rektanglar med måtten 4 gånger 3 kan använda samma resultat även om de är separata objekt med olika etiketter. Ändrade mått ska leda till att en annan post hämtas.
Swift-strukturer har värdesemantik, och kompilatorn kan automatiskt skapa en Hashable-överensstämmelse när deras lagrade egenskaper är hashbara. En ögonblicksbild som innehåller oföränderliga heltal ger därför en enkel nyckel för den här beräkningen. Se Structures and Classes och Hashable.
Kör det här fullständiga alternativet separat med swift SnapshotKeys.swift. De syntetiska indatavärdena är små positiva heltal, så exemplet behöver inte något allmänt gränssnitt för numerisk indata.
final class EditableRectangle {
var label: String
var width: Int
var height: Int
init(label: String, width: Int, height: Int) {
self.label = label
self.width = width
self.height = height
}
}
struct AreaInputs: Hashable, Sendable {
let width: Int
let height: Int
}
func demonstrateSnapshotKeys() {
let rectangle = EditableRectangle(
label: "Tile A", width: 4, height: 3
)
var areas: [AreaInputs: Int] = [:]
let original = AreaInputs(
width: rectangle.width, height: rectangle.height
)
areas[original] = original.width * original.height
rectangle.label = "Tile B"
rectangle.width = 5
let current = AreaInputs(
width: rectangle.width, height: rectangle.height
)
assert(areas[original] == 12)
assert(areas[current] == nil)
areas[current] = current.width * current.height
assert(areas[current] == 15)
assert(areas.count == 2)
print("Original area: \(areas[original]!)")
print("Current area: \(areas[current]!)")
}
demonstrateSnapshotKeys()
Förväntad utdata är först Original area: 12 och sedan Current area: 15. Båda resultaten går fortfarande att hämta eftersom programmet lade in två olika ögonblicksbilder. Det gamla resultatet beskriver fortfarande de gamla måtten korrekt.
Ta med varje indata som påverkar beräkningen i ögonblicksbilden. En etikett påverkar inte rektangelns area och utelämnas därför här. Om en senare beräkning beror på en annan inställning lägger du till den inställningen i nyckeln. Annars kan olika beräkningar felaktigt använda samma post. Det här är ett krav på beräkningens utformning, oberoende av om nyckeln följer Hashable.
En struktur som bara omsluter en referens till en ändringsbar klass skapar inte automatiskt en fryst ögonblicksbild av objektet. Kopiera de relevanta värdena till ögonblicksbilden, som i det här exemplet. Bestäm också när gamla ögonblicksbilder ska tas bort om upprepade redigeringar skulle samla på resultat du inte längre behöver.
När bör du använda ett oföränderligt ID i stället?
Använd ett ID när flera objektinstanser ska hänvisa till samma logiska post. För det här syntetiska verktyget kan ett oföränderligt heltals-ID räcka: lagra resultat i [Int: Int], använd rektangelns let id: Int för åtkomst och håll redigerbara etiketter och mått utanför nyckeln.
Med den designen hänvisar två objekt med ID 7 avsiktligt till samma ordbokspost. Reglerna för hur objekt skapas måste säkerställa att olika logiska rektanglar inte av misstag får samma ID. När du uppdaterar en befintlig nyckel ersätts dess tillhörande värde, vilket beskrivs i Dictionary.
En areatabell som använder ID kräver fortfarande en uppdateringspolicy när måtten ändras. Om du vill ha resultat för varje unik kombination av indata använder du ögonblicksbilder. Om du bara vill ha det senaste resultatet för varje logisk rektangel passar ett ID med uttrycklig omberäkning bättre.
Löser Foundation-ordböcker eller aktörer problemet med ändringsbara nycklar?
Foundation har ett annat kontrakt för insättning. NSMutableDictionary.setObject(_:forKey:) kräver att en nyckel följer NSCopying och kopierar den vid insättning. En nyckelkopiering kan skapa en ögonblicksbild om kopieringsimplementeringen ger ett stabilt nyckeltillstånd. Senare uppslagningar måste matcha den lagrade kopian. Se Apples setObject(_:forKey:).
Dra inte slutsatsen av detta beteende att Swifts inbyggda ordbok gör en oberoende kopia av en klassinstans som används som nyckel. Swift-klasstilldelning har referenssemantik. För anpassade Foundation-nycklar ska du kontrollera kopierings-, likhets- och hashbeteendet tillsammans; Foundations hashkrav gäller fortfarande.
Samtidighet är en separat fråga. Aktörer samordnar åtkomst till sitt isolerade ändringsbara tillstånd, och Sendable beskriver värden som kan passera mellan samtidighetsdomäner enligt protokollets krav. Inget av detta ändrar betydelsen av en nyckels likhet eller reparerar en nyckel vars definierande tillstånd ändras efter insättning. Se Swifts kapitel om samtidighet och Apples dokumentation för Sendable.
Exemplet med ögonblicksbilder deklarerar uttryckligen Sendable eftersom de lagrade heltalen uppfyller protokollets krav på värdetyper. Om du senare flyttar areaberäkningarna till en aktör skickar du en fullständig ögonblicksbild och låter aktören äga sin resultatordbok. Den ändringsbara rektangeln kan stanna inom sin nuvarande isoleringsdomän. De här synkrona exemplen behöver ingen aktör.
Kontrollera kontraktet innan du förlitar dig på en uppslagning
Använd en liten, deterministisk sekvens för att validera den valda designen:
- Lägg in en post och bekräfta den första uppslagningen.
- Ändra en egenskap som inte ingår i likhet och hashning; bekräfta att samma post fortfarande är åtkomlig.
- Jämför ett alias med ett separat skapat objekt eller en separat ögonblicksbild; bekräfta att resultaten stämmer med den avsedda betydelsen av likhet.
- Ändra en beräkningsindata; bekräfta att resultatet antingen uppdateras, tas bort eller hämtas via en ny ögonblicksbild.
- Kontrollera antalet poster i ordboken så att ersättning och insättning fungerar som avsett.
Om en befintlig implementation ändrar det tillstånd som definierar nyckeln ska du rätta nyckelmodellen innan du förlitar dig på fler uppslagningar. För den här lilla lokala beräkningen är det tydligare att återskapa resultaten från en separat lista med syntetiska indata än att behandla senare ordboksoperationer som en reparationsmetod. Spara tillräckligt med ursprunglig indata för att kunna räkna om i stället för att i tysthet tappa en post som du inte kan hitta.
Välj identitet för en koppling till ett levande objekt, ett oföränderligt ID för ett logiskt objekt och en värdeögonblicksbild för en beräkning. Gör sedan policyn för resultatuppdateringar tydlig. Tillsammans gör de valen att uppslagningarna förblir meningsfulla när den redigerbara rektangeln ändras.
Källor
- Hashable — Apple
- Dictionary — Apple
- Structures and Classes — Swift.org
- ObjectIdentifier — Apple
- hash — Apple
- setObject(_:forKey:) — Apple
- Concurrency — Swift.org
- Sendable — Apple