Edited at

Swiftらしいコーディングを学ぶ 「Generics」

More than 3 years have passed since last update.


はじめに

SwiftのドキュメントやOSS見るとGenericsがよく使われてますね。Genericsが使われた関数や型を理解したり柔軟で汎用的なコーディングをしていくためにもGenericsは学んでいくべきだと思います。今回はGenericsについて学んでいきたいと思います。


Generics

Generics in Swift, Part 2Why generics?では、Genericsを使う理由として以下の3つが挙げられています。



  • Type safety

  • Less code duplication

  • Library flexibility

Genericsを用いることで指定した要件、もしくは任意の様々な型を扱うことができるので、柔軟で再利用可能な型や関数を定義することができます。

ArrayDictionaryもGenericsなCollectionです。(詳しくはSwiftらしいコーディングを学ぶ 「CollectionType」参照)例えば、Arrayに格納する要素はIntStringや制限なく様々な型で生成することができますね。普段当たり前のように利用しているこれらのクラスやSwiftの標準ライブラリはGenericsが利用され、柔軟に型を指定することができます。

GenericsはGeneric FunctionsGeneric Typesに大別することができます。まずは、Generic Typesからみていきたいと思います。


Generic Types

クラスや構造体、列挙体でGenericsを使用して、様々な型で動作する汎用的な型を定義することができます。この時のGenericsを使用して定義された型をGeneric Types(ジェネリック型)と呼びます。


構造体・クラス

一般的にクラスや構造体にGenericsを用いる際は次のように記述します。


Struct

struct StructName<TypeParameter> {

//struct statements
}

var struct = StructName<TypeParameter>()



Class

class ClassName<TypeParameter> {

//Class statements
}
var class = ClassName<TypeParameter>()

クラスも構造体も使い方は同じですね。TypeParameterの部分に型を明記します。慣例としてTUが使われることが多いですが、実際に利用する場合は分かりやすい名前で定義し、UpperCamelCaseにすることが推奨されています。

実際に要素を格納/取り除く構造体Queueを定義してみます。

struct Queue<Element> {

private var elements = [Element]()

mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.removeAtIndex(0)
}
}

var intQueue = Queue<Int>()
intQueue.enqueue(3)//[3]
intQueue.enqueue(5)//[3, 5]
intQueue.dequeue()//3 elements=>[5]
intQueue.dequeue()//5 elements=>[]
intQueue.dequeue()//nil

var stringQueue = Queue<String>()
stringQueue.enqueue("Generics")//["Generics"]
stringQueue.enqueue("Generic Types")//["Generics", "Generic Types"]

このように、インスタンス化する際に利用する型を明記すれば、様々な型で利用できるんですね。<TypeParameter>の部分で定義したTypeParameterは上記のように構造体の内部で利用できます。このように汎用的な型を指定することによって様々な型に対応できるんですね。


Extending a Generic Type

Genericsを用いたクラス・構造体は拡張することができます。拡張する場合は、<TypeParameter>の記述は必要ないです。

extension Queue {

func peek() -> Element? {
return elements.first
}
}

stringQueue.peek()//"Generics"


Class Inheritance

Genericsを用いたクラスは、継承が可能です。

class BaseClass<T> {}

class InheritClass<T>: Box<T> {}

//TypeParameterの名前は変えられるが、親クラスと揃える必要がある
class InheritClass<U>: Box<U> {}//○
class InheritClass<U>: Box<T> {}//×

class StringClass: BaseClass<String> {}

継承したサブクラスでもGenericsを使用する場合は<TypeParameter>の記述が必要です。型を明示する場合は親クラスに型を明示する必要があります。


Associated Types

protocolにもGenericsを利用することができます。The Swift Programming Language (Swift 2.2)の例がわかりやすいかと思います。

まず、Containerという名前のprotocolを見てみましょう。

protocol Container {

associatedtype ItemType

mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}

protocolでGenericsを利用する際はassociatedtypeでGenericsを宣言します。ここではItemTypeという汎用的な型が定義されてますね。

Containerプロトコルを利用する場合は以下のルールに準拠しなければならないことになります。



  • appendメソッドは引数にItemTypeを利用しなければならない

  • countはInt型を返さなければならない


  • subscriptによって返される型はItemTypeでなければならない


Non-generic type

実際にContainerを利用してみましょう。Containerに準拠したIntStackという構造体を定義してみます。

struct IntStack: Container {

var items = [Int]()

typealias ItemType = Int
mutating func append(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}

var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}

重要な部分はtypealias ItemType = Intの部分です。ItemTypeInt型に宣言することでItemTypeを構造体の中でInt型として利用することができます。

var intStack = IntStack()

intStack.append(2)
intStack.append(3)
intStack.append(4)

intStack.count//3
intStack[2]//4


Generic types

associatedtypeGeneric typesにも利用することができます。

先ほど定義したQueue構造体にContainerを準拠させてみましょう。

struct Queue<Element>: Container {

private var elements = [Element]()

mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.removeAtIndex(0)
}

mutating func append(item: Element) {
enqueue(item)
}
var count: Int {
return elements.count
}
subscript(i: Int) -> Element {
return elements[i]
}
}

このようにItemTypeの部分にGenerics TypeであるElementを利用することができ、様々な型に対応することができます。

var intQueue = Queue<Int>()

intQueue.append(3)//[3]
intQueue.append(5)//[3, 5]
intQueue[1]//5
intQueue.count//2

var stringQueue = Queue<String>()
stringQueue.append("Generics")//["Generics"]
stringQueue.append("Generic Types")//["Generics", "Generic Types"]
stringQueue[0]//"Generics"
stringQueue.count//2


Generic Functions

Generic Functions(ジェネリック関数)は、汎用的に様々な型で利用できる関数のことです。一般的に次のように定義します。

func functionName<PlaceholderTypeName,..>(paramerters:PlaceholderTypeName,...)

IntStringなどの実際の型の代わりにType ParametersとしてPlaceholderTypeNameを定義し、関数名の後に<>で囲んで記述します。

例えば、DictionaryではDictionary<Key, Value>, ArrayではArray<Element>というPlaceholderTypeNameが利用されていますね。

それでは、具体的な例で見ていきましょう。The Swift Programming Language (Swift 2.2)

の引数を入れ替える関数の例がわかりやすいと思います。まずはGenericを使用していない関数を見てみましょう。以下は、Int型の引数を入れ替える関数です。

func swapTwoInts(inout a: Int, inout _ b: Int) {

let temporaryA = a
a = b
b = temporaryA
}

swapTwoIntsは参照渡しなのでsomeIntanotherIntが入れ替わっていますね。

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"

しかし、Int以外で入れ替える処理をしたい場合は型の数だけ関数を定義する必要があり、DRYの原則に反しています。

//他の型で使用するためには、それぞれの型を指定しなければならない

func swapTwoInts(inout a: Int, inout _ b: Int)
func swapTwoDouble(inout a: Double, inout _ b: Double)
func swapTwoString(inout a: String, inout _ b: String)

今度はこの関数をGeneric Functionsを利用して定義してみます。

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

let temporaryA = a
a = b
b = temporaryA
}

このGeneric FunctionsにIntStringを引数に渡してみます。

var someInt = 3

var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
print("someInt is \(someInt), anotherInt is \(anotherInt)")
// someInt is 107, anotherInt is 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
print("someString is \(someString), anotherString is \(anotherString)")
//someString is world, anotherString is hello

var someDouble: Double = 2.5
var someFloat: Float = 3.54
swapTwoValues(&someDouble, &someFloat)
//Cannot convert value of type 'inout Double' to expected argument type 'inout _'

異なる型を引数にしても正常に動作しているのがわかるかと思います。このようにGeneric Functionsを使用することで様々な型に柔軟に対応できるんですね。inout a: T, inout _ b: Tの部分で引数abTの型で定義してあるので、異なる型を引数に入れるとエラーになっていますね。複数の型を定義する場合はfunc functionName<T, U>(a: t, b: U)のように定義します。

TのようなType Parametersは引数だけではなく、返り値、関数内の定義としても利用できます。上の例では、TIntStringに置き換わっているんですね。


Type Constraints

ジェネリック関数に用いるType Parametersには制約を持たせることができます。一般的に次のように記述します。

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {

// function body goes here
}

このようにType Parametersが特定のサブクラスだったりProtocolに準拠しなければいけないという制約をつけることができるんですね。

これは実際の例で見ていくとわかりやすいかと思います。まずは制約を持たせていないジェネリック関数をみていきたいと思います。次のジェネリクス関数は配列の中の一番大きいものを返す関数です。

func findLargestInArray<T>(array: [T]) -> T? {

if array.isEmpty { return nil }

var largestValue = array.first
array.forEach { val in
//Compiler Error
largestValue = val > largestValue ? val : largestValue
}
return largestValue
}

このジェネリクス関数では、以下のエラーが出てしまいます。



Binary operator '>' cannot be applied to operands of type '_' and 'T?'



T型の要素は>の関数を持っているかわからず比較できるかわからないよ!と怒られてしましました。T型は比較することができるComparableに準拠してなくてはならないんですね。次は型に制約をつけてみます。

func findLargestInArray<T: Comparable>(array: [T]) -> T? {

if array.isEmpty { return nil }

var largestValue = array.first
array.forEach { val in
largestValue = val > largestValue ? val : largestValue
}
return largestValue
}

Compilerが通りましたね。型に制約をつけたことでTComparableに準拠するので、T>関数を持っていることが担保されるというわけです。

let array = [1, 2, 3, 5, 7, 10]

let largestValue = findLargestInArray(array)
print(largestValue)//10


Where Clauses

ジェネリクス関数はさらにWhereを用いて制約をつけることができます。先ほどのContainerを使って制約をつけてみましょう。

protocol Container {

associatedtype ItemType

mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}

Whereを用いて次のように制約をつけることができます。

func allItemsMatch<C1: Container, C2: Container

where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, _ anotherContainer: C2) -> Bool {

if someContainer.count != anotherContainer.count {
return false
}

for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
return true
}

型制約の部分ですが、順を追って見てみましょう。


  • C1: Container, C2: Container

    この部分は先ほども確認しましたが、C1の型とC2はContainerProtocolに準拠していなければならないということですね。


  • where C1.ItemType == C2.ItemType

    この部分はC1ItemType型はC2ItemTypeと同じ型でなければならないということです。


  • C1.ItemType: Equatable

    C1ItemTypeEquatableプロトコルに準拠してなくてはいけないということです。必然的にC2ItemTypeEquatableに準拠することになりますね。



Whereを用いて拡張してみる

実際にWhereを用いて実践してみたいと思います。CollectionTypeを拡張して一番大きいものを返すlargestValue ()を実装してみましょう。

Collection TypeSwiftらしいコーディングを学ぶ 「CollectionType」に詳しく記述しましたが、ArrayDictionaryなど集合体を扱うクラスが準拠しているProtocolです。以下、実装したコードです。

extension CollectionType where Self.Generator.Element: Comparable {

func largestValue() -> Generator.Element? {
guard var largestValue = first else { return nil }

for item in self {

if item > largestValue {
largestValue = item
}
}
return largestValue
}
}

where Self.Generator.Element: Comparableの部分で制約をかけていますね。

まず、Collection Typeですが、associatedtypeGenerator型の定義があります。generatorは新しい要素を返す処理のことでGeneratorTypeに準拠しています。


GeneratorType

protocol GeneratorType {

typealias Element
func next() -> Element?
}

GeneratorTypeを見てみるとElementの定義がありましたね。実際にこれがCollection Typeの要素のGeneric型です。つまりCollection Typeの要素の型はSelf.Generator.Elementでアクセスすることができます。もう一度制約の部分を見てみます。

where Self.Generator.Element: Comparable

ここではSelf.Generator.Element、つまりCollection Typeの要素がComparableに準拠している必要があるという制約をかけているんですね。この制約によって要素の大小を比較することができますね。

[1, 2, 3, 4, 5].largestValue()//5

"Generics".characters.largestValue()//s
(0..<1000).largestValue()//999

実際にlargestValue()によって要素の中の一番大きい値が返ってきてます。


参考