ジェネリクス(Generics)

最終更新日: 2023/10/28 原文: https://docs.swift.org/swift-book/LanguageGuide/Generics.html

複数の型で機能するコードを記述し、それらの型で準拠が必要な要件を指定する。

ジェネリックなコードを使用すると、定義した要件に従って、任意の型で機能する柔軟で再利用可能な関数と型を作成できます。重複を避け、その意図を明確で抽象的な方法で表現するコードを書くことができます。

ジェネリクスは Swift の最も強力な機能の 1 つで、Swift 標準ライブラリの多くはジェネリックなコードで構築されています。気づかないかもしれませんが、この言語ガイド全体でもジェネリクスを使用しています。例えば、Swift の Array 型と Dictionary 型はどちらもジェネリックなコレクションです。Int 値を保持する配列、String 値を保持する配列、または Swift で作成できる他のどんな型の配列も作成できます。同様に、指定された型の値を格納する辞書を作成でき、その型に制限はありません。

ジェネリクスが解決する問題(The Problem That Generics Solve)

これは swapTwoInts(_:_:) と呼ばれる標準の非ジェネリック関数で、2 つの Int 値を交換します:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

この関数は、In-Out Parameters(In-Out パラメータ)で説明されているように、in-out パラメータを使用して ab の値を交換します。

swapTwoInts(_:_:) 関数は、b の値を a に、a の値を b に入れ替えます。この関数を呼び出して、2 つの Int 値の変数を交換できます:

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt は現在 \(someInt) で、 anotherInt は現在 \(anotherInt)")
// someInt は現在 107 で、 anotherInt は現在 3

swapTwoInts(_:_:) 関数は便利ですが、Int 値でのみ使用できます。2 つの String 値または 2 つの Double 値を交換する場合は、下記に示す swapTwoStrings(_:_:) および swapTwoDoubles(_:_:) 関数などの関数をさらに作成する必要があります:

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoInts(_:_:)swapTwoStrings(_:_:)、および swapTwoDoubles(_:_:) 関数の本文が同一なことに気付いたかもしれません。唯一の違いは、受け入れられる値の型(IntString、および Double)です。

任意の型の 2 つの値を交換する 1 つの関数を作成する方が、より便利で、はるかに柔軟です。ジェネリックなコードを使用すると、そのような関数を作成できます。(これらの関数のジェネリックなバージョンは、下記で定義されています)

NOTE 3 つの関数全てで、ab の型は同じでなければなりません。ab が同じ型でない場合、それらの値を交換することはできません。Swift は型安全な言語であり、(例えば)String 型の変数と Double 型の変数が互いに値を交換することを許可しません。そうしようとすると、コンパイルエラーが発生します。

ジェネリック関数(Generic Functions)

ジェネリック関数は、どの型でも機能します。これは、上記の swapTwoInts(_:_:) 関数のジェネリックなバージョンで、swapTwoValues(_:_:) と呼ばれます:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoValues(_:_:) 関数の本文は、swapTwoInts(_:_:) 関数の本文と同じです。ただし、swapTwoValues(_:_:) の最初の行は swapTwoInts(_:_:) とは少し異なります。最初の行を比較すると次のようになります:

func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

ジェネリックバージョンの関数は、実際の型名(IntStringDouble など)の代わりにプレースホルダの型名 (この場合は T) を使用します。プレースホルダの型名は、T に何が必要かについては指定しませんが、T が表すものにかかわらず、ab の両方が、同じ型 T でなければならないことを示しています。T の代わりに使用する実際の型は、swapTwoValues(_:_:) 関数が呼び出される度に決定されます。

ジェネリック関数と非ジェネリック関数のもう 1 つの違いは、ジェネリック関数の名前(swapTwoValues(_:_:))の後に山括弧内にプレースホルダの型名(<T>)を記述することです。山括弧は、TswapTwoValues(_:_:) 関数定義内のプレースホルダの型名だと Swift に伝えます。T はプレースホルダのため、Swift は T という実際の型を探しません。

swapTwoValues(_:_:) 関数は swapTwoInts と同じ方法で呼び出すことができますが、2 つの値が互いに同じ型の限り、任意の型の 2 つの値を渡すことができます。swapTwoValues(_:_:) が呼び出される度に、T に使用する型は、関数に渡される値の型から推論されます。

下記の 2 つの例では、T はそれぞれ IntString と推論されます:

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt は 107 で anotherInt は 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString は "world" で anotherString は "hello"

NOTE 上記で定義された swapTwoValues(_:_:) 関数は、Swift 標準ライブラリの swap と呼ばれる自動で利用可能なジェネリック関数に由来します。独自のコードで swapTwoValues(_:_:) 関数の動作が必要な場合は、独自の実装を提供するのではなく、Swift の既存の swap(_:_:) 関数を使用できます。

型パラメータ(Type Parameters)

上記の swapTwoValues(_:_:) の例では、プレースホルダ型 T は型パラメータの例です。型パラメータは、(<T> のように)プレースホルダ型を指定して名前を付け、関数名の直後に山括弧のペアの間に記述されます。

型パラメータを指定すると、それを使用して関数のパラメータの型(swapTwoValues(_:_:) 関数の a および b パラメータなど)を定義したり、関数の戻り値の型として、または関数の本文内の型注釈として定義できます。いずれの場合も、関数が呼び出される度に、型パラメータが実際の型に置き換えられます。(上記の swapTwoValues(_:_:) の例では、関数が最初に呼び出されたときに TInt に置き換えられ、2 回目に呼び出されたときは String に置き換えられました)

山括弧内に複数の型パラメータ名をカンマ(,)で区切って記述することにより、複数の型パラメータも指定できます。

型パラメータの命名(Naming Type Parameters)

ほとんどの場合、型パラメータには、Dictionary<Key, Value>KeyValueArray<Element>Element などのその型パラメータの意味を表す名前が付いています。ただし、型パラメータに意味がない場合、上記の swapTwoValues(_:_:) 関数の T など、TUV などの 1 文字の名前を付けるのが伝統的です。

NOTE 型パラメータには常に大文字のキャメルケース名(TMyTypeParameter など)を指定して、それらが値ではなく型のプレースホルダであることを示します。

ジェネリック型(Generic Types)

ジェネリック関数に加えて、Swift では独自のジェネリック型を定義できます。これらは、ArrayDictionary と同様の方法で、独自のクラス、構造体、および列挙型の任意の型で機能します。

このセクションでは、Stack と呼ばれるジェネリックなコレクション型を作成する方法を示します。スタックは、配列に似た順序付けられた値のセットですが、Array 型よりも操作が制限されています。配列を使用すると、配列内の任意の場所で新しいアイテムを挿入および削除できます。ただし、スタックでは、コレクションの最後にのみ新しいアイテムを追加できます(スタックに新しい値をプッシュする(pushing)と呼ばれています)。同様に、スタックでは、コレクションの最後からのみアイテムを削除できます(スタックから値をポップする(popping)と呼ばれています)。

NOTE UINavigationController クラスは、スタックの概念を使用して、ナビゲーション階層内の view controller をモデル化します。UINavigationController クラスの pushViewController(_:animated:) メソッドを呼び出して view controller をナビゲーションスタックに追加(プッシュ)し、popViewControllerAnimated\(\_:\) メソッドを呼び出してナビゲーションスタックから view controller を削除(ポップ)します。スタックは、コレクションの管理に厳密な「後入れ先出し(LIFO)」アプローチが必要な場合に役立つコレクションモデルです。

下記の図は、スタックのプッシュとポップの動作を示しています:

スタックのプッシュとポップ

  1. 現在、スタックには 3 つの値があります
  2. 4 番目の値はスタックの一番上にプッシュされます
  3. スタックには 4 つの値が保持され、最新の値が一番上になります
  4. スタックの一番上のアイテムがポップされます
  5. 値をポップした後、スタックは再び 3 つの値を保持します

スタックの非ジェネリックバージョンの Int のスタックを作成する方法は次のとおりです。

struct IntStack {
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

この構造体は、items と呼ばれる Array プロパティを使用して、値をスタックに格納します。Stack には、値をスタックにプッシュおよびポップするための 2 つのメソッド、pushpop が用意されています。これらのメソッドは、構造体の items 配列を変更する必要があるため、mutating とマークされています。

ただし、上記の IntStack 型は Int 値でのみ使用できます。任意の型の値のスタックを管理できるジェネリックな Stack 構造体を定義する方がはるかに便利です。

同じコードのジェネリックなバージョンを次に示します:

struct Stack<Element> {
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

Stack のジェネリックバージョンは、本質的に非ジェネリックバージョンと同じですが、実際の型 Int の代わりに Element という型パラメータを使用していることに注意してください。この型パラメータは、構造体の名前の直後に、一対の山括弧 (<Element>) 内に記述されます。

Element は、後で提供される型のプレースホルダ名を定義します。この未来の型は、構造体の定義内のどこでも Element として参照できます。この場合、Element は 3 つの場所でプレースホルダとして使用されてます:

  • Element 型の値の空の配列で初期化される、items というプロパティを作成します
  • push(_:) メソッドに item と呼ばれる Element 型の単一パラメータを指定しなければなりません
  • pop() メソッドの戻り値が Element 型を指定しなければなりません

ジェネリック型のため、Stack を使用して、Array および Dictionary と同様の方法で、Swift で有効な型のスタックを作成できます。

スタックに格納する型を山括弧(<>)で囲んで、新しいスタック インスタンスを作成します。例えば、文字列の新しいスタックを作成するには、Stack<String>() を記述します。

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// スタックは 4 つの文字列を格納しています

これらの 4 つの値をスタックにプッシュした後、stackOfStrings がどのように見えるかを次に示します:

4 つの値をスタックにプッシュ

スタックから値をポップすると、最上位の値 "cuatro" が削除されて返されます:

let fromTheTop = stackOfStrings.pop()
// fromTheTop は "cuatro" に等しく、スタックは 3 つの文字列を含んでいます

最上位の値をポップした後のスタックは次のとおりです:

最上位の値をポップ

ジェネリック型の拡張(Extending a Generic Type)

ジェネリック型を拡張する場合、extension の定義で型パラメータリストを提供しません。代わりに、元の型定義の型パラメータリストは extension の本文内でも使用でき、元の型パラメータ名は元の定義の型パラメータを参照するために使用します。

次の例では、ジェネリックな Stack 型を拡張して、topItem と呼ばれる読み取り専用の計算プロパティを追加します。これは、スタックからポップせずにスタックの一番上のアイテムを返します:

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

topItem プロパティは、Element 型のオプショナル値を返します。スタックが空の場合、topItemnil を返します。スタックが空でない場合、topItemitems 配列の最後のアイテムを返します。

この extension は型パラメータリストを定義していないことに注目してください。代わりに、Stack 型の既存の型パラメータ名の Element が extension 内で使用され、topItem 計算プロパティのオプショナルの型を示しています。

任意の Stack インスタンスで topItem 計算プロパティを使用して最後のアイテムへ削除せずにアクセスできるようになりました。

if let topItem = stackOfStrings.topItem {
    print("スタックの一番上のアイテムは \(topItem) 。")
}
// スタックの一番上のアイテムは tres 。

ジェネリック型の extension には、下記のExtensions with a Generic Where Clause(ジェネリック where 句を使った拡張)で説明されているように、新しい機能を取得するために拡張した型のインスタンスが満たさなければならない要件を含めることもできます。

型制約(Type Constraints)

swapTwoValues(_:_:) 関数と Stack 型は、どの型でも使用できます。ただし、ジェネリック関数およびジェネリック型で使用できる型に特定の型制約を適用すると便利な場合があります。型制約は、型パラメータが特定のクラスを継承する必要があるか、特定のプロトコルまたはプロトコル合成に準拠する必要があるかなどを指定できます。

例えば、Swift の Dictionary 型では、辞書のキーとして使用できる型に制限があります。Dictionaries(辞書)で説明されているように、辞書のキーの型はハッシュ可能でなければなりません。つまり、個々のキーがユニークだということを表明する方法を提供する必要があります。Dictionary は、特定のキーの値がすでに含まれているかどうかを確認できるように、そのキーがハッシュ可能なことを要求します。この要件がなければ、Dictionary は特定のキーの値を挿入または置換するべきかどうかを判断できず、すでに存在する特定のキーの値を見つけることもできません。

この要件は、Dictionary のキーの型制約によって強制されています。これは、キーの型が Swift 標準ライブラリで定義された Hashable プロトコルに準拠する必要があることを指定します。Swift の全ての基本型(StringIntDoubleBool など)は、デフォルトでハッシュ可能です。独自の型を Hashable プロトコルに準拠させる方法については、Conforming to the Hashable Protocolを参照ください。

独自のジェネリック型を作成するときに、独自の型制約を定義できます。これらの制約は、多くのジェネリックプログラミングに強力な機能を提供します。Hashable のような抽象的な概念は、具体的な型ではなく、概念的な特性の観点から型を特徴付けます。

型制約構文(Type Constraint Syntax)

型パラメータリストの一部として、型パラメータの名前の後にコロン(:)で区切って単一のクラスまたはプロトコルを配置することで、型制約を記述します。ジェネリック関数の型制約の基本的な構文を以下に示します(構文はジェネリック型でも同じです)。

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
     // ここに関数本文が入ります
}

上記の関数には、2 つの型パラメータがあります。最初の型パラメータ T は、TSomeClass を継承していることを要求する型制約を持ちます。2 番目の型パラメータ U は、USomeProtocol プロトコルに準拠していることを要求する型制約を持ちます。

型制約の挙動(Type Constraints in Action)

下記は、findIndex(ofString:in:) という名前の非ジェネリック関数です。これには、検索する String 値と、その String が含まれているかを検索する String の配列が与えられています。findIndex(ofString:in:) 関数は、オプショナルの Int 値を返します。これは、配列内で最初に一致する文字列が見つかった場合はそのインデックス、文字列が見つからない場合は nil になります。

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

findIndex(ofString:in:) 関数を使用して、文字列が配列内に存在するかどうかを検索できます。

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("llama のインデックスは \(foundIndex)")
}
// llama のインデックスは 2

ただし、配列内の値のインデックスを見つけるという動作は、文字列だけで有用というわけではありません。文字列を何らかの T 型の値に置き換えることで、ジェネリック関数と同じ機能を作成できます。

次は、findIndex(of:in:) と呼ばれる、findIndex(ofString:in:) のジェネリックバージョンを示しています。この関数は配列のオプショナル値ではなく、オプショナル値のインデックスを返すため、この関数の戻り値の型は引き続き Int? であることに注目してください。ただし、次の例の後に説明する理由により、この関数はコンパイルできないことに注意してください:

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

この関数はコンパイルできません。問題は、等価チェック "if value == valueToFind" にあります。Swift の全ての型を等価演算子(==)で比較できるわけではありません。例えば、複雑なデータモデルを表す独自のクラスまたは構造体を作成する場合、そのクラスまたは構造体の「等しい」の意味は Swift が推論できるものではありません。このため、このコードが全ての型 T で機能することを保証することはできず、コードをコンパイルしようとすると、適切なエラーが報告されます。

ただし、全てが失われるわけではありません。Swift 標準ライブラリは、Equatable と呼ばれるプロトコルを定義しています。このプロトコルに準拠する型では、その型の 2 つの値を比較するために、等価演算子(==)および不等価演算子(!=)を実装する必要があります。Swift の全ての標準型は、Equatable プロトコルを自動的にサポートしています。

Equatable に準拠する全ての型は、等価演算子をサポートすることが保証されているため、findIndex(of:in:) 関数で安全に使用できます。このことを表現するには、関数を定義するときに、型パラメータの定義に Equatable の型制約を記述します:

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

findIndex(of:in:) の単一の型パラメータは T: Equatable と記述されます。これは、「Equatable プロトコルに準拠する任意の型 T」を意味します。

findIndex(of:in:) 関数は正常にコンパイルされ、DoubleString などの Equatable に準拠する任意の型で使用できるようになりました:

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex は、9.3 が配列にないため、オプショナルの Int 型で値はありません。
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex は、2 を含むオプショナルの Int です

関連型(Associated Types)

プロトコルを定義するとき、1 つ以上の関連型を宣言すると便利な場合があります。関連型は、プロトコル内で使用される型にプレースホルダ名を与えます。その関連型に使用する実際の型は、プロトコルが準拠されるまで特定されません。関連型は、associatedtype キーワードで指定します。

関連型の挙動(Associated Types in Action)

下記は、Item と呼ばれる関連型を持つ Container と呼ばれるプロトコルの例です:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container プロトコルは、コンテナが提供しなければならない 3 つの必須機能を定義しています:

  • append(_:) メソッドを使用して、コンテナに新しいアイテムを追加できる必要があります
  • Int 値を返す count プロパティを介して、コンテナ内のアイテムの数にアクセスできる必要があります
  • Int のインデックスを受け取るサブスクリプトを使用して、コンテナ内の各アイテムを取得できる必要があります

このプロトコルは、コンテナ内のアイテムの保存方法や、許可される型を指定していません。プロトコルは、Container と見なされるために型が提供しなければならない 3 つ小さな機能のみを指定します。準拠する型は、これら 3 つの要件を満たす限り、追加の機能を提供できます。

Container プロトコルに準拠する全ての型は、格納する値の型を指定する必要があります。具体的には、正しい型のアイテムのみがコンテナに追加されるようにする必要があり、サブスクリプトで返されるアイテムの型を明確にする必要があります。

これらの要件を定義するために、Container プロトコルは、コンテナが保持する要素の型を参照する必要がありますが、特定のコンテナでその型が何なのかを知る必要はありません。Container プロトコルは、append(_:) メソッドに渡される値の型、およびサブスクリプトによって返される値の型とコンテナの要素の型が一致することを要求します。

これを実現するために、Container プロトコルは Item と呼ばれる関連型を associatedtype Item として記述することで宣言しています。プロトコルは Item が何かを定義しません。その情報は、準拠する型が提供します。それにもかかわらず、Item エイリアスを通して、Container 内のアイテムの型への参照、append(_:) メソッドと subscript で使用する型の定義は、どんな Container でも期待通りに動くことを保証しています。

下記は、Container プロトコルに準拠した、上記のGeneric Types(ジェネリック型)の非ジェネリック型 IntStack のジェネリックバージョンです:

struct IntStack: Container {
    // 元の IntStack 実装
    var items: [Int] = []
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // Container プロトコルへの準拠
    typealias Item = Int
    mutating func append(_ item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

IntStack 型は、Container プロトコルの 3 つの要件全てを実装し、それぞれの要件を満たすために IntStack 型の既存の機能を一部使用してます。

さらに、IntStack は、Container の実装の中で使用する ItemInt 型を指定しています。typealias Item = Int の定義により、抽象的な Item が、具象的な Int に変わります。

Swift の型推論のおかげで、IntStack の定義の中で ItemInt であることを宣言する必要は実際はありません。IntStackContainer プロトコルの全ての要件に準拠しているため、append(_:) メソッドの item パラメータの型とサブスクリプトの戻り値の型から、使用する適切な Item を推論できます。実際、上記のコードから typealias Item = Int 行を削除しても、Item に使用する型が明確になっているため、全てが引き続き機能します。

ジェネリックな Stack 型を Container プロトコルに準拠させることもできます:

struct Stack<Element>: Container {
    // 元の Stack<Element> 実装
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // Container プロトコルへの準拠
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

今回は、append(_:) メソッドの item パラメータの型とサブスクリプトの戻り値の型として型パラメータ Element を使用しています。したがって、Swift は、Element がこの特定のコンテナの Item として使用するのに適切な型だと推論できます。

関連型を特定するための既存の型の拡張(Extending an Existing Type to Specify an Associated Type)

Adding Protocol Conformance with an Extension(拡張機能を使ったプロトコル準拠の追加)で説明されているように、既存の型を拡張してプロトコルへの準拠を追加できます。これには、関連型を持つプロトコルが含まれます。

Swift の Array 型は、要素を取得するために、append(_:) メソッド、count プロパティ、および Int インデックスのサブスクリプトを既存で提供しています。これら 3 つの機能は、Container プロトコルの要件に準拠します。これは、Array がプロトコルに準拠することを宣言するだけで、Container プロトコルに準拠するように Array を拡張できることを意味します。Declaring Protocol Adoption with an Extension(拡張機能を使ったプロトコル準拠の宣言)で説明されているように、これは空の extension で行います:

extension Array: Container {}

Array の既存の append(_:) メソッドとサブスクリプトにより、Swift は、上記のジェネリックな Stack 型と同様に、Item に使用する適切な型を推論できます。この extension を定義した後、任意の配列を Container として使用できます。

関連型への制約の追加(Adding Constraints to an Associated Type)

プロトコルの関連型に型制約を追加して、準拠する型がそれらの制約を満たすことを要求できます。例えば、次のコードは、Container 内のアイテムが等価比較可能なバージョンを定義しています。

protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

このバージョンの Container に準拠するには、コンテナのアイテム型が Equatable プロトコルに準拠している必要があります。

関連型の制約へのプロトコルの使用(Using a Protocol in Its Associated Type’s Constraints)

プロトコルは、自分自身を要件の一部に含めることが出来ます。例えば、下記は Container プロトコルを改良し、suffix(_:) メソッドの要件を追加するプロトコルです。suffix(_:) メソッドは、コンテナの最後から指定された数の要素を返し、それらを Suffix 型のインスタンスに格納します。

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

このプロトコルでは、Suffix は、上記の Container の例の Item 型のように、関連型です。Suffix には 2 つの制約があります。SuffixableContainer プロトコル(現在定義されているプロトコル)に準拠している必要があり、その Item 型はこのコンテナの Item 型と一致することが必要です。Item の制約はジェネリック where 句です。これについては、下記のAssociated Types with a Generic Where Clause(ジェネリック where 句を使用した関連型)で説明します。

上記のGeneric Types(ジェネリック型)Stack 型に SuffixableContainer プロトコルへの準拠を追加した拡張を次に示します:

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Suffix が Stack であると推論されます
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// Suffix には 20 と 30 が含まれます

上記の例では、Suffix の関連型も Stack のため、Stack の suffix 操作は別の Stack を返します。他にも、SuffixableContainer に準拠する型は、それ自体とは異なる Suffix 型を持つことができます。つまり、suffix 操作は異なる型を返すことができます。例えば、IntStack の代わりに Stack<Int>Suffix 型として使用して、非ジェネリック型 IntStackSuffixableContainer への準拠を追加する extension を次に示します:

extension IntStack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack<Int> {
        var result = Stack<Int>()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Suffix が Stack<Int> だと推論されます
}

ジェネリック where 句(Generic Where Clauses)

Type Constraints(型制約)で説明されているように、型制約を使用すると、ジェネリック関数、サブスクリプト、または型に関連した型パラメータの要件を定義できます。

関連型の要件を定義するのにも役立ちます。これを行うには、ジェネリック where 句を定義します。ジェネリック where 句を使用すると、関連型が特定のプロトコルに準拠する必要があること、または特定の型パラメータと関連型を同じにする必要があることを要求できます。ジェネリック where 句は where キーワードで始まり、その後に関連型の制約、または型と関連型の間の等価関係が続きます。型または関数の本文の開始中括弧({)の直前に、ジェネリック where 句を記述します。

下記の例では、2 つの Container インスタンスに同じアイテムが同じ順序で含まれているかどうかを確認する allItemsMatch というジェネリック関数を定義しています。この関数は、全てのアイテムが一致する場合はブール値 true を返し、一致しない場合は false の値を返します。

チェックする 2 つのコンテナは、必ずしも同じ型の必要はありませんが(同じ型のコンテナでもかまいません)、同じ型のアイテムを保持している必要があります。この要件は、型制約とジェネリック where 句の組み合わせによって表現されます:

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // 両方のコンテナに同じ数のアイテムが含まれていることを確認します
        if someContainer.count != anotherContainer.count {
            return false
        }

        // アイテムの各ペアをチェックして、同等かどうかを確認します
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // 全てのアイテムが一致するため、true を返します
        return true
}

この関数は、someContaineranotherContainer という 2 つの引数を取ります。someContainer 引数は C1 型で、otherContainer 引数は C2 型です。C1C2 は、どちらも関数が呼び出されたときに決定される 2 つの Container 型の型パラメータです。

関数の 2 つの型パラメータには、次の要件が適用されます。

  • C1 は、Container プロトコル(C1: Container と表記)に準拠する必要があります
  • C2 も、Container プロトコル(C2: Container と表記)に準拠する必要があります
  • C1Item は、C2Item と一致する必要があります(C1.Item == C2.Item と記述)
  • C1Item は、Equatable プロトコル(C1.Item: Equatable と表記)に準拠する必要があります

1 番目と 2 番目の要件は関数の型パラメータリストで定義され、3 番目と 4 番目の要件は関数のジェネリック where 句で定義されます。

これらの要件は次のことを意味します。

  • someContainerC1 型のコンテナです
  • anotherContainer は、C2 型のコンテナです
  • someContaineranotherContainer には、同じ型のアイテムが含まれています
  • someContainer 内の Item は、不等演算子 (!=) を使用して、互いに異なるかどうかを確認できます

3 番目と 4 番目の要件を組み合わせると、otherContainer のアイテムは someContainer のアイテムとまったく同じ型のため、!= 演算子を使用してチェックすることもできます。

これらの要件により、allItemsMatch(_:_:) 関数は、2 つのコンテナが異なるコンテナ型でも、それらを比較できます。

allItemsMatch(_:_:) 関数は、両方のコンテナに同じ数のアイテムが含まれていることを確認することから始まります。それらに含まれるアイテムの数が異なる場合、一致する可能性はなく、関数は false を返します。

このチェックを行った後、関数は for-in ループと半開範囲演算子(..<)を使用して someContainer 内の全てのアイテムを反復します。各アイテムについて、関数は someContainer のアイテムが anotherContainer の対応するアイテムと等しくないかどうかをチェックします。2 つのアイテムが等しくない場合、2 つのコンテナは一致せず、関数は false を返します。

不一致が見つからずにループが終了した場合、2 つのコンテナは一致し、関数は true を返します。

allItemsMatch(_:_:) 関数の実際の動作は次のとおりです:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("全てアイテムが一致しました。")
} else {
    print("全てのアイテムが一致したわけではありません。")
}
// 全てアイテムが一致しました。

上記の例では、String 値を格納する Stack インスタンスを作成し、3 つの文字列をスタックにプッシュします。この例では、スタックと同じ 3 つの文字列を含む配列リテラルで初期化された Array インスタンスも作成します。スタックと配列は型が異なりますが、どちらも Container プロトコルに準拠しており、両方に同じ型の値が含まれています。したがって、これら 2 つのコンテナを引数として allItemsMatch(_:_:) 関数を呼び出すことができます。上記の例では、allItemsMatch(_:_:) 関数は、2 つのアイテムの全てが合致していることを出力します。

ジェネリック where 句を使った拡張(Extensions with a Generic Where Clause)

extension にジェネリック where 句を使用することもできます。下記の例では、前の例のジェネリックな Stack 構造体を拡張して、isTop(_:) メソッドを追加しています:

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

この新しい isTop(_:) メソッドは、最初にスタックが空でないことを確認してから、指定されたアイテムをスタックの最上位のアイテムと比較します。isTop(_:) の実装では == 演算子を使用していますが、Stack の定義ではそのアイテムが Equatable の要件を設定していないため、== 演算子はコンパイルエラーになります。ジェネリック where 句を使用すると、 extension に新しい要件を追加できるため、extension はスタック内のアイテムが Equatable の場合にのみ isTop(_:) メソッドを追加します。

isTop(_:) メソッドの実際の動作は次のとおりです:

if stackOfStrings.isTop("tres") {
    print("最上位の要素は tres です。")
} else {
    print("最上位の要素はその他のなにかです。")
}
// 最上位の要素は tres です。

要素が Equatable でないスタックで isTop(_:) メソッドを呼び出そうとすると、コンパイルエラーが発生します。

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // エラー

プロトコルの extension にもジェネリック where 句を使用できます。下記の例では、前の例の Container プロトコルを拡張して、startsWith(_:) メソッドを追加しています。

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

startsWith(_:) メソッドは、まずコンテナに少なくとも 1 つのアイテムがあることを確認し、次にコンテナ内の最初のアイテムが指定されたアイテムと一致するかどうかを確認します。この新しい startsWith(_:) メソッドは、コンテナのアイテムが等価比較可能である限り、Container プロトコルに準拠する任意の型で使用できます。

if [9, 9, 9].startsWith(42) {
    print("42 から始まります。")
} else {
    print("その他の数字から始まります。")
}
// その他の数字から始まります。

上記の例のジェネリック where 句では、Item がプロトコルに準拠する必要がありますが、Item の型を特定する必要があるジェネリック where 句を記述することもできます。例えば:

extension Container where Item == Double {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += self[index]
        }
        return sum / Double(count)
    }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// 648.9

この例では、Item 型が Double のコンテナに average() メソッドを追加します。コンテナ内のアイテムを繰り返して合計し、コンテナの数で除算して平均を計算します。浮動小数点除算ができるように、countInt から Double に明示的に変換します。

他の場所で記述したジェネリック where 句の場合と同様に、extension にあるジェネリック where 句に複数の要件を含めることができます。各要件はカンマ(,)で区切ります。

コンテキスト上の where 句(Contextual Where Clauses)

既にジェネリック型を使っているコンテキスト上で、型制約を持たない宣言にジェネリック where 句を記述できます。例えば、ジェネリック型のサブスクリプト、またはジェネリック型の extension のメソッドにジェネリック where 句を記述できます。Container 構造体はジェネリックで、下記の例の where 句は、新しいメソッドを Container で使用できるようにするために満たす必要のある型制約を指定しています。

extension Container {
    func average() -> Double where Item == Int {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
    func endsWith(_ item: Item) -> Bool where Item: Equatable {
        return count >= 1 && self[count-1] == item
    }
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// 648.75
print(numbers.endsWith(37))
// true

この例では、アイテムが整数の場合は average() メソッドを Container に追加し、アイテムが Equatable の場合は endsWith(_:) メソッドを追加します。どちらの関数にも、Container で宣言したジェネリックな Item 型パラメータに型制約を追加するジェネリック where 句が含まれています。

コンテキスト上の where 句を使用せずにこのコードを記述したい場合は、ジェネリック where 句ごとに 1 つずつ、2 つの extension を記述します。上記の例と下記の例は同じ動きをします。

extension Container where Item == Int {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += Double(self[index])
        }
        return sum / Double(count)
    }
}
extension Container where Item: Equatable {
    func endsWith(_ item: Item) -> Bool {
        return count >= 1 && self[count-1] == item
    }
}

コンテキスト上の where 句を使用する例のバージョンでは、各メソッドのジェネリック where 句が、そのメソッドを使用可能にするために満たす必要がある要件を示しているため、average()endsWith(_:) の実装は両方とも同じ extension 内にありました。これらの要件を extension のジェネリック where 句に移動すると、同じ状況でメソッドを使用できるようになりますが、要件ごとに 1 つの extension が必要になります。

ジェネリック where 句を使用した関連型(Associated Types with a Generic Where Clause)

関連型にジェネリック where 句を含めることができます。例えば、Swift 標準ライブラリの Sequence プロトコルで使用するようなイテレータを含む Container のバージョンを作成するとします。書き方は次のとおりです:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

Iterator のジェネリック where 句では、Iterator の型に関係なく、コンテナのアイテムと同じアイテム型の要素をイテレータが反復することを要求します。makeIterator() 関数は、コンテナのイテレータへのアクセスを提供します。

別のプロトコルを継承したプロトコルの場合、プロトコル宣言にジェネリック where 句を含めることにより、継承した関連型に制約を追加できます。例えば、次のコードは、ItemComparable に準拠することを要求する ComparableContainer プロトコルを宣言しています:

protocol ComparableContainer: Container where Item: Comparable { }

ジェネリックサブスクリプト(Generic Subscripts)

サブスクリプトはジェネリックにすることができ、ジェネリック where 句を含めることができます。サブスクリプトの後の山括弧内(<>)にプレースホルダの型名を記述し、サブスクリプトの本文の開始の中括弧({)の直前にジェネリック where 句を記述します。例えば:

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result: [Item] = []
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

Container プロトコルへのこの extension は、インデックスのシーケンスを受け取り、指定された各インデックスのアイテムを含む配列を返すサブスクリプトを追加します。このジェネリックなサブスクリプトは、次のように制約されます:

  • 山括弧(<>)内のジェネリックパラメータのインデックスは、Swift 標準ライブラリの Sequence プロトコルに準拠した型にする必要があります
  • サブスクリプトは、単一の Indices 型のインスタンスのパラメータ indices を受け取ります
  • ジェネリック where 句では、シーケンスのイテレータが Int 型の要素を繰り返し処理することを要求します。これにより、シーケンス内のインデックスは、コンテナに使用されるインデックスと同じ型だということが保証されます

まとめると、これらの制約は、indices パラメータに渡される値が整数のシーケンスだということを意味します。

results matching ""

    No results matching ""