Użyj Dictionary(grouping:by:), aby zebrać syntetyczne wartości Swift według klucza, a następnie użyj mapValues i reduce, aby obliczyć sumę każdej grupy. Ten samouczek dla Maca pokazuje również bezpośrednie sumowanie oraz sprawdzanie pustego wejścia, zerowych sum i przepełnienia liczby całkowitej.

Zadanie polega na przekształceniu tablicy etykiet kształtów i wartości całkowitych w jeden słownik [String: Int]. Wszystkie dane wejściowe są syntetycznymi literałami. Obliczenie nie odczytuje żadnych zewnętrznych rekordów, nie wykonuje żądań sieciowych ani nie zapisuje wyników.

Określ, co oznacza jedna suma dla każdego klucza

Zanim wybierzesz metody pracy z kolekcją, określ kontrakt wyniku. W tym przypadku każdy element wejściowy wnosi swoją wartość dokładnie raz do sumy przypisanej do jego etykiety. Etykiety podlegają zwykłemu porównywaniu ciągów znaków w Swift; program nie usuwa białych znaków ani nie zmienia wielkości liter. Klucze słownika muszą spełniać wymaganie Hashable, a Swift obsługuje ciągi znaków jako klucze. Zobacz dokumentację Apple dotyczącą Dictionary.

Przykładowe dane wejściowe zawierają dwie wartości circle, dwie square i jedną triangle. Oczekiwane sumy można łatwo sprawdzić ręcznie:

Etykieta Wartości wejściowe Oczekiwana suma
circle 4, 6 10
square 3, 5 8
triangle 0 0

Ten przykład stosuje następujące reguły:

  • Puste wejście daje pusty słownik.
  • Etykieta występująca z wartością zero pozostaje w słowniku z sumą zero.
  • Wartości ujemne są dozwolone i uczestniczą w zwykłym dodawaniu.
  • Etykiety nieobecne w danych wejściowych nie występują w wyniku.
  • Każda suma pośrednia musi mieścić się w Int; w przeciwnym razie funkcja zgłasza błąd i nie zwraca żadnych sum.

Różnica między nieobecną etykietą a sumą równą zero ma znaczenie. Brak etykiety oznacza, że nie było pasujących danych wejściowych. Obecna etykieta z wartością zero może oznaczać jawne zero albo wartości, które się znoszą. Zachowaj to rozróżnienie, chyba że odbiorca wyniku potrzebuje innej konwencji.

Uruchom kompletny przykład na Macu

Użyj zainstalowanego łańcucha narzędzi Swift. [Instrukcja instalacji dla macOS](https://www.swift.org/install/macos/ na Swift.org opisuje konfigurację łańcucha narzędzi oraz polecenia tworzenia i uruchamiania pakietu wykonywalnego. Gdy łańcuch narzędzi będzie gotowy, otwórz Terminal i utwórz mały projekt lokalny:

mkdir SyntheticTotals
cd SyntheticTotals
swift package init --name SyntheticTotals --type executable

Otwórz wygenerowany plik źródłowy programu wykonywalnego w katalogu Sources i zastąp jego zawartość kompletnym programem poniżej. Wygenerowany plik może znajdować się w podkatalogu nazwanym tak jak cel. Pozostaw manifest pakietu w wygenerowanej postaci; ten program nie wymaga dodatkowych zależności ani importów, ponieważ jego typy i metody obliczeń należą do biblioteki standardowej Swift.

struct SyntheticValue: Sendable {
    let key: String
    let quantity: Int
}

enum TotalError: Error {
    case overflow(key: String)
}

func checkedAdd(_ total: Int, _ value: SyntheticValue) throws -> Int {
    let addition = total.addingReportingOverflow(value.quantity)
    guard !addition.overflow else {
        throw TotalError.overflow(key: value.key)
    }
    return addition.partialValue
}

func groupThenTotal(_ values: [SyntheticValue]) throws -> [String: Int] {
    let groups = Dictionary(grouping: values, by: { $0.key })
    return try groups.mapValues { group in
        try group.reduce(0) { total, value in
            try checkedAdd(total, value)
        }
    }
}

func totalDirectly(_ values: [SyntheticValue]) throws -> [String: Int] {
    return try values.reduce(into: [String: Int]()) { totals, value in
        let previous = totals[value.key, default: 0]
        totals[value.key] = try checkedAdd(previous, value)
    }
}

let sample: [SyntheticValue] = [
    SyntheticValue(key: "circle", quantity: 4),
    SyntheticValue(key: "square", quantity: 3),
    SyntheticValue(key: "circle", quantity: 6),
    SyntheticValue(key: "triangle", quantity: 0),
    SyntheticValue(key: "square", quantity: 5)
]

let expected = ["circle": 10, "square": 8, "triangle": 0]

 do {
    let groupedTotals = try groupThenTotal(sample)
    let directTotals = try totalDirectly(sample)
    precondition(groupedTotals == expected)
    precondition(directTotals == expected)

    let emptyGrouped = try groupThenTotal([])
    let emptyDirect = try totalDirectly([])
    precondition(emptyGrouped.isEmpty && emptyDirect.isEmpty)

    let cancellation = [
        SyntheticValue(key: "circle", quantity: 2),
        SyntheticValue(key: "circle", quantity: -2)
    ]
    let zeroGrouped = try groupThenTotal(cancellation)
    let zeroDirect = try totalDirectly(cancellation)
    precondition(zeroGrouped == ["circle": 0])
    precondition(zeroDirect == ["circle": 0])

    for key in groupedTotals.keys.sorted() {
        if let total = groupedTotals[key] {
            print("\(key): \(total)")
        }
    }

    let tooLarge = [
        SyntheticValue(key: "circle", quantity: Int.max),
        SyntheticValue(key: "circle", quantity: 1)
    ]
    do {
        _ = try totalDirectly(tooLarge)
        preconditionFailure("Expected overflow to be reported")
    } catch TotalError.overflow(let key) {
        print("Overflow check: rejected \(key)")
    }
} catch {
    fatalError("Unexpected calculation failure: \(error)")
}

W katalogu pakietu uruchom program poleceniem opisanym w [instrukcji dla macOS](https://www.swift.org/install/macos/ na Swift.org:

swift run SyntheticTotals

Oczekiwany wynik programu, poza komunikatami kompilacji, to:

circle: 10
square: 8
triangle: 0
Overflow check: rejected circle

Program zawiera sprawdzenia obliczonych sum, pustego wejścia, znoszenia się wartości do zera oraz celowo wywołanego przepełnienia. Są to oczekiwane wyniki wynikające z przykładu i udokumentowanych interfejsów API. Kod został sprawdzony, ale nie został uruchomiony podczas przygotowywania, ponieważ dostępne środowisko nie miało kompilatora Swift.

Jak działa grupowanie, po którym następuje redukcja?

Pierwsza funkcja celowo dzieli zadanie na widoczne etapy. Dictionary(grouping: values, by: { $0.key }) tworzy słownik, którego wartościami są tablice pasujących elementów. Apple dokumentuje, że każda powstała grupa jest niepusta i zachowuje kolejność elementów z sekwencji źródłowej. Zobacz init(grouping:by:).

Dla przykładu typ pośredni to [String: [SyntheticValue]]. Grupa circle zawiera wartości 4 i 6; grupa square zawiera 3 i 5; grupa triangle zawiera 0. Ta reprezentacja pośrednia jest przydatna, gdy inna część lokalnego obliczenia również potrzebuje przejrzeć elementy każdej grupy.

Następnie mapValues zachowuje klucze słownika i przekształca tablicę każdej grupy w jedną liczbę całkowitą. Zachowanie kluczy jest udokumentowanym działaniem mapValues(_:). Wyjaśnia też, dlaczego wpis triangle pozostaje w słowniku, mimo że jego suma wynosi zero.

W ramach tej transformacji reduce(0) zaczyna od zera i przekazuje każdy kolejny element do domykającego łączenie. Domknięcie zwraca następną sumę bieżącą. Apple dokumentuje sekwencyjne zmniejszanie i określa, że redukcja pustej sekwencji zwraca wartość początkową. Zobacz reduce(::).

Występują tu dwa różne przypadki pustych danych. Redukcja pustej tablicy wartości od zera dałaby zero. Jednak grupowanie pustego wejścia nie tworzy żadnych grup, więc kompletna funkcja zwraca pusty słownik zamiast tworzyć etykietę z sumą zero.

Kiedy sumować wartości bezpośrednio?

Użyj totalDirectly, gdy potrzebne są wyłącznie końcowe sumy. Akumulator zaczyna od pustego słownika [String: Int]. Dla każdego elementu wejściowego funkcja pobiera poprzednią sumę przypisaną do jego etykiety, dodaje wartość i zapisuje zaktualizowaną sumę.

Odczyt z wartością domyślną zwraca zero, gdy brakuje klucza. Sam odczyt wartości domyślnej nie tworzy wpisu; przypisanie w następnym wierszu zapisuje obliczoną sumę. Dokumentacja Apple dotycząca indeksowania z wartością domyślną wyjaśnia zarówno odczyt z wartością zastępczą, jak i aktualizacje w miejscu dla typów o semantyce wartości.

reduce(into:) zapewnia domknięciu modyfikowalny dostęp do akumulatora. Apple zaleca tę formę zamiast zwykłego reduce przy budowaniu wyniku z semantyką copy-on-write, takiego jak słownik. Zobacz reduce(into:_:).

Czego wymaga następny krok Wybierz Powód
Elementy grup i ich sumy Grupowanie, potem redukcja Pośrednie tablice można przeglądać lub wykorzystać ponownie.
Tylko jedna liczba całkowita na etykietę Bezpośrednie sumowanie Pozwala uniknąć tworzenia pośrednich tablic grup.
Powtarzalny wynik tekstowy Dowolna metoda, potem sortowanie kluczy Kolejność wyświetlania to osobna decyzja od agregacji.

Różnica w przechowywaniu danych pośrednich wynika z kodu: grupowanie zachowuje wszystkie elementy wejściowe w tablicach grup, natomiast bezpośrednie sumowanie przechowuje jedną bieżącą sumę dla każdego odrębnego klucza. To porównanie implementacji, a nie zmierzona ocena wydajności. Przykład nie zawiera pomiarów czasu.

Jak obsługiwać przepełnienie i stabilny wynik?

checkedAdd wywołuje addingReportingOverflow, które zwraca wynik częściowy i flagę przepełnienia. Apple określa, że wartość częściowa zawiera pełną sumę tylko wtedy, gdy flaga jest wyłączona. Gdy wystąpi przepełnienie, zwrócona wartość częściowa jest obcięta. Dlatego program zgłasza błąd, zanim ją zapisze. Zobacz addingReportingOverflow(_:).

Ten kontrakt sprawdza każde dodawanie w kolejności danych wejściowych, osobno dla każdego klucza. Na przykład wartości Int.max, 1 i -1 dla jednej etykiety zostaną odrzucone przy drugim dodawaniu, mimo że ich matematyczna suma końcowa mieści się w Int. Obsługa takich danych wymaga innej strategii arytmetycznej; zaakceptowanie pośredniego obciętego wyniku zmieniłoby kontrakt.

Jeśli przepełnienie wystąpi w kilku grupach, implementacja z grupowaniem nie gwarantuje, błąd dla której etykiety pojawi się jako pierwszy: kolejność iteracji słownika jest nieprzewidywalna. Implementacja bezpośrednia przetwarza elementy w kolejności źródłowej. Obie odrzucają obliczenie powodujące przepełnienie, ale mogą jako pierwszą zgłosić inną etykietę.

Sortowanie kluczy wyniku zapewnia przewidywalną kolejność wyświetlanych sum. Słowniki Swift nie mają ustalonej kolejności, dlatego bezpośredniego wypisywania słownika nie należy używać do ustalenia kolejności prezentacji. Apple opisuje tę właściwość w dokumentacji Dictionary. Przykład sortuje klucze dopiero po agregacji, nie zmieniając obliczeń.

Czy Foundation lub współbieżność Swift zmieniają to obliczenie?

Foundation nie jest potrzebne przy tym kontrakcie wartości. Apple dokumentuje NSDictionary jako alternatywę, gdy wymagana jest semantyka referencyjna. W tym przypadku typowany słownik Swift już reprezentuje wymagane odwzorowanie etykiet na sumy całkowite. Zobacz dokumentację Foundation dotyczącą NSDictionary.

Obliczenie jest synchroniczne, a jego modyfikowalny akumulator pozostaje wewnątrz funkcji. Typ SyntheticValue w przykładzie jawnie spełnia wymaganie Sendable; wyraża to kontrakt wartości związany ze współbieżnością, a nie polecenie równoległego wykonania redukcji. [Przewodnik po współbieżności](https://docs.swift.org/latest/documentation/the-swift-programming-language/concurrency/ w dokumentacji Swift wyjaśnia wartości Sendable oraz wymagania dotyczące struktur, których przechowywane właściwości są sendable.

W przypadku tego niewielkiego obliczenia na komputerze możesz bezpośrednio wywołać jedną z funkcji i sprawdzić zwrócony słownik. Wybierz grupowanie, jeśli elementy grup pozostają przydatne, albo bezpośrednie sumowanie, gdy wystarczą same sumy. Zachowaj reguły dotyczące pustego wejścia i sumy zero, jawnie odrzucaj przepełnienie arytmetyczne, a sortowania używaj wyłącznie przy prezentowaniu wyniku.

Źródła