Swiftのジェネリクスの使い方
ジェネリクス
型をパラメーターとして扱う方法。型を抽象化できて便利なコードを書けるようになる。
Swiftの標準ライブラリーの多くがジェネリクスを使っている。
この記事ではジェネリクスを使っていないコードとジェネリクスを使ったコードを比較して、ジェネリクスの有効性から使い方までみていきます。
ジェネリクスを使わないイケてないコード
Int型の引数2つを入れ替える関数を作ってみます。
func swapTwoInts(inout a: Int, inout _ b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
このswapTwoInts(_:_)
関数を実行してみます。
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someIntは\(someInt)になり、anotherIntは\(anotherInt)になった")
//-> 2つの値が入れ替わった
swapTwoInts
は引数をInt型しかとらない。
他の型も入れ替えたい場合はそれぞれ関数をつくらないといけません。
//Double型を入れ替える関数
func swapTwoDouble(inout a : Double, inout _ b : Double) {
let temporaryA = a
a = b
b = temporaryA
}
//String型を入れ替える関数
func swapTwoString(inout a: String, inout _ b: String){
let temporaryA = a
a = b
b = temporaryA
}
対応する型が増える度に関数を増やしていくのはいけてない。
関数の処理はみんなコピペで一緒です。
(T_T)
なんとかスマートに書けないの?
ということでジェネリクスの登場です!
ジェネリクスで関数を作る
//ジェネリクスを使った関数
func swapTwoValues<T>(inout a: T, inout _ b: T){
let temporaryA = a
a = b
b = temporaryA
}
ジェネリクスは<T>
。
<
と>
の間に実際の型ではなくプレースホルダーの型の名前を入れる。
このTが実際に実行される際にDouble型、Int型、String型に変わる。
swapTwoValues
関数はTというどんな型でもいいけれど、引数aとbはそのTという同じ型を持っている関数であるという意味を持っている。
<
と>
で挟むと、Swiftの文法的に型のプレースホルダーとなって、具体的な型ではなく、何かの型を表すようになる
swapTwoValues関数を実行してみます。
//Int型を引数にする
var aInt = 3
var bInt = 107
swapTwoValues(&aInt, &bInt)
print("\(aInt):\(bInt)")
//String型を引数にする
var aString = "こんにちは"
var bString = "こんばんわ"
swapTwoValues(&aString, &bString)
print("\(aString): \(bString)")
Int型でもString型でも変数を入れ替えることができました!
型パラメーターの名前
型パラメーターの名前は読みやすいような名前つければよい。言語的な意味は特にない。何でもいい。
例えば、Dictionary
型では型パラメーターの名前はDictionary<Key, Value>
とKey
, Value
になっている。
Array
型だとArray<Element>
よく、T,U,Vなどがよく使われる
ジェネリクスの型
Swiftの配列や辞書と同じような、Stackという名前のコレクション型を自分で作ってみる。
pushメソッドで要素を追加して、popメソッドで要素を追加したものから削除し取り出しできるようにする。
要素をスタックしていって、pushメソッドで上に追加する。popメソッドで追加したものから取り出すようにします。
ジェネリクスと比較するために、具体的な型Intで作ってみる。
struct IntStack {
var items = [Int]()
mutating func push(item: Int){
items.append(item)
}
mutating func pop() -> Int{
return items.removeLast()
}
}
このIntStackのインスタンスを作成してpopメソッドとpushメソッドを実行してみます。
var intStack = IntStack()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack.items)//->[1, 2, 3]
intStack.pop()
print(intStack.items)//->[1, 2]
ジェネリクスで型を抽象化するとこんな感じになる
struct Stack<Element>{
var items = [Element]()
mutating func push(item: Element){
items.append(item)
}
mutating func pop() -> Element{
return items.removeLast()
}
}
型パラメーターのElement
が具体的なInt型の替わりに使われている。
実行する際にはElement型
が実際の型(Int型
やDoubl型
、String型
など)に変換される
StackをString型で実行、インスタンスを作成する
var stackOfStrings = Stack<String>()
stackOfStrings.push("ひとつ")
stackOfStrings.push("ふたつ")
stackOfStrings.push("みっつ")
print(stackOfStrings.items)//->["ひとつ", "ふたつ", "みっつ"]
stackOfStrings.pop()
print(stackOfStrings.items)//->["ひとつ", "ふたつ"]
ジェネリクスタイプの拡張
ジェネリクスタイプを拡張するときは型パラメーターのリストはもう一度定義しなくてよい。
もとの型パラメーターが拡張する処理の中に書くことができる。
型パラメーターの名前はもとのものを使う。
先ほどのStack型を拡張する
//スタックされた一番上の値を取り出せるtopItemを作成。
extension Stack{
var topItem: Element?{
return items.isEmpty ? nil : items[items.count - 1]
}
}
実行してみます
if let topItem = stackOfStrings.topItem{
print("一番うえにあるものは:\(topItem)です")//->一番うえにあるものは:ふたつです
}
型に制限をかける
Stack型はどんな型でも対応できたけど、制限をかけたいときもある。
DictionaryのキーはHashableプロトコルを採用したものに限定されていて重複がないようになっている。
型パラメーターに [型パラメーター]:classNameまたは[型パラメーター]:プロトコル
でそのクラスを継承している型かプロトコルを採用している型かに制限をかけることができる
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U){
}
ジェネリクスではない関数をまずつくって、次に型に制限をかける方法を見ていく
配列の中に引数で渡されたものがあったら、そのインデックス番号を返す関数を作る
func findStringIndex(array: [String], _ valueToFind: String) -> Int? {
for (index, value) in array.enumerate(){
if value == valueToFind{
return index
}
}
return nil
}
今定義したfindStringIndex関数
を実行してみます
let animals = ["cat", "dog", "fish", "llama", "tiger"]
if let foundIndex = findStringIndex(animals, "dog"){
print("dogは\(foundIndex)インデックス番号です")//->dogは1インデックス番号です
}
ジェネリクスで書いてみる
func findIndex<T: Equatable>(array: [T], _ valueToFind: T) -> Int?{
for (index, value) in array.enumerate(){
if value == valueToFind{
return index
}
}
return nil
}
型に制限をかけないとvalue == valueToFind
でエラーがでる。
Swiftの型はすべてが全てが==
で評価できるわけではない。
T型はあらゆる型を表しているのでそのままではコンパイルが通らない。
ある値を==
で評価したいならEquatableプロトコル
を採用する必要がある。
<T: Equatable>
と表現することで型パラメーターにEquatableプロトコル
を採用している型のみに制限することができる。
Int型でfoundIndex関数を実行してみる
let intArray = [1, 2, 333, 444, 555]
if let foundIndex = findIndex(intArray, 333){
print("インデックス番号は\(foundIndex)です")//->インデックス番号は2です
}
Associated Type
プロトコルを定義するときにアソシエイトタイプを定義すると便利になることがある。
アソシエイトタイプはプロトコルで使う型のプレースホルダー(や別名)を作ることができる。
プロトコルではジェネリクスタイプを使うことができないので、"なんでもよい型"を表すのはアソシエイトタイプを使う。
protocol Container {
associatedtype ItemType
mutating func append(item: ItemType)
var count: Int {get}
subscript(i: Int) -> ItemType{get}
}
Containerプロダクトを具体的な型、Int型で採用してみる
struct IntStack2: Container {
//IntStack独自の実装
var items = [Int]()
mutating func push(item: Int){
items.append(item)
}
mutating func pop() -> Int{
return items.removeLast()
}
// Containerプロトコルを実装
typealias ItemType = Int//Containerのアソシエイトタイプを具体化する(ここの行はなくてもSwiftが推論してくれるので動く)
mutating func append(item: Int) {
self.push(item)
}
var count: Int{
return items.count
}
subscript(i: Int) -> Int{
return items[i]
}
}
ジェネリクスタイプを使って、Containerプロトコルの抽象化されていたアソシエイトタイプを抽象化したままで採用してみる
struct Stack2<Element>: Container {
//Stack2<Element>の独自実装
var items = [Element]()
mutating func push(item: Element){
items.append(item)
}
mutating func pop() -> Element{
return items.removeLast()
}
// Containerプロトコルに準拠する
mutating func append(item: Element) {
self.push(item)
}
var count: Int{
return items.count
}
subscript(i: Int) -> Element{
return items[i]
}
}
Where文の書き方
Where文を書くことでアソシエイトタイプに対して条件を設定することができる。(あるプロトコルを採用しているかどうか、ある型とアソシエイトタイプが同じでなければいけないとか)
例としてallItemsMatch関数
を作る。
2つのContainerのインスタンスで同じ要素があるかどうかをチェックする。
同じものがあったらtrueを返してなければfalseを返す
func allItemsMatch<C1: Container, C2: Container where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>(someContainer: C1, _ anotherContainer: C2) -> Bool{
// 2つのコンテナの要素数が同じかどうかをチェックする
if someContainer.count != anotherContainer.count{
return false
}
//それぞれの要素のペアが同じかどうかをチェックする
for i in 0..<someContainer.count{
if someContainer[i] != anotherContainer[i]{
return false
}
}
//全部同じなのでtrueを返す
return true
}
C1はContainerプロトコルに準拠する。(C1: Containerで表現)
C2もContainerプロトコルに準拠する。(C2: Containerで表現)
C1のItemTypeとC2のItemTypeは同じ型でないといけない(C1.ItemType == C2.ItemTypeで表現)
C1のItemTypeはEquatableプロトコルを採用する(C1.ItemType: Equatableで表現)
引数の意味
someContainerはC1型になる。
anotherContainerはC2型になる。
someContainerとanotherContainerは同じ型になる。
someContainerは!=オペレーターを使うことができる。型が同じなのでanotherContainerも!=オペレーターを使うことができる。
標準の配列,Array型をにContainerプロトコルを準拠させる。
extension Array :Container {}
allItemsMatch関数
を使ってみる。
独自に実装したStack2<Element>
の要素と標準ライブラリArray型
の要素が同じかどうかをチェックする。
var stackOfStrings2 = Stack2<String>()
stackOfStrings2.push("(・∀・)")
stackOfStrings2.push("(^^)")
stackOfStrings2.push("(T_T)")
var arrayOfStrings = ["(・∀・)", "(^^)", "(T_T)"]
//どちらもContainerプロトコルに準拠したインスタンスを引数にしている。
if allItemsMatch(stackOfStrings2, arrayOfStrings){
print("全部あってる")//->全部あってる
}else{
print("違う要素あり")
}
Stack2<Element>
型のインスタンスstackOfStrings2
とArray型のインスタンスarrayOfStrings
はどちらもContainer
プロトコルを採用させたので、allItemsMatch関数
で比較ができる。
全部あっているので、全部あってる
が出力されます。
まとめ
ジェネリクスの使い方を見てみました。
使いこなしてぜひ柔軟なコードを書いていきましょう!