Utrzymuj stabilność właściwości używanych do porównywania i haszowania, gdy obiekt jest kluczem słownika. Przy lokalnych obliczeniach w Swift na Macu wybierz tożsamość obiektu, niezmienny identyfikator albo migawkę wartości, zależnie od tego, co ma oznaczać wyszukiwanie.
W tym przewodniku używamy syntetycznych prostokątów i niewielkich obliczeń pola, przechowywanych w całości w pamięci. Celem jest umożliwienie zmiany etykiety lub wymiarów prostokąta bez utraty powiązanego wpisu w słowniku i bez mylenia udanego wyszukiwania z aktualnym wynikiem obliczeń.
Dlaczego zmiana obiektu może przerwać wyszukiwanie w słowniku?
Dictionary w Swift to tablica mieszająca, której klucze muszą spełniać wymagania Hashable. Dostęp przez indeksowanie kluczem zwraca wartość opcjonalną, ponieważ szukanego klucza może nie być w słowniku. Haszowanie pomaga znaleźć wpis; równość określa, które klucze są uznawane za równoważne. Zobacz dokumentację Apple dotyczącą Dictionary.
Kontrakt Hashable łączy te operacje: równe sobie instancje muszą przekazywać do Hasher te same istotne składowe, w tej samej kolejności. Implementując własny klucz, używaj właściwości określających równość w hash(into:). Apple opisuje ten wymóg w dokumentacji Hashable.
Klucz będący typem referencyjnym wymaga uwzględnienia jeszcze jednej kwestii. Przypisanie instancji klasy przekazuje referencję do istniejącego obiektu, zamiast tworzyć niezależny obiekt. Zadeklarowanie referencji za pomocą let nie zamraża jej zmiennych właściwości. Są to reguły języka opisane w Structures and Classes.
Załóżmy, że klasa prostokąta porównuje i haszuje jego edytowalną etykietę. Wstawiasz obiekt z etykietą \"Tile A\", a następnie zmieniasz ją na \"Tile B\". Przechowywana referencja wskazuje teraz stan klucza o innych wartościach. Pierwotne położenie wpisu w słowniku i późniejsze wyszukiwanie przestają być ze sobą zgodne w stabilny sposób.
Z kontraktów dotyczących tablic mieszających i równości wynika zasada projektowa: stan określający równość klucza musi pozostać niezmienny przez cały czas, gdy klucz należy do słownika. Apple wyraźnie określa analogiczny wymóg dla obiektów Foundation: hasz obiektu nie może się zmienić, gdy należy on do kolekcji, która używa tego hasza do ustalania położenia. Zobacz NSObjectProtocol.hash.
Nie zakładaj w teście, że każda nieprawidłowa modyfikacja natychmiast zwróci nil. Gdy kontrakt klucza zostanie naruszony, pozornie udane wyszukiwanie nie dowodzi, że projekt jest poprawny. Zamiast tego testuj zamierzone reguły równości i stabilne zachowanie.
Który klucz pasuje do zadania?
Zanim zaimplementujesz zgodność z protokołem, zdecyduj, co ma oznaczać wpis w słowniku. W narzędziu do obliczania pola prostokąta te trzy pytania prowadzą do różnych rozwiązań:
| Co ma reprezentować wpis? | Odpowiedni klucz | Co się dzieje po zmianie prostokąta? |
|---|---|---|
| Ten konkretny obiekt prostokąta | Klucz klasowy oparty na tożsamości | Wpis nadal jest powiązany z tą instancją |
| Prostokąt logiczny, którego tożsamość nie zależy od bieżącego obiektu | Wartość z niezmiennym ID | Wpis nadal jest powiązany z ID |
| Obliczenie dla konkretnych wymiarów | Niezmienna migawka wymiarów | Zmienione wymiary tworzą inny klucz wyszukiwania |
Ta tabela pomaga w projektowaniu i wynika z reguł Swift dotyczących tożsamości, wartości i haszowania. Rozdziela też dwa obowiązki: zapewnienie dostępności powiązania oraz aktualności obliczonego wyniku. Stabilny klucz tożsamości rozwiązuje pierwszą kwestię; ponowne obliczenie lub pełna migawka danych wejściowych — drugą.
Użyj tożsamości obiektu, gdy wpis należy do jednej instancji
Aby powiązanie przechowywane w pamięci podążało za jednym obiektem podczas edycji, zdefiniuj równość za pomocą === i haszuj jego ObjectIdentifier. Operator tożsamości w Swift sprawdza, czy referencje wskazują tę samą instancję. Apple opisuje ObjectIdentifier jako haszowalny identyfikator, którego porównania są poprawne przez czas życia instancji. Zobacz Structures and Classes oraz ObjectIdentifier.
Poniższy kompletny program działa synchronicznie. Wymaga Maca z dostępnymi narzędziami wiersza poleceń Swift; korzysta wyłącznie ze standardowej biblioteki Swift, która jest automatycznie importowana. Zapisz go jako IdentityKeys.swift i uruchom w Terminalu polecenie swift IdentityKeys.swift. Przykłady zostały sprawdzone, ale nie udało się ich uruchomić w środowisku badawczym, ponieważ Swift był niedostępny.
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()
Oczekiwany wynik to najpierw Renamed lookup: 12, a następnie Recalculated area: 15. Asercje wyrażają przydatny kontrakt: alias znajduje istniejący wpis, osobno utworzony prostokąt go nie znajduje, a edycja pierwotnego obiektu nie tworzy kolejnego klucza.
Zmiana szerokości celowo pozostawia zapisane pole równe 12, dopóki program nie przypisze 15. Tożsamość pozostaje stabilna, ale słownik nie śledzi zależności arytmetycznych ani nie przelicza wartości. Zdecyduj, czy aplikacja ma aktualizować wartość bezpośrednio po edycji, czy usuwać ją do czasu kolejnego obliczenia.
Równość oparta na tożsamości dotyczy całego typu. Każde porównanie == tej klasy oznacza teraz „ta sama instancja”. Jeśli model już definiuje równość na podstawie wymiarów lub innej wartości, użyj osobnego projektu klucza zamiast zmieniać to znaczenie na potrzeby jednego słownika.
Możesz też bezpośrednio użyć ObjectIdentifier jako klucza słownika, ale sam identyfikator nie daje dostępu do prostokąta potrzebnego w późniejszych operacjach. Utrzymuj odpowiedni obiekt przy życiu tak długo, jak jego identyfikator jest używany do porównań, i usuwaj nieaktualne wpisy. Udokumentowane ograniczenie dotyczące czasu życia sprawia, że jest to mechanizm tożsamości przeznaczony do użycia w ograniczonym zakresie i w pamięci. Zobacz ObjectIdentifier.
Użyj migawki wartości, gdy wpis reprezentuje obliczenie
W przypadku wielokrotnego wykorzystywania wyników obliczeń pola odpowiednim kluczem często są same wymiary. Dwa prostokąty o wymiarach 4 na 3 mogą współdzielić ten sam wynik, nawet jeśli są osobnymi obiektami z różnymi etykietami. Zmiana wymiarów powinna wskazywać inny wpis.
Struktury w Swift mają semantykę wartości, a kompilator może automatycznie wygenerować zgodność z Hashable, gdy ich przechowywane właściwości są haszowalne. Migawka zawierająca niezmienne liczby całkowite stanowi więc prosty klucz dla tego obliczenia. Zobacz Structures and Classes oraz Hashable.
Uruchom ten kompletny wariant osobno poleceniem swift SnapshotKeys.swift. Jego syntetyczne dane wejściowe to niewielkie liczby całkowite większe od zera, więc przykład nie wymaga ogólnego interfejsu do wprowadzania danych liczbowych.
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()
Oczekiwany wynik to najpierw Original area: 12, a następnie Current area: 15. Oba wyniki pozostają dostępne, ponieważ program wstawił dwie różne migawki. Starszy wynik nadal prawidłowo opisuje poprzednie wymiary.
Uwzględnij w migawce każde dane wejściowe wpływające na obliczenie. Etykieta nie ma wpływu na pole prostokąta, więc tutaj ją pomijamy. Jeśli późniejsze obliczenie będzie zależeć od innego ustawienia, dodaj je do klucza. W przeciwnym razie różne obliczenia mogłyby nieprawidłowo korzystać z tego samego wpisu. To wymóg dotyczący projektu obliczeń, niezależny od zgodności klucza z Hashable.
Struktura, która jedynie opakowuje referencję do zmiennego obiektu klasy, nie tworzy automatycznie zamrożonej migawki tego obiektu. Skopiuj do migawki odpowiednie wartości, tak jak w tym przykładzie. Zdecyduj też, kiedy usuwać stare migawki, jeśli kolejne edycje gromadziłyby wyniki, których już nie potrzebujesz.
Kiedy zamiast tego użyć niezmiennego ID?
Użyj ID, gdy kilka instancji obiektu ma wskazywać ten sam logiczny wpis. W tym syntetycznym narzędziu wystarczy niezmienny identyfikator całkowitoliczbowy: przechowuj wyniki w [Int: Int], używaj obiektu let id: Int do wyszukiwania prostokąta, a edytowalne etykiety i wymiary pozostaw poza kluczem.
W tym projekcie dwa obiekty o ID 7 celowo wskazują ten sam wpis w słowniku. Zasady tworzenia obiektów muszą zapewniać, że różne logiczne prostokąty nie otrzymają przypadkowo tego samego ID. Aktualizacja istniejącego klucza zastępuje powiązaną z nim wartość, zgodnie z dokumentacją Dictionary.
Tabela pola oparta na ID nadal wymaga zasad aktualizacji po zmianie wymiarów. Jeśli chcesz zachować wyniki dla każdego różnego zestawu danych wejściowych, użyj migawek. Jeśli chcesz przechowywać tylko najnowszy wynik dla każdego logicznego prostokąta, lepiej sprawdzi się ID i jawne ponowne obliczenie.
Czy słowniki Foundation lub aktory rozwiązują problem zmiennych kluczy?
Foundation ma inne zasady wstawiania. NSMutableDictionary.setObject(_:forKey:) wymaga, aby klucz spełniał NSCopying, i kopiuje go podczas wstawiania. Kopia klucza może utworzyć migawkę, jeśli jej implementacja zapewnia stabilny stan klucza. Późniejsze wyszukiwania muszą odpowiadać zapisanej kopii. Zobacz dokumentację Apple dotyczącą setObject(_:forKey:).
Nie zakładaj na tej podstawie, że natywny słownik Swift tworzy niezależną kopię instancji klasy użytej jako klucz. Przypisanie obiektu klasy w Swift ma semantykę referencyjną. W przypadku własnych kluczy Foundation sprawdź łącznie zachowanie kopiowania, równości i haszowania; nadal obowiązuje też wymaganie Foundation dotyczące haszowania.
Współbieżność to osobna kwestia. Aktory koordynują dostęp do swojego izolowanego zmiennego stanu, a Sendable opisuje wartości, które mogą przekraczać granice domen współbieżności zgodnie z wymaganiami tego protokołu. Żaden z tych mechanizmów nie zmienia znaczenia równości klucza ani nie naprawia klucza, którego stan definiujący zmienia się po wstawieniu. Zobacz rozdział Swift dotyczący współbieżności oraz dokumentację Apple dotyczącą Sendable.
Przykład z migawką jawnie deklaruje Sendable, ponieważ przechowywane w nim liczby całkowite spełniają wymagania protokołu dotyczące typów wartości. Jeśli później przeniesiesz obliczenia pola do aktora, przekaż kompletną migawkę i powierz aktorowi zarządzanie słownikiem wyników. Zmienny prostokąt może pozostać w swojej obecnej domenie izolacji. Te synchroniczne przykłady nie wymagają aktora.
Sprawdź kontrakt przed poleganiem na wyniku wyszukiwania
Użyj krótkiej, deterministycznej sekwencji, aby zweryfikować wybrany projekt:
- Wstaw wpis i sprawdź jego początkowy wynik wyszukiwania.
- Zmień właściwość nieuwzględnioną w równości i haszowaniu; sprawdź, czy ten sam wpis nadal jest dostępny.
- Porównaj alias z osobno utworzonym obiektem lub migawką; sprawdź, czy wyniki odpowiadają zamierzonemu znaczeniu równości.
- Zmień dane wejściowe do obliczenia; sprawdź, czy wynik został odświeżony, usunięty albo jest dostępny pod nową migawką.
- Sprawdź liczbę elementów słownika, aby upewnić się, że zastępowanie i wstawianie działają zgodnie z zamierzeniem.
Jeśli istniejąca implementacja zmienia stan określający klucz, popraw model klucza, zanim zaczniesz polegać na kolejnych wyszukiwaniach. W przypadku tego niewielkiego lokalnego obliczenia czytelniej jest odbudować wyniki z osobnej listy syntetycznych danych wejściowych niż traktować późniejsze operacje na słowniku jako mechanizm naprawczy. Zachowaj wystarczające dane wejściowe, by ponownie wykonać obliczenia, zamiast po cichu tracić wpis, którego nie możesz znaleźć.
Wybierz tożsamość dla powiązania z jednym żyjącym obiektem, niezmienne ID dla jednego elementu logicznego i migawkę wartości dla jednego obliczenia. Następnie jasno określ zasady aktualizowania wyników. Te decyzje razem zapewniają sensowne wyszukiwanie mimo zmian edytowalnego prostokąta.
Źródła
- Hashable — Apple
- Dictionary — Apple
- Structures and Classes — Swift.org
- ObjectIdentifier — Apple
- hash — Apple
- setObject(_:forKey:) — Apple
- Concurrency — Swift.org
- Sendable — Apple