2020/1/11 Swift ジェネリック(Generics)

ジェネリックコードを使うと、自分で定義した要請に応じて、あらゆる型に対応する柔軟で再利用可能な関数と型を作り出すことができます。重複を避け、意図を明確に抽象化したコードを記述できます。

ジェネリックはSwiftのもっとも強力な機能の一つであり、Swift標準ライブラリの多くがジェネリックコードにより書かれています。実はもうすでに私たちはジェネリック型を使っています。

例えばSwiftの配列とディクショナリはどちらもジェネリック型のコレクションです。要素がInt型の配列、要素がString型の配列、または他の型の配列も生成することができます。同様に特定の型の要素を持つディクショナリを生成することもできます。要素の型について制限はありません。

ジェネリックが解決する問題

sample1-1は二つのInt型の値を交換する、標準的な非ジェネリック関数swapTwoInts(_:_:)です。

sample1-1

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

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

aとbの値を交換するためにinout引数を使用しています。

swapTwoInts(_:_:)関数はオリジナルのbの値をaに交換し、オリジナルのaの値をbに交換します。二つのInt型の変数の値を交換する時にこの関数を実行します。

swapTwoInts関数は便利ですが、交換できるのはInt型の値のみです。二つのString型の値を交換したい時、二つのDouble型の値を交換したい時、sample1-2のように、swapTwoStrings(_:_:)関数やswapTwoDoubles(_:_:)関数を定義する必要があります。

sample1-2

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、三つの関数の本体は全て同一のコードであることにお気づきでしょうか。違う点は引数の型だけです。

あらゆる型について、同一の型の二つの値を交換する関数を一つ定義することができれば便利で非常に柔軟ですね。ジェネリックコードを使用すればそのような関数を定義することができます。

NOTE

交換するaとbは同じ型である必要があります。aとbの型が違う場合値を交換することはできません。Swiftはタイプセーフな言語です。String型の変数とDouble型の変数の値を交換することは許しません。やってみるとコンパイルエラーとなります。

ジェネリック関数

ジェネリック関数はあらゆる型で動作します。sample2-1は上記のswapTwoInts(_:_:)関数のジェネリックバージョンです。

sample2-1

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

swapTwoValues(_:_:)関数の本体はswapTwoInts(_:_:)関数の本体と同じです。しかしswapTwoValues(_:_:)関数の一行目がswapTwoIntsと微妙に違います。一行目を比較すると、

 

非ジェネリック関数がInt型、String型、Double型など実際の型を使用しているのに対して、ジェネリック関数はプレイスホルダー型名(上記サンプルではT)を使用しています。プレイスホルダー型名は、Tがどんな型か、については何も指定しませんが、Tが何型であれaとbは同じ型であることを表現しています。swapTwoValues関数を呼び出す時にTの型を指定します。

ジェネリック関数と非ジェネリック関数のもう一つの違いは、「ジェネリック関数は関数名(swapTwoValues)の後ろに、アングルブラケットで囲まれたプレイスホルダー型名(<T>)がある」ということです。アングルブラケットで囲むことで、swapTwoValues関数の定義の中で、<T>がプレイスホルダー型名であることを示しています。Tはプレイスホルダーですので、Swiftは実際に定義された型としてのT型を探すことはありません。

swapTwoValues関数はswapTwoInts関数と同様に呼び出すことができますが、swapTwoValues関数二つの値が同じ型である限り、どのような型の値でも引数として渡すことができます。 毎回swapTwoValues関数が呼び出されると、Tとして使用される型は、関数に渡された値の型がTとして推定されます。

型推定についてさらっと触れられている。

sample2-2では、TはそれぞれInt型とString型と推定されます。

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

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

NOTE

swapTwoValues関数はSwift標準ライブラリにあるジェネリック関数であるswap関数を基に定義されています。ですので、swap関数は自動的にアプリ開発に使用できます。あなたのコードでswapTwoValues関数の挙動が必要な時は、すでにあるswap関数を使用した方が良いでしょう。

型引数

上記のswapTwoValues関数のサンプルで、プレイスホルダー型のTは型引数の例です。型引数はプレイスホルダー型を指定・命名し、関数名の直後に記述します。型引数はアングルブラケットで囲みます。(<T>)

一度型引数を指定すれば、関数の引数の型指定(swapTwoValues関数の引数a,bの型指定)に型引数を使用できます。あるいは関数の戻り値の型指定、あるいは関数本体の型注釈に型引数を使用できます。それぞれのケースで関数が実行される度に型引数は実際の型に置き換えられます。(sample2-2で言うと、1度目の関数呼び出しで型引数はInt型に置き換えられ、2度目の呼び出しではString型に置き換えられた。)

アングルブラケット内にカンマで区切って型引数を複数記述することで、複数の型引数を使用することもできます。

型引数の命名

ディクショナリの<Key,Value>や、配列のArray<Element>のように、多くのケースでは、型引数とジェネリック関数の関係性を示すようなわかりやすい名前(型引数名)をつけます。しかし、そこまで意味のある関係性がない場合、慣習としてT,U,Vなど一文字で型引数を表すこともあります。

NOTE

値ではなくプレイスホルダー型であることを示すために、型引数はアッパーキャメルケース(TやMyTypeParameterのような)で記述します。

ジェネリック型

ジェネリック関数に加えて、Swiftでは独自のジェネリック型を定義することもできます。あらゆる型で機能するクラス・構造体・列挙型を定義できます。

このセクションではStackと言う名のジェネリックなコレクション型を定義します。stackは配列に似た、順番づけした値の集合ですが、配列よりもより制限された処理を付け加えたものとして定義します。配列(Array)は新しい要素を配列中のどの場所にも追加できます。しかしStackでは新しい要素はコレクションの最後に追加する方法しかありません。(stackへのpushとして知られる。)要素を削除する時もコレクションの最後から取り除く方法しかありません(stackからのpopとして知られる)。

下の図はstackのpushとpopを図示しています。

../_images/stackPushPop_2x.png

  1. stackに3つの値がある。
  2. 4つ目の値がstackの最上部へpushされる。
  3. stackは今4つの値を持っている。一番新しい値が最上部にある。
  4. 最上部の値がpopされる。
  5. 値をpopした後、stackは3つの値を持っている状態に戻る。

非ジェネリックとしてstackを定義したのがsample3-1です。Int型の値を保持するstackです。

sample3-1

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

構造体IntStackはitemsと名付けたArray<Int>型のプロパティを用意して、stackとして値を格納していきます。

stackへ値をpushするメソッドpush、stackから値をpopするメソッドpopを定義しています。これらのメソッドはmutatingが指定されています。構造体のitemsプロパティを変える必要があるからです。

sample3-1のIntStack型はInt型の値のみ取り扱えます。しかしあらゆる型の値を取り扱えるジェネリックなStack型を定義すればさらに便利です。

以下がジェネリック版のStack構造体です。

sample3-2

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が使用されています。

  • Element型要素が入る空の配列で初期化された、itemsプロパティ
  • Element型の引数を取るpushメソッド
  • Element型の値を返すpopメソッド

Stackはジェネリック型なので、Swiftに置いて適切なあらゆる型を格納するstackとして使用できます。その点はArray型やDictionary型と似ていますね。

ジェネリック型の生成方法

アングルブラケットで型名を囲んで記述することで、新しいStack型インスタンスを生成できます。sample3-3では

Stack<String>();

と記述して、文字列を格納するstackを生成しています。

sample3-3

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

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings

4つの値をstackにpushした後の状況を以下に図示します。../_images/stackPushedFourStrings_2x.png


stackから一つ値をpopして、最上部の値”cuatro”を取り除き、返り値として返します。

sample3-4

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

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

let fromTheTop = stackOfStrings.pop()
// fromTheTop は "cuatro"です。 そして現在 3 つの文字列が格納されています。

../_images/stackPoppedOneString_2x.png

ジェネリック型を拡張する

ジェネリック型を拡張する時は、エクステンションの定義内で型引数のリストを提供しません。代わりに、「オリジナルの型の定義内の型引数のリスト」をエクステンションの本体内で使えます。

下のサンプルはジェネリック型のStack型に読み取り専用コンピューテドプロパティtopItemを付け加えます。topItemはstackの中のトップの要素をポップすることなく返します。

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

topItemプロパティはオプショナル型のElement型(の値)を返します。もしstackが空なら、topItemはnilを返します。stackが空でないなら、topItemはitems配列の最後の要素を返します。

エクステンション内で型引数のリストを定義していないことに注目してください。代わりに、Stack型の既存の型引数名、つまりElement、がコンピューテドプロパティtopItemの型注釈に使用されています。

topItemコンピューテドプロパティはあらゆるStackインスタンスでトップの要素を削除せずに取得するために使うことができます。


型の制約

swapTwoValues(_:_:)関数とStack型はあらゆる型を取り扱えます。しかし、

工事中🏗

 

 


連想型(Associated Types)

プロトコルを定義する時、定義の一部として一つ、あるいは複数の連想型(associated type)を宣言すると便利な場合があります。連想型(associated type)はプロトコルの中で使用する型のプレースホルダー名を与えます。その連想型(associated type)の実際の型は、プロトコルへ準拠する型の中で決定されるので、プロトコル定義時には決まっていません。連想型はassociatedtypeキーワードを使用して宣言します。

実際の連想型

下のサンプルはItemという連想型を宣言したContainerプロトコルの例です。

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

Containerプロトコルは、自身に準拠する型に対して、三つの要求をしています。

  • container(Containerプロトコルに準拠した何らかの型)に対してappend(_:)メソッドを用いて新しい要素を追加することができる。
  • container内の要素数に、Int型を返すcountプロパティによってアクセスできる。
  • container内の要素の値を、Int型のインデックスを指定するサブスクリプトにより取得できる。

このプロトコルでは、containerの要素がどのように格納されるか、要素の型はどの型が許されるか、については特定していません。このプロトコルは、Containerとみなされる(Containerプロトコルに準拠した型とみなされる)ために、提供する必要がある三つの実装すべき機能を特定しています。準拠する型はこれらの三つの機能を実装すれば、その他の機能を追加することは自由です。

Containerプロトコルへ準拠するいかなる型も、自身に格納する要素の型を指定できなければなりません。特に、適切な型の要素のみがcontainerに追加されること、そしてサブスクリプトにより返される要素の型がはっきりしていることが必要です。

これらの要求を定義するために、Containerプロトコルには、特定のcontainerのその型が何型かを知る必要なく、それぞれのcontainerに格納される要素の型を参照する方法が必要です。Containerプロトコルは

  • append(_:)メソッドに渡されるいかなる値もcontainerの要素の型と同じ型であること
  • containerのサブスクリプトから返される値の型が、containerの要素の型と同じ型であること

上記の事柄を指定しなければなりません。

これを成し遂げるため、ContainerプロトコルはItemという名の連想型を

associatedtype Item

と記述することで宣言しています。このプロトコルは、Itemが具体的に何型か、は示していません。その情報は、このプロトコルへ準拠する型の定義内で示されます。にもかかわらず、Itemというalias(別名)は、全てのContainerプロトコルに準拠した型が期待通りの振る舞いをするように、containerの要素の型を参照する方法を提供し、append(_:)メソッドとサブスクリプトで使用する型を定義しています。

以下のサンプルはsample3-1の非ジェネリック型のIntStack型をContainerプロトコルへ準拠させた例です。

sample4-1

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // conformance to the Container protocol
    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プロトコルが求める三つの要求を全て実装しています。三つの要求を満たすため、それぞれのケースでIntStack型の元々あった機能をラップしています。

さらにIntStackは、Containerプロトコルの要求を実装するにあたり、連想型ItemをInt型と指定しています。

typealias Item = Int

という定義により、連想型ItemをInt型に固定しています。

Swiftの型推定のおかげで、実はIntStack型の定義の中でItemをInt型と指定しなくても問題ありません。IntStack型はContainerプロトコルの全ての要求を満たしてContainerプロトコルへ準拠していますから、SwiftはItemの具体的な型を(append(_:)メソッドの引数itemの型と、サブスクリプトの返り値の型を見ることで)推定することができます。実際

typealias Item = Int

の行を消してもエラーは出ません。連想型Itemが何型にすべきかは明確だからです。

sample3-2のジェネリック型のStack型をContainerプロトコルへ準拠させることもできます。(sample4-2)

sample4-2

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]
    }
}

sample4-2では、appendメソッドの引数の型、サブスクリプトの返り値の型としてジェネリック型の型引数Elementが指定されています。Swiftは連想型Itemの具体的な型をElement型(インスタンス生成時に具体的な型が指定される)と推定します。


連想型を特定して既存の型を拡張する

プロトコルへ準拠することで既存の型を拡張する。これにはプロトコルの連想型も含まれます。

SwiftのArray型はすでにappend(_:)メソッドとcountプロパティ、そしてInt型のインデックスによるサブスクリプトによる要素の取得は提供されています。これらの三つの機能はContainerプロトコルの要求と一致します。ということでArray型はContainreプロトコルへの準拠を宣言することだけでContainerプロトコルへ準拠することができます。空のエクステンションによりプロトコルへ準拠します。

extension Array: Container {}

Array型の既存のappend(_:)メソッドとサブスクリプトは、まさに上記のジェネリック型のStack型の場合のように、連想型Itemの具体的な型を推定します。このエクステンションの宣言後、あらゆるArray型をContainerとして(Containerプロトコルへ準拠した型として)使用できます。

つまりContainer型と型注釈されているところに、Arrayインスタンスを代入したりできる、ということ。元々Array型にはContainerプロトコルの要求するものは全て揃っているけど、準拠の宣言が無いと、ArrayインスタンスをContainer型に代入したりはできない、ということ。


連想型に型制約を加える(Adding Constraints to an Associated Type)

プロトコル内の連想型に型制約を設けることができます。準拠する型は、この型制約に従い、associated typeの具体的な型を指定しなければなりません。例えば、下のコードはcontainerの要素に対して、「equatableプロトコルに準拠した型」であることを要求するバージョンのContainerプロトコルの定義例です。

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

このバージョンのContainerプロトコルへ準拠するために、containerの連想型ItemはEquatableプロトコルへ準拠した型を指定する必要があります。


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

プロトコルは自身の要求のなかに出現することができます。例えば、suffix(_:)メソッドの実装の要求を追加して、Containerプロトコルを改良します。(suffix:接尾辞)

 

 

 

 

 

参考

https://docs.swift.org/swift-book/LanguageGuide/Generics.html

コメントを残す

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