Используйте Dictionary(grouping:by:), чтобы сгруппировать синтетические значения Swift по ключу, а затем mapValues и reduce, чтобы вычислить сумму для каждой группы. Это руководство для Mac также показывает прямое накопление суммы и проверки на пустой ввод, нулевые суммы и переполнение целых чисел.
Задача — преобразовать массив меток фигур нейтрального вида и целочисленных количеств, хранящийся в памяти, в один словарь [String: Int]. Все входные данные — синтетические литералы. Вычисление не считывает внешние записи, не отправляет сетевые запросы и не сохраняет результаты.
Определите, что означает одна сумма для каждого ключа
Прежде чем выбирать методы коллекций, определите контракт результата. Здесь каждый элемент входных данных ровно один раз добавляет своё количество к сумме для своей метки. Для меток используется обычное сравнение строк Swift на равенство; программа не удаляет пробелы и не меняет регистр. Ключи словаря должны соответствовать протоколу Hashable, а Swift поддерживает строки в качестве ключей. См. документацию Apple по Dictionary.
В иллюстративных входных данных два значения для круга, два для квадрата и одно для треугольника. Предполагаемые суммы достаточно просты, чтобы проверить их вручную:
| Метка | Входные количества | Ожидаемая сумма |
|---|---|---|
circle |
4, 6 |
10 |
square |
3, 5 |
8 |
triangle |
0 |
0 |
В этом примере действуют следующие правила:
- Пустой ввод даёт пустой словарь.
- Метка, встречающаяся с нулевым количеством, остаётся в словаре с нулевой суммой.
- Допускаются отрицательные количества; они учитываются обычным сложением.
- Метки, отсутствующей во входных данных, нет и в результате.
- Каждая промежуточная сумма должна помещаться в
Int; в противном случае функция выдаёт ошибку и не возвращает никаких сумм.
Различие между отсутствующей меткой и нулевой суммой важно. Отсутствующая метка означает, что совпадающих входных данных не было. Присутствующая метка с нулём может означать явно заданный ноль или взаимно компенсирующиеся количества. Сохраняйте это различие, если читателю результата не нужна другая конвенция.
Запустите полный пример на Mac
Используйте установленную цепочку инструментов Swift. В руководстве Swift.org по установке для macOS описаны настройка цепочки инструментов и команды для создания и запуска исполняемого пакета. Когда цепочка инструментов будет готова, откройте Terminal и создайте небольшой локальный проект:
mkdir SyntheticTotals
cd SyntheticTotals
swift package init --name SyntheticTotals --type executable
Откройте созданный файл исходного кода исполняемого файла в каталоге Sources и замените его содержимое полной программой ниже. Сгенерированный файл может находиться во вложенном каталоге с именем целевого компонента. Оставьте манифест пакета без изменений: программе не нужны дополнительные зависимости или импорты, поскольку используемые типы и методы вычислений входят в стандартную библиотеку 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)")
}
В каталоге пакета запустите исполняемый файл командой, описанной в руководстве Swift.org для macOS:
swift run SyntheticTotals
Ожидаемый вывод программы, не считая сообщений сборки:
circle: 10
square: 8
triangle: 0
Overflow check: rejected circle
Программа проверяет рассчитанные суммы, пустой ввод, взаимное погашение до нуля и намеренно созданное переполнение. Это ожидаемые результаты, выведенные из примера и документированных API. Код был проверен, но во время подготовки не запускался, поскольку в доступной среде не было компилятора Swift.
Как работает группировка с последующим сведением?
Первая функция намеренно разделяет задачу на наглядные этапы. Dictionary(grouping: values, by: { $0.key }) создаёт словарь, значения которого — массивы соответствующих элементов. Apple указывает, что каждая созданная группа непуста и сохраняет порядок элементов из исходной последовательности. См. init(grouping:by:).
Для примера промежуточный тип — [String: [SyntheticValue]]. Группа круга содержит количества 4 и 6; группа квадрата содержит 3 и 5; группа треугольника содержит 0. Такое промежуточное представление полезно, если другой части локального вычисления также нужно изучить элементы каждой группы.
Затем mapValues сохраняет ключи словаря и преобразует массив каждой группы в одно целое число. Это сохранение ключей описано в документации к mapValues(_:). Благодаря этому запись треугольника остаётся в словаре, даже если её сумма равна нулю.
Внутри этого преобразования reduce(0) начинает с нуля и передаёт каждый следующий элемент объединяющему замыканию. Замыкание возвращает очередную промежуточную сумму. Apple описывает последовательное сведение и уточняет, что при сведении пустой последовательности возвращается начальное значение. См. reduce(::).
Здесь есть два разных случая пустоты. Если свести пустой массив количеств, начав с нуля, результатом будет ноль. Однако группировка пустого ввода не создаёт групп, поэтому вся функция возвращает пустой словарь, а не придумывает метку с нулевой суммой.
Когда следует накапливать суммы напрямую?
Используйте totalDirectly, если требуется только итоговая сумма для каждой метки. Его аккумулятор начинается с пустого словаря [String: Int]. Для каждого входного элемента он получает предыдущую сумму этой метки, прибавляет количество и сохраняет обновлённую сумму.
При чтении по умолчанию возвращается ноль, если ключ отсутствует. Одно лишь чтение значения по умолчанию не создаёт запись; вычисленная сумма сохраняется присваиванием в следующей строке. В документации Apple по индексатору со значением по умолчанию описаны и поиск с резервным значением, и обновление на месте для значений с семантикой значения.
reduce(into:) предоставляет замыканию изменяемый доступ к аккумулятору. Apple рекомендует эту форму вместо обычного reduce при построении результата с поведением копирования при записи, например словаря. См. reduce(into:_:).
| Что требуется на следующем шаге | Выберите | Причина |
|---|---|---|
| Элементы групп и их суммы | Сначала группировка, затем сведение | Промежуточные массивы доступны для просмотра или повторного использования. |
| Только одно целое число для каждой метки | Прямое накопление | Оно позволяет не создавать промежуточные массивы групп. |
| Повторяемый текстовый вывод | Любой способ, затем сортировка ключей | Порядок вывода — отдельное решение, не связанное с агрегацией. |
Различие в хранении промежуточных данных следует из кода: группировка сохраняет все входные элементы в массивах групп, а прямое накопление хранит по одной текущей сумме для каждого уникального ключа. Это сравнение реализации, а не утверждение об измеренной производительности. В примере нет замеров времени.
Как обрабатывать переполнение и обеспечивать стабильный вывод?
checkedAdd вызывает addingReportingOverflow, который возвращает промежуточный результат и флаг переполнения. Apple указывает, что промежуточное значение содержит полную сумму, только если флаг равен false. При переполнении возвращаемое промежуточное значение обрезается. Поэтому программа выдаёт ошибку, прежде чем сохранить его. См. addingReportingOverflow(_:).
Этот контракт проверяет каждое сложение в порядке поступления данных внутри каждого ключа. Например, количества Int.max, 1 и -1 для одной метки отклоняются при втором сложении, хотя их математическая итоговая сумма помещается в Int. Для поддержки таких входных данных нужна другая стратегия вычислений; принятие промежуточного обрезанного результата изменило бы контракт.
Если переполнение возникает в нескольких группах, реализация с группировкой не гарантирует, ошибка для какой метки появится первой: порядок обхода словаря непредсказуем. Прямая реализация обрабатывает элементы в исходном порядке. Обе реализации отклоняют вычисление с переполнением, но первая указанная в ошибке метка может различаться.
Сортировка ключей результата делает отображаемые суммы предсказуемыми. Словари Swift не упорядочены, поэтому печать словаря напрямую не должна задавать порядок представления. Apple описывает это ограничение порядка в документации по Dictionary. В примере ключи сортируются только после агрегации, так что само вычисление не меняется.
Меняют ли Foundation или параллелизм Swift это вычисление?
Foundation не требуется для такого контракта значений. Apple описывает NSDictionary как альтернативу, когда необходима семантика ссылочного типа. Здесь типизированный словарь Swift уже представляет требуемое соответствие между метками и целочисленными суммами. См. документацию Foundation по NSDictionary.
Вычисление выполняется синхронно, а изменяемый аккумулятор остаётся внутри функции. В примере тип SyntheticValue явно соответствует протоколу Sendable; это выражает контракт значения, связанный с параллелизмом, а не требование выполнять сведение параллельно. В руководстве по параллелизму Swift описаны передаваемые значения и требования к структурам, хранимые свойства которых являются передаваемыми.
Для такого небольшого настольного вычисления вызовите любую из функций напрямую и изучите возвращённый словарь. Выбирайте группировку, если элементы групп ещё пригодятся, или прямое накопление, если достаточно сумм. Сохраняйте правила для пустого ввода и нулевой суммы, явно отклоняйте арифметическое переполнение и сортируйте только при выводе результата.
Источники
- Установка Swift — macOS — Swift.org.
- Dictionary — документация Apple для разработчиков.
- init(grouping:by:) — документация Apple для разработчиков.
- mapValues(_:) — документация Apple для разработчиков.
- reduce(::) — документация Apple для разработчиков.
- reduce(into:_:) — документация Apple для разработчиков.
- subscript(_:default:) — документация Apple для разработчиков.
- addingReportingOverflow(_:) — документация Apple для разработчиков.
- NSDictionary — документация Apple для разработчиков.
- Concurrency — язык программирования Swift.