ジェネリックタイプ
ジェネリクスの機能は、クラスやストラクチャ、列挙型に対しても用いることができます。ジェネリック関数のときと同様の型引数宣言を型名の後に記述することで、型に紐づいたプロパティやメソッドなど、その型の中で使用できる型引数を定義できます。
(class/struct/enum) 型名<型引数1: (クラス名 or プロトコル名), 型引数2: (クラス名 or プロトコル名), ... where ...> { 型の内容 }
このように、型名の後に型引数宣言があるような型のことをジェネリックタイプといいます。
struct Queue<T> { private var values = [T]() mutating func put(value: T) { values.append(value) } mutating func get() -> T? { let value = values.first if values.count > 0 { values.removeAtIndex(0) } return value } } var queue = Queue<Int>() queue.put(1) queue.put(2) queue.get() // 1 queue.put(3) queue.get() // 2 queue.get() // 3 queue.get() // nil
このQueueストラクチャはFIFO(First In First Out)の待ち行列を表したデータ型で、putメソッドで待ち行列内部に値を格納し、getメソッドで待ち行列のうち最も前に格納された値を返却します。
ストラクチャの宣言の際にQueueという型名の後に型引数Tを宣言しておけば、それを型の中のvaluesプロパティやメソッドの「間に合わせの型」として使用できます。
また、ジェネリックタイプの特徴として、型名の直後に具体的な型引数を注入することで、値を初期化できる点があります。
var queue = Queue<Int>()
この部分では型名Queueの直後に型引数Intを注入することで、queue変数を初期化しています。
ジェネリックタイプをインスタンスとして実際に使用するときには、型引数が必ず推測できるようになっていなければなりません。
例えば、先ほどの例で<Int>という型引数を注入している部分を消してみると、コンパイルエラーになります。
var queue = Queue()
初期化の際に型名の後に付けられた型引数は、コンパイラが行う型推論の際に手助けとなっています。次のように宣言しても、コンパイラはQueue<T>の具体的な型を推論します。
var queue: Queue<Int> = Queue()
型に紐づいたメソッドについても、ジェネリック関数と同様に型に宣言された型引数とは関係のない型引数を宣言すれば、次のように型推論に関して独立したジェネリック関数のように使用できます。
struct Checker<T> { func notEqual<E: Equatable>(lhs: E, _ rhs: E) -> Bool { return lhs != rhs } } var checker = Checker<Int>() checker.notEqual("a", "b") // true
頻出ジェネリックタイプ
ジェネリクスが用いられたSwiftの汎用的な型としては、配列:Array<T>や辞書型:Dictionary<Key: Hashable, Value>が挙げられます。オプショナル列挙型Optional<T>もジェネリックタイプの一例です。
これらの頻出型についてはシンタックスシュガーがあります。今一度ここでおさらいしてみましょう。
配列
配列Array<T>にはシンタックスシュガーとして[T]が提供されています。これらの変数には同じ値が入ります。
var array1 = Array<Int>() var array2 = [Int]() var array3: [Int] = []
3番目のarray3への格納時には、左辺の型注釈によって、右辺のリテラルの具体的な型が決まっています。配列のリテラル[]は、Array<T>の型に対してはArray<T>()として振る舞います。
この場合だと、左辺の型がArray<Int>であると分かるため、右辺のリテラルはArray<Int>()として振る舞っています。
辞書型
配列の場合と同様に、辞書型Dictionary<Key: Hashable, Value>に対し、シンタックスシュガーとして[Key: Value]が提供されています。
var dic1 = Dictionary<String, Int>() var dic2 = [String: Int]() var dic3: [String: Int] = [:]
配列の例と同様に、3番目のdic3に対しても左辺の型注釈で、右辺リテラルの具体的な型が定まります。
この場合だと、左辺の型がDictionary<String, Int>であると分かるため、右辺のリテラル[:]はDictionary<String, Int>()として振る舞っています。
オプショナル列挙型
オプショナル列挙型は、型引数を用いて次のようなインターフェイスが定義されています(一部省略)。
enum Optional<T>: NilLiteralConvertible { case None case Some(T) init() // Optional.None型の初期化に用いられる init(_ some: T) // Optional.Some型の初期化に用いられる init(nilLiteral: ()) // NilLiteralConvertibleに準拠するための実装(後述) }
さらに、オプショナル列挙型Optional<T>には本連載の第3回で取り上げたように、シンタックスシュガーとしてT?が提供されています。
このようなインターフェイスを用いることで、配列型や辞書型と同じように、次のようなコードを書くことができます。
var optSome1 = Optional.Some(1) // --(1) var optSome2 = Int?(1) // --(2) var optSome3: Int? = 1 // --(3) var optNone1 = Optional<Int>.None // --(4) var optNone2 = Int?() // --(5) var optNone3: Int? = nil // --(6)
この例では、optSomeで始まる変数についてはすべてOptional<Int>.Some(1)が代入されます。一方、optNoneで始まる変数についてはすべてOptional<Int>.Noneが代入されます。
--(1)のコードで型引数が省略できる理由は、オプショナル列挙型のイニシャライザ引数1によって型推論がなされるためです。それとは対照的に、--(4)のコードでは型引数が必要です。型引数がないとOptionalの具体的な型引数が推論できないためです。
Optional<T>型はシンタックスシュガーとしてT?を代用できるため、--(2), --(5)のような書き方で変数の初期化を行うことができます。このとき--(2)ではイニシャライザinit(_ some: T)を用いて、--(5)ではイニシャライザinit()を用いて変数の初期化を行っています。
オプショナル列挙型の性質として、Tの型の値であってもT?の型の変数に代入できるという点がありました。--(3)のコードではこの性質を用いて変数の初期化を行っています。--(6)のコードでnilが代入できるのはOptional<T>がNilLiteralConvertibleプロトコルに準拠しており、T?へのリテラルnil代入の際にイニシャライザinit(nilLiteral: ())が内部的には読み出され、Optional<T>.Noneとして初期化が行われるためです。
NilLiteralConvertibleプロトコルはリテラルnilからの初期化を要求するプロトコルです。プロトコルに準拠した型は次のイニシャライザを実装して、リテラルnilからの初期化を定める必要があります。
init(nilLiteral: ())
nilとOptional<T>が1対1につながった概念であるという説明も頻繁に見られますが、nilはSwiftが発表された当初とは扱いが異なり、現在ではリテラルとして扱われているため、オプショナル列挙型以外の型に対しても用いられることに注意しましょう。