iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

Swift(5.3) の Generic を学ぼう

今回は、Swift における Generic Programming について理解を含めようということで、Swift の公式ドキュメントを眺めがらメモした内容をまとめていきます。

docs.swift.org

目次

  • Why generics?
  • Generic Functions
  • Type Parameters
  • Generic Type
  • Type Constraints
  • Associated Types
  • Generic Where
  • Generic Subscripts

Why Generics

Generic Code を使用すると、定義した用件に応じて、任意の型で機能する柔軟で再利用可能な関数と型を記述できます。そのため、重複を避け、その意図を明確で抽象化された方法で表現するコードを書くことができます。

  • タイプセーフ
  • コードの重複
  • ライブラリの柔軟性

Generics は Swift の最も強力な機能の1つであり、Swift standard library の多くは generic code で構築されています。たとえば、Swift の Array と Dictionary タイプはどちらも Generic collections です。Int 値を保持する配列、String 値を保持する配列、または Swift で作成できる他のタイプの配列を作成できます。同様に、指定したタイプの値を格納する Dictionary を作成できます。そのタイプに制限はありません。

Generic Functions

これは、swapTwoInts(_:_:) と呼ばれる標準の nongeneric 関数で、2つの Int 値を交換します。

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

これを Generic function にして汎用的にメソッドが使えるようにしてみましょう。

※ これらの引数の値の型は両方同じである必要があります。Swift は type-safe 言語なので、相互に違う方の変数が変換されることを許容しません。

改善がこちら。Generic バージョンの関数は、実際の型名(Int, Stringなど)の代わりに、Placeholder type name を使用します(この場合は T)。T の代わりに使用する実際のタイプは、swapTwoValue(_:_:) 関数が呼び出されるたびに決定されます。

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

Type Parameters

swapTwoValues(_:_:) の例では、Placeholder type の T が Type Parameter の例になります。

Type Prameter を使用すると、それを使用して関数のパラメータの型を定義できたり、関数の戻り値、関数の本体内の型の注釈として使用できます。いずれの場合も、関数が呼び出されるたびに、Type Prameter は、実際の型に置き換えられます。

<> 内をコンマで区切って、複数の Type Prameter を指定することもできます。(例: <T: Hoge1, U: Hoge2>)

Type Parameters のネーミング

ほとんどの場合、Type Parameters には、Dictionary<Key, Value>KeyValueArray<Element>Element など、分かりやすい名前がついています。これは、Type Parameter と、それが使用される Generic Type または Generic functions との関係を分かりやすくします。ただし、それらの間に意味のある関係が存在しない場合などは、慣例的に TUV などの1文字を使用して名前をつけるのが一般的です。

※ Type Parameters には TMyTypeParameter のように Upper camel case を付けて、値ではなく型のプレースホルダーであることを示す必要があります。

Generic Type

Generic functions に加えて、Swift では独自の Generic Types を定義することができます。これらは、Array や Dictionary のように、任意のタイプで機能できる custom classstructenum を定義することができます。

下記は、スタックとしての機能を持った nongeneric の Struct です。pushpop といったメソッドは、Structure の items 配列を変更(mutate)する必要があるため、mutating としてマークされています。

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

上記の IntStack は Int 値でのみ使用することができるので、これを汎用的に使えるようするため、下記のように Generic Type として定義します。

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

この Generic Type としての定義により、Array や Dictionary と同様に、Swift で任意の有効な型のスタックを作成できます。

また、Generic Type として定義した上記の Structure は下記のようにインスタンス化することができます。

var stackOfStrings = Stack<String>()

Generic Type の Extension

Generic Type を Extension する場合は、元のオブジェクトで定義した Type Parameter を使用することが可能です。

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

Generic Type の extension には、WHERE 句を使用して、関数やプロパティを取得するために拡張型のインスタンスが満たさなければならない要件を含めることもできます。(こちらについては記事の後半で触れます。)

docs.swift.org

Type Constrains

Generic function および Generic type で使用できる型に特定の Type constrains(型制約) を適用すると便利な場合があります。Type Constraints は、Type Prameter が特定のクラスから継承するか、特定のプロトコルまたはプロトコル構成に準拠する必要があることなどを指定することができます。

例: Swift's Dictionary の Key は hashable である必要がある。つまり、Key を一意に表現できるようにするために、Dictionary には、特定の Key の値がすでに含まれているかどうかを確認できるように、Keyが hashable である必要があります。

docs.swift.org

この要件は、Dictionary の Key Type に対する Type Constraints によって適用されています。これは、Key Type が Swift Standard Library で定義されている、Hashable プロトコルに準拠する必要があることを指定します。Swift の全ての basic types (String, Int, Double, Boolなど) は、デフォルトで hashable です。

developer.apple.com

Type Constrains の構文

Type Parameter の一部として、コロンで区切られた Type Parameter の名前の後に単一のクラスまたはプロトコル制約を配置することにより、型の制約を記述することができます。

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

Associated Types

プロトコルを定義する時に、プロトコルの定義の一部として Associate Types を宣言すると便利な場合があります。これらの Associated Types に使用される型は、プロトコルが採用されるまで指定されません。

Associated Types in Action

下記が、Protocol に Associated Type を設けた場合の例になります。

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

Container プロトコルに準拠するように適合された、Generic Type の例が下記になります。

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

typealias Item = Int により、Item の抽象型を Int の具象型に変換します。また、Swift には型推論があるので、Item の具体的な値を宣言する必要はなく、それぞれのメソッドにプロパティに独自の型を定義することでそれらが解決します。

Generic Type を 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]
    }
}

また、今回定義した Container での複数の機能は、Swift の Array ではすでに存在するプロパティや関数が含まれているので、下記のように Array を拡張して Container プロトコルに準拠させるだけで、Swift が Array の Element を Associated Type として推測するので、空の状態で、Container Type として Array を扱えるようになります。

extension Array: Container {}

Associated Type への制約の追加

下記のように、プロトコル内の Associated Type に型制約を追加して、適合型がそれらの制約を満たすことを要求することができます。

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

Associated Type の制約でプロトコルを使用する

プロトコルは、それ自体の要件の一部として表示することができます。下記の例は、Container プロトコルsuffix(_:) メソッドの要件を追加するプロトコルです。

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

このプロトコルでは、Suffix は、Associated Type です。Suffix に2つの制約があります。

  • SuffixableContainer プロトコルに準拠している
  • Item Type は、Container の Item Type と同じである必要があります。

StackSuffixableContainer プロトコルへの適合性を追加した例は下記のようになります。

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)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30

Generic Where

下記のように、where句を定義して、Associated Type が特定のプロトコルに準拠する必要があることや、または、特定の Type Prameter に関連づけられた型が同じである必要があることなどを要求することができます。

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
}

Generic Where句を使用した Extension

extension の一部の機能として、Generic where句を使用することもできます。下記の例では、以前に定義した Stack の Type Prameter である Element が Equatable に準拠している場合にのみ使用できる isTop(_:) 関数を定義しています。これにより、Elemnt が Equatable であることが担保されるので、== operator を問題なく使用することができるようになります。

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

これらの Equtable に準拠していないオブジェクトを Element として使用した場合、isTop(_:) にアクセスした場合にコンパイルエラーが発生します。

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

また、Protocol の Extension としても Generic の Where句を使用できます。

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

上記の例では、Item が特定のプロトコルに準拠している必要があることを表しましたが、Item が特定のタイプである必要がある場合には Generic 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())
// Prints "648.9"

コンテキスト Where句

下記のように Generic Type の Extension のメソッドに Generic Where句を記述することもできます。

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())
// Prints "648.75"
print(numbers.endsWith(37))
// Prints "true"

Generic Where句 を使用した Associated Types

下記のように、Associated Type に generic where句を含めることができます。

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
}

別のプロトコルから継承するプロトコルの場合、プロトコル宣言に generic where句 を含めることにより、継承された Associated Type に制約を追加することができます。

protocol ComparableContainer: Container where Item: Comparable { }

Generic Subscripts

subscript は generic にすることができ、generic 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
    }
}

これにより、indices パラメータ渡される値が整数のシーケンスであることの制約を要求することができます。

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com