2020/2/8 Swift ジェネリック(Generics)パート2

連想型の型制約にプロトコルを使う(Using a Protocol in Its Associated Type’s Constraints)

プロトコル定義内の要求の中にプロトコルを出現させることもできます。例えば、suffix(_:)メソッドの実装の要求を追加して、Containerプロトコルを改良します(suffix:接尾辞)。suffix(_:)メソッドは与えられた数の要素をcontainerの末尾から取り出して返します。取り出された要素をSuffix型(連想型)のインスタンスに格納して返します。

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

上の例のContainerプロトコルにおける連想型Itemのように、このプロトコルにおいてSuffixは連想型です。連想型Suffixには二つの制約があります。

  • Suffixは、SuffixableContainerプロトコル(まさに定義しようとしているプロトコル自身)に準拠した型を指定する必要がある。
  • SuffixのItem型とcontainerのItem型が同じ型出なければならない。

Itemへの制約はgeneric where clauseです。generic where clauseはこの後説明します。

sample3-2のジェネリック型のStack<Element>型をSuffixableContainerプロトコルに準拠させたのが、sample5-1です。

sample5-1

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

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

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
 
 
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..<count {
            result.append(self[index])
        }
        return result
    }
    // Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
stackOfInts.append(40)
stackOfInts.append(50)
stackOfInts.append(60)
let suffix = stackOfInts.suffix(3)
print(suffix)

//Stack<Int>(items: [40, 50, 60])

sample5-1で、Stack<Element>を拡張した際の連想型Suffixとして指定された型はStack型です。ですので「拡張後のStack型」におけるsuffixメソッド実行での返り値は、別のStack型、ということになります。あるいは、SuffixableContainerプロトコルに準拠した型は、その型自身とは別の型であるSuffix型を持つ、–つまりsuffixメソッドはその型自身とは別の型を返す、と言えます。例えば、下のサンプルは非ジェネリック型のIntStack型をSuffixableContainerプロトコルに準拠させた例です。

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
    }
    // Inferred that Suffix is Stack<Int>.
}

Generic Where Clauses

型引数の制約は、ジェネリック関数・ジェネリックサブスクリプト・ジェネリック型の型引数に対する要求(制約)を定義できます。

プロトコルの連想型に対しても要求(制約)を定義すると便利な時があります。generic where clauseを使用して要求(制約)を定義します。generic where clauseは「連想型の具体的な型を指定する際に、特定のプロトコルに準拠した型を指定する必要がある」

工事中🏗

generic where clauseはwhereキーワードで書き始めます。whereキーワードの後ろに連想型の制約、あるいは型と連想型のイコール関係を記述します。generic where clauseは型や関数の本体の始まりを表す波かっこの直前に記述します。

sample6-1はallItemsMatchという名のジェネリック関数を定義しています。この関数は二つのContainerインスタンス(Containerプロトコルに準拠した型のインスタンス)が同じ要素を同じ順番で格納しているか否かをチェックする関数です。全ての同じ要素が同じ順番で格納されている場合trueを返し、そうでない場合falseを返します。

チェックされる二つのcontainerは同じ型である必要はありません(同じ型であっても問題ありません)。しかし二つのcontainerが含む要素の型は同じである必要があります。この要求を型制約とgeneric where clauseで表現しています。

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

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

この関数はsomeContainerとanotherContainerの二つの引数を取ります。someContainerはC1型、anotherContainerはC2型と型注釈されています。C1とC2は二つのcontainerの型を制約する型引数であり、関数が呼び出される時に(C1,C2に対応する実際の引数の型が)決定します。

二つの型引数に関して下記の要求があります。

  • C1はContainerプロトコルへ準拠した型でなければならない(C1:Container)
  • C2もContainerプロトコルへ準拠した型でなければならない(C2:Container)
  • C1のItemはC2のItemと同じでなければならない。(C1.Item == C2.Item)
  • C1のItemはEquatableプロトコルへ準拠した型でなければならない。(C1.Item: Equatable)

一つ目と二つ目の要求は関数の型引数で定義されています。三つ目と四つ目の要求はgeneric where clauseで定義されています。

これらの要求はつまり

  • someContainerはC1型のcontainerである
  • anotherContainerはC2型のcontainerである
  • someContainerとanotherContainerは同じ型の要素を格納している
  • someContainerの要素はnot equal to演算子( != )で、互いに違う値であるかをチェックできる

三つ目の四つ目の要求により、anotherContainerの要素もnot equal to演算子( != )を用いることが可能です。なぜならsomeContainerの要素と同じ型だからです。

これらの要求により、allItemsMatch(_:_:)関数は、例え二つのcontainerが別の型であっても、(二つともContainerプロトコルに準拠していて、要素の型が同じで、Equatableプロトコルに準拠していれば)二つのcontainerを比較できます。

allItemsMatch(_:_:)関数は最初に、二つのconatinerが含む要素の数が同じかをチェックします。要素数が違う場合、その時点でマッチしませんので、関数はfalseを返します。

このチェックの後、イテレートで全ての要素を一つずつ比較していきます。もし値が等しくない要素があればその時点で二つのcontainerはマッチしませんので、関数はfalseを返します。

ループが最後まで終わりミスマッチが見つからなかった場合、二つのconatinerはマッチしたということで、関数はtrueを返します。

sample6-1

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

extension Array: Container {}

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
 
 
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

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

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

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

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


if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
//All items match.

sample6-1ではStack<String>型のインスタンスを生成しています。そしてpushメソッドにより三つの文字列を格納しています。さらに三つの同じ文字列を持つ配列リテラルにより初期化したArrayインスタンスを生成しています。Stack<String>型とArray<String>型は別の型ですが、両方ともContainerプロトコルに準拠しています。そして両方とも同じ要素を持っています。ですので、二つのcontainerを引数に取りallItemsMatch(_:_:)関数を呼び出すことができます。


Generic Where Clauseを用いたエクステンション

エクステンションの一部としてgeneric where clauseを使用することもできます。以下のサンプルはジェネリック型のStack構造体にisTop(_:)メソッドを追加(エクステンション)しています。

sample7-1

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

この新しいisTop(_:)メソッドははじめにstackに要素があるかをチェックします。次に引数として受け取った要素を、一番上の要素と比較します。もしgeneric where clause無しでisTopメソッドを呼び出すと、問題があります。isTopメソッドの実装の中で==演算子が使用されています。しかしStack構造体は型引数Elementに対してEquatableである制約を要求していませんので、コンパイルエラーが発生します。generic where clauseを使用することで、エクステンションに対して新たな要求を追加できます。stack内の要素がequatableな場合のみエクステンションがisTop(_:)メソッドを追加するために、。

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

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

sample7-1のエクステンション宣言後も、Stack<NotEquatable>型のインスタンスを生成することや、そのインスタンスにEquatableでない要素を追加することはできる。ただ、当たり前だが、isTop(_:)メソッドは呼び出せない。ということ。


generic where clauseをプロトコルヘのエクステンションに使用することもできます。下のサンプルはContainerプロトコルにstartsWith(_:)メソッドを追加するエクステンションの例です。

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

startsWith(_:)メソッドは、はじめにcontainerに少なくとも一つ要素があることをチェックします。次に引数として与えられた要素と、containerの一番目の要素が等しいかをチェックします。startsWith(_:)メソッドは、containerの要素がEquatableプロトコルに準拠した型である限り、Containerプロトコルに準拠したあらゆる型で呼び出せます。

Containerに対してエクステンションを宣言した後も、Containerプロトコルに準拠した型のインスタンスは要素として非Equatableなインスタンスを格納すること自体は可能。

 

 

 

 

 

コメントを残す

メールアドレスが公開されることはありません。