Чтобы объединить два значения структур Swift, напишите функцию, которая выбирает источник для каждого поля и возвращает новое значение. В этом примере для macOS объединяются два условных стиля панели рисования с явными правилами замены, сохранения, очистки необязательного значения и обработки недопустимых данных.

Определите приоритеты до написания функции объединения

Задача читателя — вычислить стиль панели рисования на Mac на основе двух существующих значений. Назовём входные данные base и incoming: эти имена описывают их роли, а приоритет каждого значения определяется соглашением для отдельных полей. Вычисление использует только условные значения в памяти.

Swift описывает структуры как типы-значения и позволяет создавать их экземпляры с помощью инициализаторов. Документированный Apple API merge(_:uniquingKeysWith:) относится к Dictionary и разрешает дублирующиеся ключи с помощью замыкания. Для этой пользовательской структуры определите функцию объединения, реализация которой задаёт нужные правила для полей. Структуры и классы, Dictionary merge(_:uniquingKeysWith:).

Используйте приведённое ниже иллюстративное соглашение. Это проектные решения для данного примера, а не поведение, автоматически обеспечиваемое Swift.

Поле Правило Пример результата
title Взять входной текст, удалить пробелы и символы новой строки по краям, затем отклонить пустой результат " Tile Study \n" становится \"Tile Study\"
spacing Сохранить базовое значение; оно должно быть неотрицательным Базовое 12 имеет приоритет над входным 30
showsGrid Взять входное логическое значение без изменений Входное false заменяет базовое true
caption Взять входное необязательное значение без изменений Входное nil очищает базовую подпись

Эта таблица различает три операции, которые легко спутать: выбор источника, нормализацию выбранного значения и проверку результата. Пробелы во входном значении намеренно не используются. Они не могут изменить результат, даже если это значение отличается от базового.

Метод Foundation trimmingCharacters(in:) возвращает новую строку, удалив совпадающие символы с обоих концов. Apple документирует использование .whitespacesAndNewlines для пробелов по краям; строка, состоящая только из таких символов, становится пустой. Отклонение пустого результата — дополнительное правило, заданное нами. Foundation trimmingCharacters(in:).

Запустите полный локальный пример на Mac

Для этого примера используйте инструментарий Swift 6. Swift.org предоставляет инструкции по установке на macOS с помощью Swiftly и указывает Xcode как один из вариантов разработки для платформ Apple. Откройте Terminal и выполните swift --version, чтобы убедиться, что команда доступна. Если её нет, сначала выполните инструкции по установке. Установка Swift на macOS, Начало работы.

Сохраните приведённую ниже полную программу в рабочей папке под именем MergeStyles.swift. Она импортирует Foundation, задаёт оба входных значения, выполняет объединение, проверяет ожидаемый результат и обрабатывает два случая отклонения. Вычисление не отправляет сетевых запросов и не сохраняет данные приложения.

import Foundation

struct PanelStyle: Equatable, Sendable {
    let title: String
    let spacing: Int
    let showsGrid: Bool
    let caption: String?
}

enum StyleMergeError: Error {
    case emptyTitle
    case negativeSpacing
}

func mergeStyles(
    base: PanelStyle,
    incoming: PanelStyle
) throws -> PanelStyle {
    let title = incoming.title.trimmingCharacters(
        in: .whitespacesAndNewlines
    )
    guard !title.isEmpty else {
        throw StyleMergeError.emptyTitle
    }
    guard base.spacing >= 0 else {
        throw StyleMergeError.negativeSpacing
    }

    return PanelStyle(
        title: title,
        spacing: base.spacing,
        showsGrid: incoming.showsGrid,
        caption: incoming.caption
    )
}

let base = PanelStyle(
    title: "Shape Study",
    spacing: 12,
    showsGrid: true,
    caption: "Blue squares"
)
let incoming = PanelStyle(
    title: "  Tile Study \n",
    spacing: 30,
    showsGrid: false,
    caption: nil
)

let expected = PanelStyle(
    title: "Tile Study",
    spacing: 12,
    showsGrid: false,
    caption: nil
)

let result = try mergeStyles(base: base, incoming: incoming)
precondition(result == expected)
precondition(base.title == "Shape Study")
precondition(base.caption == "Blue squares")
precondition(incoming.title == "  Tile Study \n")

print(result.title)
print("spacing: \(result.spacing)")
print("grid: \(result.showsGrid)")
print("caption cleared: \(result.caption == nil)")

let blankTitle = PanelStyle(
    title: " \n\t",
    spacing: 30,
    showsGrid: false,
    caption: nil
)
do {
    _ = try mergeStyles(base: base, incoming: blankTitle)
    preconditionFailure("Expected emptyTitle")
} catch StyleMergeError.emptyTitle {
    print("Rejected blank title")
}

let negativeBase = PanelStyle(
    title: "Shape Study",
    spacing: -1,
    showsGrid: true,
    caption: "Blue squares"
)
do {
    _ = try mergeStyles(base: negativeBase, incoming: incoming)
    preconditionFailure("Expected negativeSpacing")
} catch StyleMergeError.negativeSpacing {
    print("Rejected negative spacing")
}

Выполните в папке с файлом:

swift MergeStyles.swift

Ожидаемый вывод показан ниже. В исследовательской среде Swift был недоступен, поэтому программу проверили, но не запускали.

Tile Study
spacing: 12
grid: false
caption cleared: true
Rejected blank title
Rejected negative spacing

Инициализатор помогает принять главное решение: каждый аргумент указывает либо на выбранное поле входных данных, либо на именованное промежуточное значение. Ни одно из входных значений не изменяется. Семантика типов-значений Swift подходит для такого подхода; сами поля здесь тоже имеют типы-значения. Если позднее добавить поле со значением-классом, копирование содержащей его структуры не создаст отдельную копию экземпляра класса, на который она ссылается. Структуры и классы.

Проверка выполняется до создания экземпляра. Если входной заголовок пуст, функция выдаёт ошибку, а не молча восстанавливает базовый заголовок; если базовое расстояние отрицательное — тоже выдаёт ошибку, а не молча заменяет его нулём. Если не проходят обе проверки, первой сообщается ошибка emptyTitle, поскольку соответствующая проверка выполняется первой. Вызывающий код должен обработать ошибку, прежде чем использовать результат.

Решите, означает ли необязательное значение очистку или сохранение

В основном примере incoming.caption трактуется как окончательное желаемое значение подписи. Поэтому nil означает «в результате подписи нет». Это явно закреплено в соглашении, хотя представлено значение просто как необязательное.

В другом случае входное nil может означать «оставить базовую подпись». При такой политике аргумент caption в инициализаторе для замены задаётся так: caption: incoming.caption ?? base.caption. Оператор объединения с nil в Swift выбирает левый операнд, если тот содержит значение, а иначе использует правый. Это фрагмент для замены с меткой, а не вторая полная программа. Оператор объединения с nil.

При такой политике нельзя также использовать то же входное nil для запроса очистки. До применения выражения определите его смысл. Пустая строка — отдельное значение: при любой из политик входная подпись \"\" останется пустой подписью, если явно не добавить нормализацию.

Если в будущей операции редактирования потребуются все три действия — сохранить, заменить и очистить, — представьте действие отдельно. Ниже приведена самостоятельная полная программа, показывающая операцию только с подписью с помощью пользовательского перечисления:

enum CaptionChange {
    case keep
    case set(String?)
}

func applying(
    _ change: CaptionChange,
    to caption: String?
) -> String? {
    switch change {
    case .keep:
        return caption
    case .set(let replacement):
        return replacement
    }
}

let original: String? = "Blue squares"
precondition(applying(.keep, to: original) == original)
precondition(applying(.set("Green circles"), to: original)
    == "Green circles")
precondition(applying(.set(nil), to: original) == nil)
print("Caption actions checked")

Это перечисление иллюстрирует расширение соглашения. Оставьте исходную функцию с двумя значениями, если оба входа — полные стили; добавьте тип действия, если вход действительно описывает изменения.

Проверьте приоритет с заведомо разными значениями

Для полезной проверки объединения поля, конкурирующие за приоритет, должны различаться. Если у обоих входных значений расстояние 12 и в обоих включена сетка, неверный выбор источника может остаться незамеченным. В примере используются расстояния 12 и 30, а также значения true и false, поэтому ожидаемый результат показывает, какой вариант выбран.

Перед адаптацией функции проверьте следующие граничные случаи в соответствии с нужным соглашением:

  • Входное false должно остаться false; не используйте логическое ИЛИ для выражения замены.
  • При основной политике входная подпись nil должна очистить подпись.
  • Пробелы по краям заголовка должны удаляться, а внутренние пробелы — сохраняться.
  • Если входной заголовок состоит только из пробелов, должна возникнуть ошибка; базовый заголовок подставляться не должен.
  • Базовое расстояние 0 допустимо; отрицательное базовое расстояние должно вызвать ошибку.
  • При перестановке аргументов правила должны применяться к назначенным заново ролям.

Последний пункт важен, потому что результат этого объединения зависит от порядка. Если поменять входные данные примера местами, правила выберут \"Shape Study\", расстояние 30, значение сетки true и подпись \"Blue squares\". Это результат заданных правил. В местах вызова указывайте имена обоих аргументов, чтобы их роли оставались понятны.

При добавлении поля расширьте таблицу, инициализатор и проверку, используя конкурирующие значения. Если в будущем вложенный стиль будет содержать несколько независимых настроек, решите, заменяется ли всё вложенное значение целиком или для каждого его поля задаётся отдельный приоритет. Текущая реализация определяет правила только для четырёх показанных полей.

Отделите параллелизм от правил объединения

Это синхронное вычисление: оно читает аргументы, проверяет выбранные значения и возвращает результат либо выдаёт ошибку. Для определения приоритетов ему не нужны ни задача, ни актор. В документации Swift асинхронными называются функции, которые могут приостанавливаться; добавление асинхронного выполнения не определит, какое поле имеет приоритет. Параллелизм.

В примере PanelStyle объявлен как Sendable, чтобы указать, что его значения можно передавать через границы параллельного выполнения. Apple требует, чтобы структура, соответствующая Sendable, содержала передаваемые члены. Это соответствие касается передачи значений; правила выбора полей по-прежнему полностью задаются mergeStyles. Sendable.

Для этой локальной задачи с панелью рисования держите соглашение рядом с функцией и используйте заметно разные входные значения. Результат должен быть объясним по каждому полю: нормализованный входной заголовок, сохранённое базовое расстояние, входная настройка сетки и входная подпись, которая может явно очистить предыдущее значение.

Источники