はじめに
SwiftのドキュメントやOSS見るとGenericsがよく使われてますね。Genericsが使われた関数や型を理解したり柔軟で汎用的なコーディングをしていくためにもGenericsは学んでいくべきだと思います。今回はGenericsについて学んでいきたいと思います。
Generics
Generics in Swift, Part 2の***Why generics?***では、Genericsを使う理由として以下の3つが挙げられています。
- Type safety
- Less code duplication
- Library flexibility
Genericsを用いることで指定した要件、もしくは任意の様々な型を扱うことができるので、柔軟で再利用可能な型や関数を定義することができます。
Array
やDictionary
もGenericsなCollectionです。(詳しくはSwiftらしいコーディングを学ぶ 「CollectionType」参照)例えば、Array
に格納する要素はInt
やString
や制限なく様々な型で生成することができますね。普段当たり前のように利用しているこれらのクラスやSwiftの標準ライブラリはGenericsが利用され、柔軟に型を指定することができます。
GenericsはGeneric FunctionsとGeneric Typesに大別することができます。まずは、Generic Typesからみていきたいと思います。
Generic Types
クラスや構造体、列挙体でGenericsを使用して、様々な型で動作する汎用的な型を定義することができます。この時のGenericsを使用して定義された型をGeneric Types(ジェネリック型)と呼びます。
構造体・クラス
一般的にクラスや構造体にGenericsを用いる際は次のように記述します。
struct StructName<TypeParameter> {
//struct statements
}
var struct = StructName<TypeParameter>()
class ClassName<TypeParameter> {
//Class statements
}
var class = ClassName<TypeParameter>()
クラスも構造体も使い方は同じですね。TypeParameter
の部分に型を明記します。慣例としてT
やU
が使われることが多いですが、実際に利用する場合は分かりやすい名前で定義し、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
の部分です。ItemType
をInt
型に宣言することでItemType
を構造体の中でInt
型として利用することができます。
var intStack = IntStack()
intStack.append(2)
intStack.append(3)
intStack.append(4)
intStack.count//3
intStack[2]//4
Generic types
associatedtype
はGeneric 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,...)
Int
やString
などの実際の型の代わりに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
は参照渡しなのでsomeInt
とanotherInt
が入れ替わっていますね。
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にInt
やString
を引数に渡してみます。
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
の部分で引数a
とb
はT
の型で定義してあるので、異なる型を引数に入れるとエラーになっていますね。複数の型を定義する場合はfunc functionName<T, U>(a: t, b: U)
のように定義します。
T
のようなType Parameters
は引数だけではなく、返り値、関数内の定義としても利用できます。上の例では、T
がInt
やString
に置き換わっているんですね。
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が通りましたね。型に制約をつけたことでT
がComparable
に準拠するので、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はContainer
Protocolに準拠していなければならないということですね。 -
where C1.ItemType == C2.ItemType
この部分はC1
のItemType
型はC2
のItemType
と同じ型でなければならないということです。 -
C1.ItemType: Equatable
C1
のItemType
はEquatable
プロトコルに準拠してなくてはいけないということです。必然的にC2
のItemType
もEquatable
に準拠することになりますね。
Whereを用いて拡張してみる
実際にWhereを用いて実践してみたいと思います。CollectionType
を拡張して一番大きいものを返すlargestValue ()
を実装してみましょう。
Collection Type
はSwiftらしいコーディングを学ぶ 「CollectionType」に詳しく記述しましたが、Array
やDictionary
など集合体を扱うクラスが準拠している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
ですが、associatedtype
でGenerator
型の定義があります。generatorは新しい要素を返す処理のことで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()
によって要素の中の一番大きい値が返ってきてます。
参考
- The Swift Programming Language (Swift 2.2): Generics
- 【Swift】ジェネリクスが使われているメソッドを理解する
- Codable Tech Blog / Swift ジェネリックス(Generics)
- Objective-Cより柔軟かつ安全なプログラミングを可能にするSwiftの「ジェネリクス」
- Swiftのジェネリックスについてのメモ
- Swift - Generics
- GENERIC PROTOCOLS & THEIR SHORTCOMINGS
- Generics in Swift, Part 2
- Swift Tutorial: Introduction to Generics