2つのSwift構造体の値をマージするには、各フィールドの採用元を選び、新しい値を返す関数を記述します。このmacOSの例では、2つの架空の描画パネルスタイルを組み合わせ、置き換え、維持、オプショナル値のクリア、無効な入力に関する明示的なルールを示します。

マージ処理を書く前に優先順位を決める

読者が行うのは、Mac上で既存の2つの値から描画パネルスタイルを1つ計算することです。入力をbaseとincomingと呼びます。これらの名前はそれぞれの役割を表し、どちらの値を採用するかはフィールドごとの取り決めで決まります。この計算では、メモリ上の架空の値だけを使います。

Swiftでは構造体は値型として説明され、イニシャライザを使ってインスタンスを作成できます。Appleがドキュメントで説明するmerge(_:uniquingKeysWith:) APIはDictionaryに属し、クロージャを使って重複キーを解決します。この独自の構造体では、実装にフィールドごとの意図したルールを表すマージ関数を定義します。構造体とクラス、Dictionary merge(_:uniquingKeysWith:)。

以下の例示的な取り決めを使います。これらはこの例のための設計上の選択であり、Swiftが自動的に提供する動作ではありません。

フィールド ルール 結果の例
title 入力テキストを採用し、前後の空白と改行を削除してから、空でないことを検証する " Tile Study \n"は\"Tile Study\"になる
spacing ベース値を維持し、0以上であることを必須とする ベースの12が入力の30より優先される
showsGrid 入力のブール値をそのまま採用する 入力のfalseがベースのtrueを置き換える
caption 入力のオプショナル値をそのまま採用する 入力のnilによってベースのキャプションがクリアされる

この表では、混同しやすい3つの操作を区別しています。採用元の選択、選択した値の正規化、結果の検証です。入力側の間隔は意図的に使いません。その値がベースの間隔と異なっていても、出力には影響しません。

FoundationのtrimmingCharacters(in:)は、両端から一致する文字を削除した新しい文字列を返します。Appleのドキュメントでは、前後の空白を処理するために.whitespacesAndNewlinesを使う方法が説明されています。これらの文字だけでできた文字列は空になります。空の結果を拒否するのは、ここで追加するルールです。Foundation trimmingCharacters(in:)。

Macで完全なローカルの例を実行する

この例にはSwift 6のツールチェーンを使います。Swift.orgでは、Swiftlyを使ったmacOSへのインストール手順が案内され、Appleプラットフォーム開発の選択肢としてXcodeも紹介されています。Terminalを開き、swift --versionを実行してコマンドが使えることを確認します。見つからない場合は、続行する前にインストール手順に従ってください。macOSへのSwiftのインストール、入門。

次の完全なプログラムを作業フォルダにMergeStyles.swiftとして保存します。Foundationをインポートし、2つの入力値を定義して、マージを実行し、期待する結果を確認したうえで、2つの拒否ケースを試します。この計算はネットワークリクエストを送信せず、アプリケーションデータも保存しません。

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の値セマンティクスを活用しており、ここで扱うフィールド自体も値型です。後でクラス型のフィールドを追加した場合、外側の構造体をコピーしても、参照先のクラスインスタンスが独立してコピーされるわけではありません。構造体とクラス。

検証は構築前に行います。入力タイトルが空白なら、ベースのタイトルに暗黙に戻すのではなくエラーをスローします。ベースの間隔が負なら、0に暗黙に丸めるのではなくエラーをスローします。両方の条件に該当する場合、先に実行されるガードによりemptyTitleが最初に報告されます。呼び出し側は、結果を使う前にエラーを処理する必要があります。

オプショナル値がクリアを意味するか維持を意味するか決める

主な例では、incoming.captionをキャプションの完全な希望値として扱います。そのため、そのnilは「結果にキャプションがない」ことを意味します。表現は単なるオプショナルですが、これにより取り決めでクリアを明示できます。

別の要件として、入力のnilが「ベースのキャプションを維持する」ことを意味する場合もあります。その方針では、置き換え用のイニシャライザ引数をcaption: incoming.caption ?? base.captionとします。Swiftのnil合体演算子は、左側に値があれば左側を選び、なければ右側を使います。これはラベル付きの置き換え用コードであり、2つ目の完全なプログラムではありません。nil合体演算子。

このフォールバック方針では、同じ入力のnilをクリア要求としても使うことはできません。この式を採用する前に、意味を決めてください。空文字列は別の値です。どちらの方針でも、明示的に正規化を追加しない限り、入力キャプションが\"\"なら空のキャプションのままです。

今後の編集操作で「維持」「置き換え」「クリア」の3つすべてが必要になったら、操作を別に表現します。次の独立した完全なプログラムは、独自のenumを使ったキャプションだけの操作を示します。

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")

このenumは取り決めを拡張した例です。両方の入力が完全なスタイルであれば、元の2値関数を使います。入力が実際に編集内容を表す場合は、操作型を導入します。

意図的に異なる値を使って優先順位を確認する

有用なマージ確認では、競合するフィールドの値を異なるものにします。両方の入力で間隔が12、グリッドが有効だと、採用元を間違えても気づかずに通ることがあります。この例では、間隔を12と30、またtrueとfalseをそれぞれ使い、期待結果からその選択が分かるようにしています。

関数を応用する前に、次の境界ケースが意図した取り決めと一致するか確認してください。

  • 入力のfalseはfalseのままにします。置き換えを表すためにブール値のORを使わないでください。
  • 主な方針では、入力キャプションのnilによってキャプションがクリアされます。
  • タイトルの前後の空白は削除されますが、内部の空白は残ります。
  • 空白文字だけの入力タイトルは、ベースに戻らずエラーになります。
  • ベースの間隔0は有効です。負のベース間隔はエラーになります。
  • 引数を入れ替えると、新しく割り当てられた役割にルールが適用されます。

最後の点は重要です。このマージは順序に依存します。例の入力を逆にすると、ルールに従って\"Shape Study\"、間隔30、グリッドtrue、キャプション\"Blue squares\"が選ばれます。呼び出し箇所では両方の引数に名前を付け、役割が見えるようにしてください。

フィールドを追加するときは、表、イニシャライザ、そして異なる値を使う確認項目をそれぞれ更新します。今後、入れ子になったスタイルに複数の独立した設定が含まれる場合、入れ子全体を置き換えるのか、各フィールドに個別の優先順位を設けるのかを決めてください。現在の実装で定義しているのは、ここに示した4つのフィールドだけです。

並行処理とマージ方針を分けて考える

この計算は同期的です。引数を読み、選択した値を検証し、結果を返すかエラーをスローします。優先順位を決めるためにタスクやアクターは必要ありません。Swiftでは非同期関数は中断できる関数として説明されています。非同期実行を加えても、どのフィールドを採用するかは決まりません。並行処理。

この例ではPanelStyleをSendableとして宣言し、その値を並行処理の境界を越えて渡せることを示しています。Appleでは、Sendableな構造体がSendableなメンバーを含むことを必須としています。この適合性が関わるのは値の受け渡しであり、フィールド選択のルールはすべてmergeStylesによって決まります。Sendable。

このローカルな描画パネルのタスクでは、取り決めを関数のそばに置き、値が明らかに異なるサンプル入力を使います。結果の値はフィールドごとに説明できます。正規化された入力タイトル、維持されたベースの間隔、入力のグリッド設定、そして以前の値を明示的にクリアできる入力キャプションです。

参考資料