Swiftの初期化 | 初期化のカスタマイズ(Customizing Initialization)

Swiftの初期化 | 初期化のカスタマイズ

ここでは初期化のカスタマイズに関して詳しく説明していきます。具体的には、インプットパラメータの指定、オプショナル型のプロパティの初期化、または定数プロパティの初期化です。さらに、デフォルト初期化子(Default Initializers)についても詳しく説明します。構造体のmemberwise initialzerは、クラスと構造体のページで簡単に触れましたが、実はデフォルト初期化子の1つです。

初期化のパラメータ

初期化のパラメータは、関数メソッドのパラメータと全く同じものです。したがって、その定義構文や使い方も全く一緒です。公式マニュアルでは、ここで摂氏の構造体が出てくるのですが、さっき使ってしまったので、ケルビン構造体を作ってみます。

初期化子の具体例 | ケルビン(K)構造体

// ケルビン構造体
struct Kelvin {
    var temperatureKelvin: Double
    init(fromCelsius celsius: Double) {
        temperatureKelvin = celsius + 273.15
    }
}

let boilingPointOfWater = Kelvin(fromCelsius: 100.0)
print("沸点=\(boilingPointOfWater.temperatureKelvin) K")
//"沸点=373.15 K"と表示

構造体(structures)としては、公式マニュアルの例とほぼ同じ形になっています。

ケルビン構造体の初期化子はパラメータを1つ持っています。

init(fromCelsius celsius: Double) {
    temperatureKelvin = celsius + 273.15
}

関数やメソッドの場合と同様に、メソッド呼び出し用ラベルfromCelsiusとメソッド定義用パラメータ名celsiusを定義しています。ラベルとパラメータ名の違いに関しては、後で詳しく説明します。

そもそも、ケルビンという単位は「絶対温度」で定義される温度スケールで、0度を「絶対零度」として、全ての運動が停止する温度として定義しています。摂氏と比べると、単に273.15度ずれているだけですので、(考え方は置いとくと)計算としては全然難しくありません。

したがって、例えば摂氏での沸点100°Cをケルビンに変換すると、上記の例のように

let boilingPointOfWater = Kelvin(fromCelsius: 100.0)
print("沸点=\(boilingPointOfWater.temperatureKelvin) K")
//"沸点=373.15 K"と表示

373.15°Kになります。

補足:摂氏と華氏とケルビン

摂氏とか華氏とかケルビンとか、結局温度を測る基準とかスケールが違うだけなので、ルールさえ分かれば難しくはありません。摂氏をC、華氏をF、ケルビンをKと置くと
温度、摂氏と華氏とケルビンの関係
このような関係にあります。ケルビンは単に温度をずらしただけですので、余り深く考えなくても大丈夫ですが、華氏は5/9が掛かっているので、やや複雑です。代表的な温度を書き出してみると少し感覚が分かるかもしれません。

摂氏(°C) 華氏(°F)
0 32
5 41
10 50
15 59
20 68
25 77
30 86
35 95

感覚としては、30度だと滅茶苦茶寒い、50度だと寒い、60度で涼しい、80度を超えると暑い、100度だと滅茶苦茶暑い、という感じでしょうか。

パラメータ名と呼び出し用ラベル

Swiftでは、メソッド(または関数)定義用のパラメータ名のことを「パラメータ名(parameter name)」、メソッド等を呼び出す際に使用するパラメータ名を「ラベル(argument label)」、と呼んでいます。外部呼び出し用の名前は「アーギュメントラベル」と呼んでもいいのですが、長いのでラベルとします。これらの呼称はSwift 3からの仕様で、Swfit 2.xではローカルパラメータ名(local parameter name)、外部パラメータ名(external parameter name)と呼んでいました。

初期化子と関数(メソッド)との違い

初期化子はメソッドの一部ですが、もっと正確に言うと「戻り値のないメソッド」です。したがって、先程のケルビン構造体でも定義していましたが、パラメータ名(初期化子内部で使う名前)とラベル(初期化子を呼び出す際に使う名前)を持つことが出来ます。

初期化子と関数やメソッドとの違いは何か?というと、名前による識別が可能かどうか、です。関数やメソッドには、それぞれ固有の名前がありますが、初期化子の名前はinit限定です。したがって、特に初期化子が複数ある場合、パラメータの名前が非常に重要になります。何故かと言うと、パラメータの名前が指定できないと、どの初期化子を呼び出しているか分からなくなるからです。

このような理由で、Swiftでは全ての初期化子のパラメータには、(外部呼び出し用)ラベルが自動的に割り当てられます。デフォルトのラベルは、単純に与えたパラメータ名と同じになります。

Swift 2.xでは第一引数が特別だったので、これは初期化子固有の仕様でした。しかし、Swift 3から全ての関数やメソッドの引数が同等の扱いになったので、パラメータ名がデフォルトで外部呼び出し用ラベルにもなるという仕様は、初期化子固有の機能という訳ではありません

初期化子におけるデフォルトラベルの具体例

先程のケルビン構造体を拡張して、華氏から初期化できるように改造してみます。

// ケルビン構造体、華氏からも初期化可能
struct Kelvin {
    var temperatureKelvin: Double
    init(fromCelsius celsius: Double) {
        temperatureKelvin = celsius + 273.15
    }

    init(fahrenheit: Double) {
        temperatureKelvin = 5.0/9.0*(fahrenheit-32.0) + 273.15
    }
}
let freezingPointOfWater = Kelvin(fahrenheit: 32.0)
print("融点=\(freezingPointOfWater.temperatureKelvin) K")
//"融点=273.15 K"と表示

新たに初期化子を追加して、華氏からもケルビン単位で温度をセット出来るようにしました。追加した初期化子はパラメータfahrenheitを持っています。今指定しているパラメータ名が1つだけですから、fahrenheitは初期化子定義用のパラメータ名であり、同時に呼び出し時に使用するラベルでもあります。

実際に使用する場合には、

let freezingPointOfWater = Kelvin(fahrenheit: 32.0)
print("融点=\(freezingPointOfWater.temperatureKelvin) K")
//"融点=273.15 K"と表示

このようにラベルfahrenheitを指定します。

今回の例では初期化子が2つあって、どちらもパラメータを1つしか取りません。したがって、そもそもパラメータ名を指定しないとどちらを呼びだそうとしているのか分からない、というのは理解出来ると思います。

実際に、外部パラメータ名を指定せずに初期化しようとすると、

//error: argument labels '(_:)' do not match any available overloads
//または
//error: missing argument labels '***' in call

という感じのコンパイルエラーになります。

呼び出し用ラベルが無い初期化子のパラメータ

関数やメソッドの場合と同様に、ラベルを省略することが出来ます(関数呼び出し用ラベルの省略はこちらを参照)。ラベルを省略したい場合はアンダースコア_を指定します。

公式マニュアルと似たような構造になりますが、ケルビン単位で温度をセット出来る初期化子を追加してみます。

// ケルビン単位で初期化
struct Kelvin {
    var temperatureKelvin: Double
    init(fromCelsius celsius: Double) {
        temperatureKelvin = celsius + 273.15
    }

    init(fahrenheit: Double) {
        temperatureKelvin = 5.0/9.0*(fahrenheit-32.0) + 273.15
    }

    init(_ kelvin: Double) {
        temperatureKelvin = kelvin
    }
}

let temperature = Kelvin(25.0)

このようにラベルを省略することで、値だけを使って初期化することが出来ます。

複数ある初期化子のラベルを省略したい場合は注意が必要です。というのも、ラベルを省略すると、呼び出す際に2つの初期化子の区別が付かなくなるからです。ただし、これは初期化子のパラメータ数が同じ場合に限ります。試しに、上記のinit(fahrenheit: Double)のラベルを省略してみると、

//error: invalid redeclaration of 'init'

こんな感じでコンパイルエラーになります。

パラメータの数が違う場合(例えばパラメータが1つの初期化子と2つの初期化子)は、両方のラベルを省略しても問題ありません。

初期化におけるオプショナル型のプロパティ

独自型(クラスや構造体列挙型)の持つプロパティが、「値がない(nil)」という状態になる場合はオプショナル型(optionals)を使います。どういう場合に「値がない」状態になるかというと、例えば

  • 初期化の時に値がセット出来ない
  • 初期化の際には値があるが、その後「値がない」状態になる可能性がある

というケースです。オプショナル型プロパティは自動的にnilがセットされますので、初期化の時には放っておいても大丈夫です。

ケルビン構造体に、新たに摂氏の温度を指すプロパティvar temperatureCelsius: Double?追加します。

// オプショナル型プロパティ
struct Kelvin {
    var temperatureKelvin: Double
    var temperatureCelsius: Double?
    init(fromCelsius celsius: Double) {
        temperatureCelsius = celsius
        temperatureKelvin = celsius + 273.15
    }

    init(fahrenheit: Double) {
        temperatureKelvin = 5.0/9.0*(fahrenheit-32.0) + 273.15
    }

    init(_ kelvin: Double) {
        temperatureKelvin = kelvin
    }
}

let boilingPointOfWater = Kelvin(fromCelsius: 100.0)
print(boilingPointOfWater.temperatureCelsius)
//"Optional(100.0)"と表示

let freezingPointOfWater = Kelvin(fahrenheit: 32.0)
print(freezingPointOfWater.temperatureCelsius)
//"nil"と表示

temperatureCelsiusは、一番最初の初期化子でしか初期化されていません。例えば2番目の初期化子を使ってインスタンスを生成した場合、temperatureCelsiusは「値がない」状態になります。したがって、オプショナル型にしておかないといけません(オプショナルでないとコンパイルエラーになります)。

実際、初期化子init(fromCelsius celsius: Double)を使った場合は、ちゃんと値がセットされています。一方で、初期化子init(fahrenheit: Double)の場合は、temperatureCelsiusnilのままです。

オプショナル型は、forced unwrappingかoptional bindingでオプショナルを外さないと、値として使えません。上記の例ではわざと外していません。

定数プロパティの初期化

定数プロパティの値は初期化子の中でセットすることが出来ます。もちろん定数ですので、一度初期化したら以降は変更することが出来ません。試しにプロパティ変数temperatureKelvinを定数にしてみます。

// 定数プロパティ
struct Kelvin {
    let temperatureKelvin: Double
    var temperatureCelsius: Double?
    init(fromCelsius celsius: Double) {
        temperatureCelsius = celsius
        temperatureKelvin = celsius + 273.15
    }

    init(fahrenheit: Double) {
        temperatureKelvin = 5.0/9.0*(fahrenheit-32.0) + 273.15
    }

    init(_ kelvin: Double) {
        temperatureKelvin = kelvin
    }
}

変数が定数になっただけです。このように、定数プロパティは定数ですが、初期化子の中ではあたかも変数のように値をセットすることが出来ます。

公式マニュアルに注意書きがありますが、クラスインスタンスの場合、定数プロパティはそれを導入したクラスでないと変更できません。言い換えると、サブクラス先では変更できない、ということです。

“NOTE

For class instances, a constant property can be modified during initialization only by the class that introduces it. It cannot be modified by a subclass.”

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

デフォルト初期化子(Default Initializers)とは?

初期化子が1つもない場合に用意される初期化子

デフォルト初期化子(default initializers)というのは、C言語で言う所のデフォルトコンストラクタのようなものです。クラスや構造体を定義して、初期化子を1つも定義しなかった場合、暗黙に作られる初期化子のことです。

逆に言うと、初期化子を1つでも定義した場合は、デフォルト初期化子は作られません。

「初期化子を1つも作らない」ということは「全てのプロパティ等を直接初期化した」ことになります。そうしないとコンパイルエラーですからね。ただし、「全てのプロパティに初期値を与えて、さらに初期化子を作った」というケースでは、デフォルト初期化子は作られません。あくまで「初期化子が1つもない」という状態が必要です。

例えば、ケルビン構造体の温度に初期値を与えた場合、

// デフォルト初期化子
struct Kelvin {
    let temperatureKelvin: Double = 0.0
    var temperatureCelsius: Double?
}

このように、init()を定義していませんが、実質

init() {
    temperatureKelvin = 0.0
    temperatureCelsius = nil
}

と同じ処理をしています。Optionalsへのnil代入は必要ありませんが、明示的に値を見せたかったので敢えて書きました。

これは、実際にインスタンスを生成してプロパティの値を確認するとすぐに分かります。

let temperature = Kelvin()
print(temperature.temperatureKelvin)
//0.0
print(temperature.temperatureCelsius)
//nil

構造体のmemberwise initializers

構造体がmemberwise initializersという初期化子を持つことは、以前クラスと構造体のページで説明しました。名前の通り、これは初期化子の一種です。Memberwise initializerは、初期化子を定義しなかった場合、自動的に作られます。したがって、memberwise initializerはデフォルト初期化子と言えますが、パラメータを持つのが最大の違いです。また、プロパティに初期値が与えられているかどうかは関係ありません。つまり、プロパティがデフォルト値を持とうが持つまいが、それとは関係なく、初期化子がない場合に自動的に作られます。

デフォルト初期化子と同様に、もし初期化子を1つでも作った場合、memberwise initializerは生成されません。

実は、1つ前のデフォルト初期化子で作った構造体の例では、デフォルト初期化子とmemberwise initializerが作られています。ココで注意しないといけないのは、今temperatureKelvinが定数ですから、memberwise initializerのパラメータは変数であるtemperatureCelsiusのみです。試しに、memberwise initializerを呼び出してみると

let temperature = Kelvin(temperatureCelsius: 273.15)
print(temperature.temperatureCelsius)
//"Optional(273.15)"と表示

という具合に、ちゃんと呼び出せるのが分かります。temperatureKelvinを変数にすると、2変数をパラメータに持つmemberwise initializerになるので、試してみると良く分かると思います。

まとめ

  • 初期化子のパラメータは、関数やメソッドのパラメータと同じ取扱い
  • 呼び出し用ラベルを省略したい場合は、アンダースコア_を使う
  • オプショナル型プロパティはデフォルトnilで初期化されるので、初期化子で値をセットする必要はない
  • 定数プロパティの初期化も可能。ただし、サブクラスではスーパークラスの定数プロパティをセットできない。
  • 初期化子を定義しなかった場合、パラメータを持たないデフォルト初期化子が作られる
  • 構造体の場合は、全ての変数プロパティをパラメータとして持つmemberwise initializerも、デフォルト初期化子の1つとして自動的に生成される