Aby zachować unikatowość syntetycznych wartości Swift bez zmiany ich kolejności wstawiania, przejrzyj tablicę wejściową jeden raz, zapisuj zaakceptowane klucze w zbiorze i dodawaj do tablicy wynikowej tylko pierwsze wystąpienia. W tym przewodniku utworzymy niewielkie lokalne obliczenie, które można uruchomić na Macu.
Zadanie polega na przetworzeniu uporządkowanej listy syntetycznych wartości opisujących kształty, usunięciu powtórzeń i obliczeniu sumy na podstawie zachowanych elementów. Wszystko odbywa się w pamięci; przykład nie odczytuje danych z zewnątrz ani nie zapisuje wyników obliczeń.
Zanim wybierzesz kolekcję, określ, co oznacza „unikatowość”
Tablice Swift zachowują kolejność i dopuszczają duplikaty. Zbiory zawierają unikatowe wartości, ale nie mają określonej kolejności. W związku z tym konwersja tablicy na zbiór, a następnie z powrotem na tablicę nie zachowuje kolejności pierwszych wystąpień. Późniejsze sortowanie ustala kolejność sortowania, która może różnić się od kolejności wstawiania. Te właściwości kolekcji opisano w dokumentacji Swift Typy kolekcji.
W tym przykładzie każda syntetyczna wartość ma nazwę kształtu i rozmiar. Domyślna reguła uznaje dwa elementy za duplikaty tylko wtedy, gdy obie właściwości są zgodne. Okrąg o rozmiarze 12 i okrąg o rozmiarze 20 pozostają więc odrębnymi wartościami.
| Pytanie dotyczące reguły | Zasada przyjęta w tym obliczeniu |
|---|---|
| Co określa kolejność danych wejściowych? | Pozycja w podanej tablicy |
| Kiedy dwie wartości są duplikatami? | Gdy mają taki sam kształt i rozmiar |
| Które wystąpienie zostaje zachowane? | Pierwsze wystąpienie |
| Czy późniejszy duplikat przesuwa zachowaną wartość? | Nie |
| Co się dzieje przy pustych danych wejściowych? | Zwracana jest pusta tablica i suma równa zero |
| Które wartości są uwzględniane w sumie? | Tylko zachowane wartości |
Ta reguła zapobiega łatwemu do popełnienia błędowi: usuwaniu elementów według nazwy kształtu, gdy obliczenie ma rozróżniać kombinacje kształtu i rozmiaru. W przykładzie obliczana jest też suma dla samych kształtów, aby pokazać, jaki skutek ma zmiana tej zasady.
Utwórz kompletny lokalny przykład w Swift na Macu
Potrzebujesz zainstalowanego zestawu narzędzi Swift. W aplikacji Terminal uruchom swift --version, aby sprawdzić, czy polecenie jest dostępne. W oficjalnym samouczku Swift Tworzenie narzędzia wiersza poleceń opisano to sprawdzenie, tworzenie pakietu wykonywalnego i uruchamianie pakietu.
Utwórz nowy katalog na potrzeby przykładu i zainicjuj pakiet wykonywalny:
mkdir OrderDemo
cd OrderDemo
swift package init --name OrderDemo --type executable
Zastąp wygenerowany plik źródłowy programu wykonywalnego w katalogu Sources kompletnym programem poniżej. Nazwa wygenerowanego pliku i układ katalogów mogą się różnić w zależności od zestawu narzędzi; zastąp istniejący punkt wejścia zamiast dodawać drugi. Ten program używa punktu wejścia @main, dlatego umieść go w pliku o nazwie OrderDemo.swift, zmieniając w razie potrzeby nazwę wygenerowanego pliku main.swift. W samouczku wiersza poleceń Swift wyjaśniono wybór między plikiem main.swift a punktem wejścia @main.
Nie są wymagane żadne importy ani zależności pakietów. Program korzysta ze standardowych typów i funkcji biblioteki.
struct Tile: Hashable {
let shape: String
let size: Int
}
func uniqueFirst<Value, Key: Hashable>(
_ values: [Value],
by key: (Value) -> Key
) -> [Value] {
var seen: Set<Key> = []
var result: [Value] = []
for value in values {
if seen.insert(key(value)).inserted {
result.append(value)
}
}
return result
}
@main
struct OrderDemo {
static func main() {
let input = [
Tile(shape: "circle", size: 12),
Tile(shape: "square", size: 8),
Tile(shape: "circle", size: 12),
Tile(shape: "circle", size: 20),
Tile(shape: "square", size: 8)
]
let retained = uniqueFirst(input, by: { $0 })
let total = retained.reduce(0) { $0 + $1.size }
let skipped = input.count - retained.count
let expected = [
Tile(shape: "circle", size: 12),
Tile(shape: "square", size: 8),
Tile(shape: "circle", size: 20)
]
precondition(retained == expected)
precondition(total == 40)
precondition(skipped == 2)
let empty: [Tile] = []
precondition(uniqueFirst(empty, by: { $0 }).isEmpty)
precondition(
uniqueFirst([input[0], input[0]], by: { $0 })
== [input[0]]
)
let byShape = uniqueFirst(input, by: { $0.shape })
precondition(byShape == [input[0], input[1]])
let shapeTotal = byShape.reduce(0) { $0 + $1.size }
precondition(shapeTotal == 20)
for tile in retained {
print("\(tile.shape): \(tile.size)")
}
print("Total: \(total)")
print("Skipped duplicates: \(skipped)")
print("Shape-only total: \(shapeTotal)")
}
}
W katalogu pakietu uruchom:
swift run OrderDemo
Oczekiwany wynik działania programu, po ewentualnych komunikatach kompilacji, pokazano poniżej. Program sprawdzono na podstawie pobranej dokumentacji API, ale nie uruchomiono go tutaj, ponieważ w tym środowisku badawczym nie ma pliku wykonywalnego Swift.
circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20
Dlaczego podejście z tablicą i zbiorem zachowuje kolejność?
Jedynym źródłem kolejności wynikowej jest tablica. Pętla odwiedza pozycje wejściowe po kolei, a append(_:) umieszcza zaakceptowaną wartość na końcu wyniku. Zbiór służy wyłącznie do sprawdzenia, czy wybrany klucz już wystąpił; program nigdy go nie iteruje.
Kluczowe jest wyrażenie seen.insert(key(value)).inserted. W dokumentacji Swift podano, że insert(_:) zwraca inserted jako true dla nowego elementu, a false, gdy istnieje już równy element. Istniejący element pozostaje bez zmian. Wartość zwracana pozwala w jednej operacji zdecydować, czy dodać oryginalną wartość. Zobacz dokumentację Apple dotyczącą wstawiania do zbioru.
W podanych danych wejściowych pierwszy okrąg i kwadrat zostają zaakceptowane. Drugi identyczny okrąg zostaje pominięty. Większy okrąg zostaje zaakceptowany, bo jego pełna wartość jest inna, a powtórzony kwadrat zostaje pominięty. Zachowane rozmiary to zatem 12 + 8 + 20 = 40.
Implementacja utrzymuje przydatny niezmiennik: po każdej iteracji wynik zawiera dokładnie najwcześniej napotkaną wartość dla każdego klucza z seen, w kolejności napotkania. Nowy klucz rozszerza obie kolekcje; powtórzony klucz nie zmienia żadnej z nich. To wyjaśnia gwarancję zachowania kolejności niezależnie od sposobu przechowywania elementów przez zbiór.
Jak niestandardowe wartości Swift powinny określać równość?
Deklaracja struct Tile: Hashable żąda syntezowanego dostosowania. Obie zapisane właściwości, String i Int, są haszowalne, więc Swift może zsyntetyzować wymagane zachowanie. Apple opisuje warunki syntezy w dokumentacji Hashable.
Użycie { $0 } jako klucza sprawia, że o unikatowości decyduje cała wartość Tile. Użycie { $0.shape } zmienia regułę tak, że zachowywany jest jeden element dla każdego kształtu. W przykładzie oznacza to usunięcie późniejszego okręgu o rozmiarze 20 i zmniejszenie sumy do 20. To celowe porównanie dwóch zasad, a nie druga implementacja tej samej zasady.
Wybór klucza powinien być deterministyczny przez cały czas trwania obliczenia. Wyprowadzaj go z przetwarzanej wartości, a nie z licznika ani zmiennej zewnętrznej, która może się zmieniać. W przeciwnym razie identyczne dane wejściowe mogą otrzymać różne klucze i przetrwać jako odrębne elementy.
Jeśli implementujesz równość i haszowanie ręcznie, używaj w obu tych mechanizmach tych samych właściwości istotnych dla równości. Apple wymaga, aby równe instancje przekazywały haszerowi te same wartości w tej samej kolejności. Unikaj używania samego hashValue jako klucza unikatowości: hasz nie zastępuje reguł równości wartości. Przechowywanie rzeczywistego klucza haszowalnego pozwala zbiorowi stosować te reguły. Zobacz wymagania dotyczące zgodności z Hashable.
Kiedy zamiast tego użyć kolekcji zachowującej kolejność?
W przypadku tego pojedynczego obliczenia tablica wraz z lokalnym zbiorem pozwala jasno wyrazić zachowanie. Jeśli aplikacja wielokrotnie dodaje, usuwa, porządkuje i wyszukuje unikatowe elementy, dedykowana kolekcja zachowująca kolejność może ułatwić utrzymanie tych operacji.
| Wybór | Odpowiednia sytuacja | Istotne ograniczenie |
|---|---|---|
| Tablica i zbiór | Jedno przejście po syntetycznych wartościach Swift, opcjonalnie z wybranym kluczem | Funkcja określa zachowanie dla pierwszego wystąpienia |
OrderedSet z Swift Collections |
Długotrwałe użycie uporządkowanej kolekcji unikatowych wartości haszowalnych | Wymaga osobnego pakietu Swift Collections |
NSOrderedSet z Foundation |
Gdy API już korzysta z uporządkowanych kolekcji obiektów Foundation | Klasa reprezentująca statyczną uporządkowaną kolekcję; do modyfikowania służy NSMutableOrderedSet |
W oficjalnej dokumentacji Swift Collections opisano OrderedSet jako uporządkowaną kolekcję unikatowych elementów, importowaną przez OrderedCollections. Udostępnia ona operacje uwzględniające pozycję, takie jak append(_:) i insert(_:at:). Są to API pakietu, więc samo dodanie import OrderedCollections nie konfiguruje projektu, który w pozostałym zakresie nie ma zależności. Zobacz OrderedSet.
W dokumentacji Foundation firmy Apple opisano NSOrderedSet jako statyczną uporządkowaną kolekcję różnych obiektów i wskazano NSMutableOrderedSet jako jej modyfikowalny odpowiednik. W przedstawionym wyżej obliczeniu wartości natywnych Swift funkcja generyczna bezpośrednio wyraża wybrany klucz równości, nie wprowadzając tej kolekcji obiektów. Zobacz NSOrderedSet.
Co się zmienia, gdy obliczenie zasila interfejs desktopowy?
Kompletny przykład działa synchronicznie, a jego tymczasowe kolekcje są tworzone wewnątrz funkcji. W przypadku niewielkiej syntetycznej listy daje to jeden jasny porządek przetwarzania bez wprowadzania zadań.
W interfejsie macOS odpowiednio izoluj stan i aktualizacje powiązane z interfejsem. Apple opisuje MainActor jako globalny aktor, którego executor jest równoważny głównej kolejce dyspozytorskiej; w przewodniku Swift dotyczącym współbieżności wyjaśniono stosowanie @MainActor do funkcji i typów uzyskujących dostęp do stanu interfejsu. Zobacz MainActor i Współbieżność.
Współbieżność wymaga też jawnego określenia kolejności. Swift nie gwarantuje kolejności kończenia zadań podrzędnych w grupie zadań. Dopisywanie wyników w miarę kończenia zadań zachowuje więc kolejność ukończenia, która może różnić się od pierwotnej kolejności danych wejściowych. To ograniczenie opisano w oficjalnym przewodniku dotyczącym współbieżności.
Jeśli przyszła wersja będzie wykonywać niezależne syntetyczne obliczenia współbieżnie, dołącz pozycję wejściową do każdego wyniku, po zebraniu przywróć kolejność według pozycji wejściowej, a następnie wykonaj przebieg usuwający duplikaty. To rozwiązanie wynika z opisanego ograniczenia dotyczącego kolejności ukończenia. Zachowaj synchroniczne działanie obecnego niewielkiego przykładu, chyba że dodatkowe obliczenia uzasadnią potrzebę koordynacji.
Przed ponownym użyciem funkcji sprawdź przyjętą regułę
Program zawiera kontrole dokładnej zachowanej kolejności, pustych danych wejściowych, powtórzonych identycznych wartości oraz alternatywnej reguły uwzględniającej wyłącznie kształt. Kontrole te dotyczą decyzji, które najłatwiej mogą zmienić znaczenie obliczenia.
Podczas dostosowywania funkcji sprawdź trzy kwestie: dane wejściowe muszą mieć już określony sensowny porządek, klucz musi wyrażać zamierzoną regułę równości, a zachowanie pierwszego wystąpienia musi być właściwe. Funkcja celowo pomija późniejsze pasujące elementy; nie łączy ich rozmiarów ani nie zastępuje zachowanej wartości. Podana liczba pominiętych elementów pokazuje ten efekt.
Dla podanej syntetycznej listy kształtów gotowy wynik zawiera trzy wartości w kolejności pierwszych wystąpień, a ich suma wynosi 40. Tablica odpowiada za kolejność, zbiór za przynależność, a wybrany klucz za znaczenie słowa „unikatowy”.
Źródła
- Typy kolekcji — The Swift Programming Language, projekt Swift.
- Tworzenie narzędzia wiersza poleceń — Swift.org.
- insert(_:) — dokumentacja Apple dla programistów.
- Hashable — dokumentacja Apple dla programistów.
- OrderedSet — Apple, Swift Collections.
- NSOrderedSet — dokumentacja Apple dla programistów.
- MainActor — dokumentacja Apple dla programistów.
- Współbieżność — The Swift Programming Language, projekt Swift.