Swiftの初期化 | 値型の初期化子の委譲(Initializer Delegation for Value Types)

値型の初期化の委譲(Initializer Delegation for Value Types)

初期化子の委譲(Initializer Delegation)です。和訳しても今いちピンとこないですが、要するに初期化子が別の初期化子を呼び出すことです。値型の場合は、ものすごく簡単ですが、参照型(というよりも、継承できるクラス)の場合は一気に複雑になります。

ここでは、初期化子の委譲とは何か?から始めて、値型(特に構造体について)の初期化子の委譲に関して、具体例を使って詳しく説明します。

初期化子の委譲(Initializer Delegation)って何?

Initializer Delegationというのは少し難しい表現ですが、delegationは日本語で「代表」、「委譲」または「委任」です。ここでの最適な和訳は「委譲」で、委譲は「権利や権限などを譲って任せること」です。したがって「initializer delegation」を直訳すると

初期化子の委譲 = 初期化を他の初期化子に任せること

ということになります。冒頭にも書きましたが、要するに新しい初期化子を作った場合に既存の初期化子を呼び出す(再利用する)ことです。

Swiftにおける初期化子の委譲は、

インスタンス初期化の際に、ある初期化子が別の初期化子を呼び出すこと(またはその過程)

を指します。初期化子の委譲を使うことで、複数の初期化子間におけるコードの重複を防げます。

値型(構造体と列挙型)と参照型(クラス)では、初期化子の委譲の使い方や、それが取り得る形が違います。したがって、このページでは先ず比較的簡単な値型の初期化子の委譲に関して、具体例を示しつつ詳しく説明します。クラスの初期化子の委譲については、次のページで掘り下げて行きます。

値型(構造体)の初期化子の委譲

値型の場合は簡単です。理由は、値型である構造体や列挙型は継承(inheritance)が出来ないからです。したがって、委譲で呼び出せる初期化子は、自分自身が保有している初期化子に他なりません。

具体例で見る構造体の初期化子の委譲

具体例で見た方が分かりやすいので、早速構造体を使って説明します。

// 構造体で初期化子の委譲
import Darwin

// 2次元座標
struct Point {
    var x = 0.0, y = 0.0
}

// 直線構造体
struct Line {
    var start = Point()
    var stop = Point()

    init() {}
    init(start: Point, stop: Point) {
        self.start = start
        self.stop  = stop
    }

    init(center: Point, length: Double, theta: Double) {
        let x1 = center.x - 0.5*length*cos(theta)
        let y1 = center.y - 0.5*length*sin(theta)
        let x2 = center.x + 0.5*length*cos(theta)
        let y2 = center.y + 0.5*length*sin(theta)

        self.init(start: Point(x:x1,y:y1), stop: Point(x:x2,y:y2))
    }
}

直線を表す構造体Lineです。直線の始点startと終点stopを与えるために、別の構造体Pointも作っています。

直線構造体には初期化子が3つあって、

  • パラメータ無しの初期化子(デフォルト初期化子のような初期化子)init() {}
  • 始点と終点で初期化init(start: Point, stop: Point)
  • 直線の中心、直線の長さ、x軸(横軸)からの角度で初期化init(center: Point, length: Double, theta: Double)

です。3番目の初期化子では、2番目の初期化子を呼び出していますが、まさにこれが初期化子の委譲です。

// 初期化子の委譲、全体の構造
// 2番目の初期化子
init(start: Point, stop: Point) {
   ....
}

// 3番目の初期化子
init(center: Point, length: Double, theta: Double) {
  self.init(....) // 2番目の初期化子を呼び出す -> 初期化子の委譲
}

以下、1つずつ詳しく見ていきます。

パラメータなしの初期化子(デフォルト初期化子のようなもの)

直線構造体では、直線の始点startと終点stopプロパティで定義しています。どちらもデフォルト初期化子でインスタンスを生成しています。

// 直線構造体
struct Line {
    var start = Point()
    var stop = Point()

    init() {}
    ....
}

始点と終点に初期値を与えている理由は、構造体Lineをパラメータ無しで初期化したいからです。

構造体Lineでは独自の初期化子を実装していますので、デフォルト初期化子が自動的に生成されません。したがって、パラメータ無しの初期化子(デフォルト初期化子のようなもの)を明示的に定義しないといけません。

// 空のパラメータ無し初期化子
init() {}

本体を見ると{}で何もしていませんが、この初期化子を使ってインスタンスを生成すると、変数プロパティは全て初期値がセットされます。

これはデフォルト初期化子かと思うのですが、公式マニュアルを読む限り、「デフォルト初期化子と機能的に同じ初期化子」という取り扱いのようです。これはただの定義の問題ですから、余り気にしなくても良いかもしれません。

もし始点と終点に初期値を与えなかった場合、初期化子内部で値を代入(初期化)しないといけません。

// こう書いても良い
struct Line {
    var start: Point
    var stop: Point

    init(){
        start = Point()
        stop = Point()
    }
    ....
}

好みなのでどちらでも良いと思いますが、前者(今回の例で採用した、デフォルト初期値+パラメータ無しの空の初期化子)の方がすっきりして、個人的にはオススメです。繰り返しになりますが、これはデフォルト初期化子(のようなもの)を使いたいための実装ですので、常にパラメータを与えて初期化するのであれば必要ありません。

パラメータ付き初期化子1 | memberwise initializerのような初期化子

2番目の初期化子は、memberwise initializerのような初期化子です。つまり、構造体(または一般的に、独自型)が保有する変数プロパティが全てパラメータになっている初期化子です。

Memberwise initializerは、プロパティの名前がそのままパラメータ名になっていること、他に初期化子が無い場合に自動的に定義されること、という条件付きの初期化子です。公式マニュアルでは、ここでも「memberwise initializerと機能的に同じ初期化子」という言い方をしています。

今回の例ですと、パラメータ名がプロパティ名と同一なので、ほぼmemberwise initializerです。

// パラメータ付き初期化子
init(start: Point, stop: Point) {
    self.start = start
    self.stop  = stop
}

パラメータ付き初期化子2 | Initializer Delegation

3番目の初期化子は中身が少しややこしいですが、与えられたパラメータ(直線の中心、直線の長さ、直線と横軸のなす角度)から始点と終点を計算して、2番目の初期化子に渡しています。

// 直線の中心、長さ、角度から始点と終点を計算
init(center: Point, length: Double, theta: Double) {
    let x1 = center.x - 0.5*length*cos(theta)
    let y1 = center.y - 0.5*length*sin(theta)
    let x2 = center.x + 0.5*length*cos(theta)
    let y2 = center.y + 0.5*length*sin(theta)

    self.init(start: Point(x:x1,y:y1), stop: Point(x:x2,y:y2))
}

ここで組み込み関数のcos (sin)を使うので、この例の先頭でimport Darwinしてます。この計算は図解の方が分かりやすいです。

中心と長さと角度から直線の始点と終点を計算
cos(cosine, コサイン、余弦)の定義から、始点から直線の中心までのx方向の長さはl/2 cos(θ)です(図の赤い太線)。同様に、y方向の長さはl/2 sin(θ)になります(図の青い太線)。今中心の座標(xc,yc)が与えられているので、始点の座標はこれで計算できます。

終点も同様に計算していくと、最終的に上の画像にあるような計算式が出て来ます。直感的に対称的な形になりそうですが、確かにそうなっています。始点と終点が計算出来たら、後はそれを既存の初期化子に渡します。

// Initializer Delegation
self.init(start: Point(x:x1,y:y1), stop: Point(x:x2,y:y2))

自分自身の初期化子を呼び出す際には、必ずselfを付けてコールします。初期化子のみで呼びだそうとするとコンパイルエラーになります。

公式マニュアルに書いてありますが、もちろん初期化子の委譲を使わずに、始点と終点の値を直接セットすることも可能です。しかし、初期化子を再利用する方が便利かつ意図が明確ですし、さらに既に用意されている別の初期化子(関数とかメソッドと言い換えると分かりやすいかもしれません)で実装されている機能を使わないのは勿体無いです。

“The init(center:size:) initializer could have assigned the new values of origin and size to the appropriate properties itself. However, it is more convenient (and clearer in intent) for the init(center:size:) initializer to take advantage of an existing initializer that already provides exactly that functionality.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

Extensionを使うと、同じ初期化を別の方法で実装できるようですが、またExtensionのページで触れたいと思います。

まとめ

  • 初期化子の委譲(Initializer Delegation)とは、ある初期化子が別の初期化子を呼び出すこと
  • 値型(構造体、列挙型)の場合、呼び出されるのは自身が保有する初期化子(継承が出来ないから)
  • 自分自身の初期化子を呼び出す場合は、必ずselfを付ける