# ジェネリクスとは
ジェネリクスはSwiftの強力な機能の一つです。関数 の章で説明した通り、関数の引数などには具体的な型を指定する必要がありました。しかし、ジェネリクスを使用すると、型を抽象化し、どのような型でも動作するように柔軟で再利用可能なコードを書くことができます。ジェネリクスは、Swiftの標準ライブラリの多くで使われています。例えば、Arary型やDictionary型もジェネリクスを使ったコレクションです。
Array<Element>
型は、Elementの型を配列を生成する時に変えることができます。配列は様々な型の要素が入る可能性があるので、その用途に応じた型を設定する必要があります。そのため、Swiftはジェネリクスを使い、柔軟に型を変更できるようになっています。
let stringArray: Array<String> = ["a", "b", "c"]
let intArray: Array<Int> = [1,2,3]
let dictionaryArray: Array<[String: String]> = [["key": "value"], ["key2": "value2"]]
上記のように、Element
に具体的な型を指定して、様々な要素の配列を生成することができます。
# ジェネリック型 | Generic Types
ジェネリクスで定義された型を ジェネリック型 (Generic Types) といいます。構造体、クラス、列挙型でジェネリクスを使用することができます。ジェネリック型にすることで、より汎用的な型を作ることができます。
# 定義方法
ジェネリック型の定義方法は、構造体やクラス名のあとに、<型引数>
のフォーマットで書きます。この 型引数
が抽象化された型で、実際に使用するときに具体的な型を指定すると置き換えられます。
struct 構造体<型引数> {}
class クラス<型引数> {}
enum 列挙型<型引数> {}
次の例では、構造体をジェネリック型にして 型引数
を定義しています。型引数名は T
にしていますが、任意で決めることができます。
// <T> で型引数を定義
struct Data<T> {
// 型引数を prop プロパティの型として指定
var prop: T
}
TIP
型引数名は慣例として、T
、U
、V
などが使用されることが多いですが、具体的な名前にして Upper Camel Case で書くことが推奨されています。なるべく具体的な命名にして、どうしても決められない場合は慣例に従うのがいいでしょう。
推奨:
struct Stack<Element> { ... }
func write<Target: OutputStream>(to target: inout Target)
func swap<T>(_ a: inout T, _ b: inout T)
非推奨:
struct Stack<T> { ... } // Tを避けて、具体的な名前にすべき
func write<target: OutputStream>(to target: inout target) // Upper Camel Caseにすべき
func swap<Thing>(_ a: inout Thing, _ b: inout Thing) // Thingという抽象的な名前なら、T で簡略化すべき
# 型引数に型を指定する
ジェネリック型をインスタンス化するときに具体的な型を指定することができます。
型引数は、<型名>()
で指定できます。
構造体<型名>()
クラス<型名>()
列挙型<型名>()
次の例では、型引数に String型
と Int型
を指定しています。
struct Data<DataType> {
var prop: DataType
}
// インスタンス化させるタイミングで型引数を指定する
let data1 = Data<String>(prop: "data")
let data2 = Data<Int>(prop: 100)
data1.prop // data
data2.prop // 100
型引数の型とイニシャライザの引数は同じ型にする必要があります。異なる型の引数を渡すと、コンパイラはエラーを出力します。
struct Data<DataType> {
var prop: DataType
}
// String型を指定してるのに、数値型を渡している
let data1 = Data<String>(prop: 100)
/* 実行結果 */
// error: cannot convert value of type 'Int' to expected argument type 'String'
// let data1 = Data<String>(prop: 100)
明示的に型を指定することもできますが、Swiftは型推論によって、イニシャライザの引数の値から型を推論できます。そのため、型引数を省略して次のように書くこともできます。
struct Data<DataType> {
var prop: DataType
}
// <String>を省略する
let data1 = Data(prop: "data")
# 型の制約
ジェネリック型では、型引数に対して型の制約をかけることができます。スーパークラスやプロトコルを指定して、制約をかけます。
型の制約をかけるには、<型引数: スーパークラス or プロトコル>
のフォーマットで指定します。
構造体<型引数: スーパークラス or プロトコル> {}
クラス<型引数: スーパークラス or プロトコル> {}
列挙型<型引数: スーパークラス or プロトコル> {}
次の例では、DataType
に対してプロトコルを指定し、プロトコルに準拠させた型でしか許容しないように制約をかけています。
protocol ItemProtocol {
var item: String { get }
}
// 型引数を ItemProtocol に準拠させる
struct Data<DataType: ItemProtocol> {
var prop: DataType
}
struct Item: ItemProtocol {
var item: String
}
// ItemProtocol に準拠した値を渡す
let data1 = Data(prop: Item(item: "book"))
# エクステンション
ジェネリック型は、extension
キーワードで拡張させることもできます。拡張させるときは、型引数の記述は必要ありません。
struct Data<DataType> {
var props: [DataType]
}
// Dataを拡張させる。型引数名を書かなくても DataType を使える
extension Data {
func first() -> DataType? {
props.first
}
}
let data1 = Data(props: [1,2,3])
data1.first() // Optional(1)
# where
where
キーワードを使うと、拡張させる際の制約をかけることができます。
次の例では、型引数がString型のときだけ、エクステンションが有効になっています。
struct Data<DataType> {
var props: [DataType]
}
// whereを使って、制約をかける。
// DataTypeがString型のみエクステンションを有効にする
extension Data where DataType == String {
func first() -> DataType? {
props.first
}
}
// String型の配列を渡す
let data1 = Data(props: ["a", "b", "c"])
data1.first() // Optional(a)
String型以外を指定すると、エクステンションは有効になりません。
// Int型を渡す
let data1 = Data(props: [1,2,3])
data1.first()
/* 実行結果 */
// error: referencing instance method 'first()' on 'Data' requires the types 'Int' and 'String' be equivalent
// data1.first()
// note: where 'DataType' = 'Int'
// extension Data where DataType == String {
また、型制約をかけることによって、DataType
は String型として振舞うので、String型のメソッドを使うことができます。
次の例では、split
struct Data<DataType> {
var prop: DataType
}
extension Data where DataType == String {
func splitData() -> [String.SubSequence] {
// split メソッドを使うことができる
prop.split(separator: " ")
}
}
let data1 = Data(prop: "book chair desk")
data1.splitData() // ["book", "chair", "desk"]
上記のように型を限定させることによって、そのプロパティやメソッドを使うのが型を制約させる目的の一つになります。
# where とプロトコルの準拠
where
キーワードを使って、プロトコルへの準拠に制約をかけることもできます。
extension ジェネリック型: プロトコル where 制約 {}
次の例では、DataType
がString型のときだけ ItemProtocol
を準拠しています。
protocol ItemProtocol {
func printItem()
}
struct Data<DataType> {
var prop: DataType
}
// String型のときだけItemProtocolを準拠する
extension Data: ItemProtocol where DataType == String {
func printItem() {
print(self.prop)
}
}
let data1 = Data(prop: "book")
data1.printItem() // book
Int型のときは、ItemProtocol
を準拠していないので printItem
を実行することはできません。
let data2 = Data(prop: 100)
data2.printItem()
/* 実行結果 */
// error: referencing instance method 'printItem()' on 'Data' requires the types 'Int' and 'String' be equivalent
// data2.printItem()
// note: where 'DataType' = 'Int'
// extension Data: ItemProtocol where DataType == String {
# ジェネリック関数 | Generic Functions
ジェネリクスを使った関数を ジェネリック関数 といいます。ジェネリック関数は、引数に型引数を持たせて引数の型を抽象化させることができます。
# 定義方法
ジェネリック関数の定義方法は、関数名のあとに、<型引数>
を書きます。
func 関数<型引数>(引数) -> 戻り値の型 {}
次の例では、受け取った引数をもとにタプルを返しています。型引数を使って任意の型を返しています。
func tuple<T>(_ x: T, _ y: T) -> (T, T) {
return (x, y)
}
# 型引数に型を指定する
ジェネリック関数を呼び出すときに、型引数に具体的な型を指定することができます。
型引数は、関数<型名>()
のフォーマットで指定できます。
関数<型名>()
次の例では、型引数に Int型
と String型
を指定しています。引数には型と同じ値を渡します。
func tuple<T>(_ x: T, _ y: T) -> (T, T) {
return (x, y)
}
// <型> で型引数を指定する
let tutple1 = tuple<Int>(1,2)
let tutple2 = tuple<String>("a", "b")
型引数と引数の値の整合性を保つため、異なる型の引数を渡すとコンパイラはエラーを出力します。
func tuple<T>(_ x: T, _ y: T) -> (T, T) {
return (x, y)
}
// 型引数とは異なる型の引数を渡す
let tutple1 = tuple<String>(1,2)
/* 実行結果 */
// error: cannot explicitly specialize a generic function
// let tutple1 = tuple<String>(1,2)
// note: while parsing this '<' as a type parameter bracket
// let tutple1 = tuple<String>(1,2)
また、ジェネリック型と同様に明示的に型を指定しなくてもSwiftは型推論によって、引数の値から型を推論できます。そのため、型引数を省略して次のように書くこともできます。
func tuple<T>(_ x: T, _ y: T) -> (T, T) {
return (x, y)
}
// 型引数 <String> を省略して書く
let tutple1 = tuple(1,2)
# 型の制約
ジェネリック型と同様に、ジェネリック関数も型引数に制約をかけることができます。ジェネリック関数では、スーパークラスやプロトコル、関連型を型の制約として使うことができます。
スーパークラスやプロトコルの型の制約をかけるには、<型引数: スーパークラス or プロトコル>
のフォーマットで指定します。
func 関数<型引数: スーパークラス or プロトコル>(引数) {}
次の例では、型引数の Item
に対してプロトコルを指定し、プロトコルに準拠させた型でしか許容しないように制約をかけています。
protocol ItemProtocol {
var quantity: Int { get }
}
// ItemProtocolに準拠させる
func doubleQuantity<Item: ItemProtocol>(_ item: Item) -> Int {
return item.quantity * 2
}
struct Item: ItemProtocol {
var quantity: Int
}
// ItemProtocol に準拠した Item を渡す
let qty = doubleQuantity(Item(quantity: 2))
qty // 4
# where
where
キーワードを使うとさらに詳細な制約をかけることができます。
定義方法は、where
キーワードの後に、スーパークラス、プロトコルの準拠を書きます。
func 関数<型引数: プロトコル>(引数) -> 戻り値の型
where プロトコルの関連型: プロトコル or スーパークラス
次の例では、型引数に Collection
プロトコルを準拠させるのと同時に、where キーワードで T.Element
が BinaryInteger
プロトコルに準拠した型を要求しています。
func filterArray<T: Collection>(_ arg: T) -> [T.Element] where T.Element: BinaryInteger {
arg.filter { $0 > 5 }
}
// CollectionとBinaryIntegerを満たすため、Array<Int>型の配列を引数として渡す
let array = [1,2,3,4,5,6,7,8,9,10]
let filtered = filterArray(array)
filtered //[6, 7, 8, 9, 10]
BinaryInteger
は数値型の基本となるプロトコルなので、数値型以外を指定すると制約を満たしていないとみなされて、コンパイラはエラーを出力します。
func filterArray<T: Collection>(_ arg: T) -> [T.Element] where T.Element: BinaryInteger {
arg.filter { $0 > 5 }
}
// Array<String>型の配列を渡す
let array = ["a", "b", "c"]
let filtered = filterArray(array)
/* 実行結果 */
// error: global function 'filterArray' requires that 'String' conform to 'BinaryInteger'
// let filtered = filterArray(array)
// note: where 'T.Element' = 'String'
// func filterArray<T: Collection>(_ arg: T) -> [T.Element] where T.Element: BinaryInteger {
# 型同士の一致
また、==
で型同士の一致を制約にすることができます。
func 関数<型引数: プロトコル>(引数) -> 戻り値の型
where プロトコルの関連型 == プロトコル or スーパークラス
次の例では、型引数に Collection
プロトコルを準拠させるのと同時に、where キーワードで T.Element
が String型
に一致した型を要求しています。
// Collectionに準拠しているかつ、T.ElementがString型の型を要求している
func join<T: Collection>(_ arg: T) -> String where T.Element == String {
arg.reduce("", +)
}
// CollectionとString型を満たすため、Array<String>型の配列を引数として渡す
let array = ["a", "b", "c"]
let result = join(array)
result // abc
Array<Int>型
を引数に渡すと、ElementがString型ではないため、コンパイラはエラーを出力します。
func join<T: Collection>(_ arg: T) -> String where T.Element == String {
arg.reduce("", +)
}
// Array<Int>型の配列を引数として渡す
let array = [1,2,3]
let result = join(array)
/* 実行結果 */
// error: cannot convert value of type '[Int]' to expected argument type '[String]'
// let result = join(array)
// note: arguments to generic parameter 'Element' ('Int' and 'String') are expected to be equal
// let result = join(array)
このように型の制約をかけることによって、より安全にコードを実行することができます。
ジェネリクスを使うことで、重複したコードを統一し、汎用的で再利用可能なコードを書くことができます。また、型の制約を設けることでコードの安全性を保つことができます。
しかしながら、汎用的にし過ぎると機能過多になったり安全性が低下する恐れがあります。また、型の制約を強め過ぎると使い勝手が悪くなることもあります。
この両者のバランスと特性を意識しながら、ジェネリクスをうまく活用するのが重要になるでしょう。