合成したSwiftの値を挿入順を変えずに一意に保つには、入力配列を一度走査し、受け入れたキーをセットで管理して、最初に現れた値だけを出力配列に追加します。このガイドでは、Macで実行できる小さなローカル計算を作成します。

このタスクでは、合成した図形値の順序付きリストを処理し、重複する値を取り除き、残った項目から合計を計算します。すべてメモリ上で行われ、この例では外部入力を読み込まず、計算結果も保存しません。

コレクションを選ぶ前に「一意」の意味を定める

Swiftの配列は順序を保ち、重複を許します。セットには一意の値が含まれますが、順序は定義されていません。そのため、配列をセットに変換してから配列に戻しても、最初に現れた順序は保たれません。後からソートするとソート順が定まり、挿入順とは異なる場合があります。これらのコレクションの動作については、Swiftのコレクション型に記載されています。

この例では、合成した各値は図形名とサイズを持ちます。既定の仕様では、両方のプロパティが一致する場合に限り、2つの項目を重複とみなします。したがって、サイズが12の円と、サイズが20の円は別々の値として残ります。

仕様に関する質問 この計算でのルール
入力順を決めるものは何か? 渡された配列内の位置
2つの値を重複とみなす条件は何か? 図形とサイズが等しいこと
どの出現を残すか? 最初の出現
後から出現した重複値によって、残す値の位置は変わるか? いいえ
入力が空の場合はどうなるか? 空の配列と合計0を返す
合計に含める値はどれか? 残った値のみ

この仕様により、計算で図形名とサイズの組み合わせごとの区別が必要なのに、図形名だけで項目を削除してしまうという、ありがちなミスを防げます。実例では、ルールを変えた場合の結果がわかるよう、図形名だけで判定した結果も計算します。

Macで完全なローカルSwiftの例を作成する

Swiftツールチェーンがインストールされている必要があります。ターミナルでswift --versionを実行し、コマンドを利用できることを確認します。Swift公式のコマンドラインツールのビルドチュートリアルには、この確認方法、実行可能パッケージの作成方法、パッケージの実行方法が記載されています。

例用の新しいディレクトリを作成し、実行可能パッケージを初期化します。

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

Sources内に生成された実行可能ファイルのソースを、以下の完全なプログラムに置き換えます。生成されるファイル名とディレクトリ構成はツールチェーンによって異なることがあります。既存のエントリーポイントを置き換え、2つ目のエントリーポイントは追加しないでください。このプログラムは@mainエントリーポイントを使うため、ファイル名をOrderDemo.swiftにします。必要に応じて、生成されたmain.swiftの名前を変更してください。Swiftのコマンドラインチュートリアルでは、main.swiftファイルと@mainエントリーポイントのどちらを選ぶかについて説明しています。

インポートやパッケージ依存関係は必要ありません。このプログラムは標準ライブラリの型と関数を使います。

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

パッケージのディレクトリから、次を実行します。

swift run OrderDemo

ビルドメッセージの後に表示される、プログラムの想定出力を以下に示します。この調査環境にはSwift実行ファイルがないため、取得したAPIドキュメントと照合してプログラムを確認しましたが、ここでは実行していません。

circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20

配列とセットの組み合わせで順序が保たれるのはなぜか?

出力順の唯一の基準は配列です。ループは入力の各位置を順番に確認し、append(_:)は受け入れた値を結果の末尾に追加します。セットの役割は、選択したキーがすでに現れたかどうかを判定することだけです。プログラムがセットを反復処理することはありません。

重要な式はseen.insert(key(value)).insertedです。Swiftのドキュメントによると、新しいメンバーの場合、insert(_:)はtrueとしてinsertedを返し、等しいメンバーがすでに存在する場合はfalseを返します。既存のメンバーは変更されません。この戻り値を使うと、1つの操作で元の値を追加するかどうかを決められます。AppleのSetの挿入に関するドキュメントを参照してください。

渡された入力では、最初の円と四角形が受け入れられます。2つ目の同一の円はスキップされます。より大きな円は値全体が異なるため受け入れられ、繰り返された四角形はスキップされます。したがって、残るサイズは12 + 8 + 20 = 40です。

この実装は有用な不変条件を保ちます。各反復の後、結果には、seenの各キーについて最初に見つかった値だけが、出現順に含まれています。新しいキーは両方のコレクションに追加され、繰り返されたキーはどちらも変更しません。この考え方により、セットがメンバーをどのように格納しているかに関係なく、順序の保証を説明できます。

独自のSwift値では等価性をどう表現すればよいか?

宣言struct Tile: Hashableは、準拠の自動合成を指定します。格納プロパティのStringとIntはどちらもハッシュ可能なので、Swiftは必要な動作を合成できます。AppleのHashableには、この合成条件が記載されています。

キーに{ $0 }を使うと、Tile値全体によって一意性が決まります。{ $0.shape }を使うと、図形ごとに1項目だけ残す仕様に変わります。この例では、サイズが20の後の円が削除され、合計は20に減ります。これは2つのルールを意図的に比較するものであり、同じルールの別実装ではありません。

計算の実行中は、キーの選択が一貫しているようにします。カウンターや変化する外部変数からではなく、処理中の値からキーを導出してください。そうしないと、等しい入力に異なるキーが割り当てられ、別々の項目として残る可能性があります。

等価性とハッシュを手動で実装する場合は、両方で同じ基本プロパティを使います。Appleは、等しいインスタンスが同じ順序で同じ値をハッシャーに渡すことを求めています。一意性キーとしてhashValue自体を使うのは避けてください。ハッシュは値の等価性の仕様の代わりにはなりません。実際のハッシュ可能なキーを保持すれば、セットがその仕様を適用できます。Hashableの準拠要件を参照してください。

代わりに順序付きコレクションを使うべきなのはどんなときか?

この1回の計算では、配列とローカルなセットを組み合わせると、動作が明確になります。アプリケーションで一意なメンバーを繰り返し追加、削除、並べ替え、検索する場合は、専用の順序付きコレクションを使うと、こうした操作を保守しやすくなります。

選択肢 適した条件 関連する制約
配列とセット 選択したキーを使うこともある、合成したSwift値の1回の走査 初出時の動作は関数で定義する
Swift CollectionsのOrderedSet ハッシュ可能な一意の値を持つ順序付きコレクションを継続的に使用する 別途Swift Collectionsパッケージが必要
FoundationのNSOrderedSet Foundationの順序付きオブジェクトコレクションを使うAPIがすでにある 静的な順序付きコレクションを表すクラス。変更可能な操作にはNSMutableOrderedSetを使う

Swift Collectionsの公式ドキュメントでは、OrderedSetを一意な要素の順序付きコレクションとして説明しており、OrderedCollectionsを通じてインポートします。append(_:)やinsert(_:at:)など、位置を考慮した操作を提供します。これらはパッケージAPIなので、依存関係のないプロジェクトにimport OrderedCollectionsを追加するだけでは設定できません。OrderedSetを参照してください。

AppleのFoundationドキュメントでは、NSOrderedSetは一意なオブジェクトの静的な順序付きコレクションと説明されており、変更可能な対応クラスとしてNSMutableOrderedSetが挙げられています。上記のネイティブなSwift値の計算では、ジェネリック関数によってオブジェクトコレクションを導入せずに、選択した等価キーを直接表現できます。NSOrderedSetを参照してください。

デスクトップのインターフェイスに計算結果を渡すと何が変わるか?

完全な例は同期的に動作し、一時的なコレクションを関数内で管理します。小さな合成リストなら、タスクを導入せずに処理順を明確にできます。

macOSのインターフェイスでは、UIに関わる状態と更新を適切に分離します。Appleの説明によると、MainActorは、そのエグゼキューターがメインディスパッチキューと等価なグローバルアクターです。Swiftの並行処理ガイドでは、インターフェイスの状態にアクセスする関数や型に@MainActorを適用する方法を説明しています。MainActorと並行処理を参照してください。

並行処理では、順序を明示的に定義する必要もあります。Swiftは、タスクグループ内の子タスクが完了する順序を保証しません。タスクの完了時に結果を追加すると、完了順が保たれますが、これは元の入力順とは異なる場合があります。この制約は公式の並行処理ガイドに記載されています。

将来、独立した合成計算を並行して実行する場合は、各入力位置を結果とともに保持し、収集後に入力位置順へ戻してから、一意性の判定を行ってください。これは、ドキュメントに記載された完了順の制約から導かれる設計です。追加の作業によって必要になるまでは、現在の小さな例を同期処理のままにしてください。

関数を再利用する前に仕様を確認する

このプログラムには、残る値の正確な順序、空の入力、同一値の繰り返し、図形名だけで判定する代替ルールを確認するチェックが含まれています。これらのチェックは、計算の意味が変わりやすい判断を対象にしています。

このプログラムを変更する場合は、次の3点を確認してください。入力に意味のある順序がすでにあること、キーが意図した等価ルールを表すこと、そして最初の出現を残すのが適切であることです。この関数は、後から現れる一致項目を意図的にスキップします。サイズを統合したり、残す値を置き換えたりはしません。スキップした数を報告することで、その影響を明らかにします。

渡された合成図形リストでは、完成した結果は初出順の3つの値と、合計40です。順序は配列、メンバーの有無はセット、「一意」の意味は選択したキーに担当させてください。

参考資料