ジェネリクスとは
ジェネリクスとは、型をパラメータとして受け取ることで汎用的なプログラムを記述するための機能です。ジェネリクスはSwiftの最も強力な機能の一つであり、標準ライブラリの多くでジェネリクスが使われています。Array型やDictionaryがその一つです。
ジェネリック関数
定義方法
ジェネリック関数やジェネリック型を定義するには、通常の定義に型引数を追加します。型引数は<>で囲み、複数ある場合は,区切りで定義します。
func 関数名<型引数>(引数名: 型引数) -> 戻り値の型 {
呼び出し時の実行文
}
型引数として受け取った型は、引数や戻り値だけでなく、関数内の文でも使用できます。
次のidentity(_:)関数では、引数と戻り値に型引数Tを使用しています。
func identity<T>(_ x: T) -> T {
return x
}
identity(1) // 1
identity("abc") // "abc"
特殊化方法
ジェネリクスを使用して汎用的に定義されたものに対して、具体的な型引数を与えて型を確定させること特殊化(specialization)と言います。
ジェネリック関数を特殊化するには、引数から型推論によって型引数を決定する方法と、戻り値から型推論によって型引数を決定する方法の2つが用意されています。
// 引数からの型推論による特殊化
func someFunction<T>(_ argument: T) -> T {
return argument
}
let int = someFunction(1) // 1
let string = someFunction("a") // "a"
// 戻り値からの型推論による特殊化
func someFunction<T>(_ any: Any) -> T {
return any as! T
}
let a: String = someFunction("abc") // "abc"
let b: Int = someFunction(1) // 1
let c = someFunction("abc") // Tが決定できずコンパイルエラー
型制約
型引数に対して型制約を設けることもできます。型制約の種類は、スーパークラスや準拠するプロトコルに対する制約、連想型のスーパークラスや準拠するプロトコルに対する制約、型どうしの一致を要求する制約の3つに大別できます。
スーパークラスや準拠するプロトコルに対する制約
次の例では、引数に使用される型引数TをEquatableプロトコルに準拠したものに限定しています。これにより、isEqual(::)関数内のT型に対してEquatableプロトコルで定義されている演算子==が利用できます。
func isEqual<T: Equatable>(_ x: T, _ y: T) -> Bool {
return x == y
}
isEqual("abc", "def") // false
連想型のスーパークラスや準拠するプロトコルに対する制約
次の例では、型引数TがCollectionプロトコルに準拠していることを要求しているのに加えて、where節でT.Element型がComparableプロトコルに準拠していることを要求しています。これにより、sorted(_:)関数の引数は比較可能な要素を持ったコレクションに限定されるため、ソート処理を実装できます。
func sorted<T: Collection>(_ argument: T) -> [T.Element]
where T.Element: Comparable {
return argument.sorted()
}
sorted([3, 2, 1]) // [1, 2, 3]
型どうしの一致を要求する制約
型引数と連想型の一致や連想型どうしの一致を要求する型制約を設けるには、where節内で一致すべき型どうしを==演算子で結びます。
func 関数名<型引数1: プロトコル1, 型引数2: プロトコル2>(引数) -> 戻り値の型
where プロトコル1に連想型 == プロトコル2の連想型 {
関数呼び出し時に実行される文
}
次の例では、ジェネリック関数concat(::)は2つのCollection型の値を連結します。concat(::)関数はCollectionプロトコルに準拠した別々の型引数TとUを受け取りますが、これらの連想型T.ElementとU.Elementが一致することを要求しています。
(Set型とは集合を表す順序のないコレクションであり、Collectionプロトコルに準拠しています。 )
func concat<T: Collection, U: Collection>(_ arugument1: T, _ arugument2: U)
-> [T.Element] where T.Element == U.Element {
return Array(arugument1) + Array(arugument2)
}
let array = [1, 2, 3] // [Int]型
let set = Set([1, 2, 3]) // Set<Int>型
let stringArray = ["a", "b", "c"] // [String]型
let result = concat(array, set) // [1, 2, 3, 2, 3, 1]
let result2 = concat(array, stringArray) // Elementの型が一致しないためコンパイルエラー
ジェネリック型
定義方法
ジェネリック型とは、型引数を持つクラス、構造体、列挙型のことです。ジェネリック型は、型定義の型名の直後に型引数を追加して定義します。
struct GenericStruct<T> {
var property: T
}
class GenericClass<T> {
func someFunction(x: T) {}
}
enum GenericEnum<T> {
case SomeCase(T)
}
特殊化方法
ジェネリック型のインスタンス化や、ジェネリック型のスタティックメソッドの実行には、特殊化が必要となります。ジェネリック型の特殊化には明示的に型引数を指定する方法と、型推論によって型引数を決定する方法の2つが用意されています。
// 明示的に型引数を指定しての特殊化
struct Container<Content> {
var content: Content
}
let intContainer = Container<Int>(content: 1)
let stringContainer = Container<String>(content: "abc")
// 型引数とイニシャライザの引数の型が一致しないのでエラー
let container = Container<Int>(content: "abc")
// イニシャライザやスタティックメソッドの引数からの型推論によっての特殊化
struct Container<Content> {
var content: Content
}
let intContainer = Container(content: 1) // Container<Int>型
let stringContainer = Container(content: "abc") // Container<String>型
型制約
ジェネリック関数と同様に、ジェネリック型の型引数にも型制約を設けられます。しかし、使用できる型制約の種類や場所にはいくつかの違いがあります。
型の定義で使用できる型制約
ジェネリック関数では、3つの種類の型制約を使用できましたが、ジェネリック型の方の定義ではその3つのうち、型引数のスーパークラスや準拠するプロトコルに対する制約が使用できます。制約を指定するには以下のように記述します。
struct 型名<型引数: プロトコル名やスーパークラス名> {
構造体の定義
}
ジェネリック型の型制約付きのエクステンション
ジェネリック型では、型引数が特定の条件を満たす場合にのみ有効となるエクステンションを定義でき、これを型制約付きエクステンションと言います。型制約付きエクステンションを利用すると、型制約を満たす型が持つプロパティやメソッドを使った機能を、汎用的に実装することができます。
// 定義方法
extension ジェネリック型名 where 型制約 {
制約を満たす場合に有効となるエクステンション
}
次の例では、ジェネリック型Pairに対して、型引数ElementがString型の場合のエクステンションを定義しています。型制約付きエクステンションが有効となるPairがでは定義したhasElementメソッドが使用できる一方で、無効となるPairではメソッドが存在しないためコンパイルエラーとなります。
struct Pair<Element> {
let first: Element
let second: Element
}
extension Pair where Element == String {
func hasElement(containing character: Character) -> Bool {
return first.contains(character) || second.contains(character)
}
}
let stringPair = Pair(first: "abc", second: "def")
stringPair.hasElement(containing: "e") // true
let integerPair = Pair(first: 1, second: 2)
integerPair.hasElement(containing: "e") // 型引数がInt型のためメソッドが存在せずコンパイルエラー
プロトコルへの条件付き準拠
型制約付きエクステンションでは、プロトコルへの準拠も可能です。これをプロトコルへの条件付き準拠と言い、ジェネリック型は型引数が型制約を満たす時のみプロトコルへ準拠します。
// 定義方法
extension ジェネリック型名: 条件付き準拠するプロトコル名 where 型制約 {
制約を満たす場合に有効となるエクステンション
}
次の例では、Pair型の型引数ElementがEquatableプロトコルに準拠している場合、Pair型もまたEquatableプロトコルに準拠させることができるようになっています。Pair型の値どうしが==演算子で比較できていることから、Pair型がEquatableプロトコルに準拠できていることがわかります。
struct Pair<Element> {
let first: Element
let second: Element
}
extension Pair: Equatable where Element: Equatable {
static func ==(_ lhs: Pair, _ rhs: Pair) -> Bool {
return lhs.first == rhs.first && lhs.second == rhs.second
}
}
let stringPair1 = Pair(first: "abc", second: "def")
let stringPair2 = Pair(first: "def", second: "ghi")
let stringPair3 = Pair(first: "abc", second: "def")
stringPair1 == stringPair2 // false
stringPair1 == stringPair3 // true
参考
・Swift実践入門 (https://gihyo.jp/book/2020/978-4-297-11213-4)