Swiftのオプショナルチェイン(Optional Chaining)

Swiftのオプショナルチェイン(Optional Chaining)

オプショナルチェイン(Optional Chaining)です。これまではオプショナル型の何か(プロパティなど)に対してのアクセスを取り扱ってきました。ここではオプショナル型が主体で、オプショナル型の変数等がプロパティやメソッドなどにアクセスする方法の1つとしてオプショナルチェインという機能がある、ということを詳しく説明していきます。

まず、オプショナルチェインとは何か?から始めて、オプショナルチェインとforced unwrappingの違いについて説明します。次に、公式マニュアルにある具体例を流用してオプショナルチェインを使ったプロパティ、メソッド、サブスクリプトへのアクセスについて説明します。最後に、多重オプショナルチェインと、メソッドを経由する場合のオプショナルチェインに関しても簡単に説明します。

オプショナルチェイン(Optional Chaining)って何?

オプショナル型からアクセス

オプショナルチェイン(Optional Chaining)というのは、

nilになる可能性のあるオプショナル型(optionals)から、プロパティ、メソッド、サブスクリプトなどを呼び出す(呼びだそうとする)プロセスのこと

を指します。

これまで見てきたのは、optional bindingやforced unwrappingなど、オプショナル型の変数等々を呼び出すことに焦点を置いていました。ここではオプショナル型からプロパティ等を呼び出すにはどうするか?に関する決まりを見ていきます。

オプショナルチェイン = オプショナル型の連結(連鎖)

「チェイン」という部分は、例えば

//オプショナルチェインのイメージ
someVariable.optionalProperty?.someMethod()

のように、プロパティ(property)メソッド(method)が連結して呼び出されている様子を指しています。この後詳しく説明しますが、上記のサンプルコードでオプショナルチェインに該当するのは、2番目以降のoptionalProperty?.someMethod()の部分です。

上記の例ではオプショナル型は1つでしたが、プロパティやメソッド等の連結で複数のオプショナル型がある場合、どれか1つでもnilになると全体がnilになるように設計されています。

クラスと構造体のページで言及しましたが、Swiftの基本型も実は構造体ですから、プロパティやメソッドを持っている可能性があります。したがって、上記の例で示したような3段の連結くらいなら普通に起こり得ます。

Forced Unwrappingの代替として使うオプショナルチェイン

Forced unwrappingとoptional chainingの違い

オプショナルチェインの構文は、先程も見たように、

//オプショナルチェインのイメージ
someVariable.optionalProperty?.someMethod()

という感じで、クエスチョンマーク?をオプショナル型の後に付けてプロパティ等を呼び出す形になります。

「Forced unwrappingを使えばいいのでは?」と思うかもしれませんが、オプショナルがnilだった場合の処理が大きく違って、オプショナルチェインだと全体がnilになるだけですが、forced unwrappingだと実行エラーになります

オプショナルチェインの具体例 | String構造体の場合

試しにStringを使ってテストしてみます。String型はSwiftの基本的型ですが構造体です。構造体なのでプロパティやメソッドを持っています。下記の例で使っているのは、String型が持つlowercased()というメソッドで、文字列を全部小文字にして返してくれるメソッドです。

// String型でテスト
var property: String?
property = "TEST"

// オプショナルチェイン
if let lower = property?.lowercased() {
    print(lower)
}
//"test"と表示

// Forced unwrapping
print(property!.lowercased())
//"test"と表示

この例だと、オプショナル型Stringにちゃんと値が入っているので、オプショナルチェインとforced unwrappingでの違いはなく、どちらもproperty変数の持っている文字列をちゃんと返しているのが分かります。

コンパイルエラーか実行時エラーか

次に、変数propertynilを代入して再び動作確認してみると、

// nilの場合
property = nil
if let lower = property?.lowercased {
    print(lower)
} else {
    print("nilです")
}
//"test"と表示

print(property!.lowercased())
//fatal error: unexpectedly found nil while unwrapping an Optional value
//....
//と表示されクラッシュ

という感じで、forced unwrappingの場所で実行エラーを起こし、プログラムがクラッシュします。オプショナルチェインの場合は、実行エラーを起こすのではなく、ちゃんとnilを返しているだけなのが分かります。

オプショナルチェインの結果はオプショナル型

また、オプショナルチェインの結果がnilになるということは、

オプショナルチェイン呼び出しの結果はオプショナル型になる、

ということです。当たり前のように思えるかもしれませんが、最終的に呼び出されるプロパティやメソッド等がnonoptionalな値であっても適用されます。

もう少し具体的に説明すると、例えば通常Stringのプロパティがあったとして、それをオプショナルチェインで呼び出した場合は、戻り値が(通常Stringですが)String?になる、ということです。

ここからオプショナルチェインの具体的な使い方を見ていきます。具体例は公式マニュアルのものを流用します。

オプショナルチェインの説明に使うクラス4つ

以降の説明に使うクラス4つ(公式マニュアルから流用したもの)を先に紹介しておきます。

Personクラス

まずPersonクラス。

// Personクラス
class Person {
    var residence: Residence?
}

居住情報であるResidenceクラスをオプショナル型プロパティとして持っています。このクラスに関しては説明の必要はないと思います。

Residenceクラス

今回一番複雑なのがこのResidenceクラスです。

// Residenceクラス 
// - RoomクラスのArrayを保持
// - Addressクラスのプロパティ
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        get { return rooms[i] }
        set { rooms[i] = newValue }
    }
    func printNumberOfRooms() {
        print("The number of rooms is \(numberOfRooms)")
    }
    var address: Address?
}

Residenceクラスが持っている格納プロパティは2つで、1つはRoomクラスの配列Array、もう1つはAddressクラス(オプショナル型)。RoomAddressクラスに関しては後述します。それ以外のプロパティやメソッド等々は全てroomsRoomクラスのArray)を操作するためのものです。

サブスクリプト(subscriptは配列roomsに簡単にアクセスするために用意されています。

Roomクラス

Roomクラスは部屋用クラスです。

// Room(部屋)クラス
class Room {
    let name: String
    init(name: String) { self.name = name }
}

この例ですと、部屋の名前を与えるだけのクラスになっています。

Addressクラス

Addressクラスは住所周りの情報を与えるクラスです。

// Address(住所)クラス
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if buildingName != nil {
            return buildingName
        } else if buildingNumber != nil && street != nil {
            return "\(buildingNumber) \(street)"
        } else {
            return nil
        }
    }
}

建物の名前(buildingName)、建物の番号(buildingNumber)、通りの名前(street)が文字列として定義されていて、かつそれらは全てオプショナル型になっています。buildingIdentifier()メソッドは、順番にプロパティをチェックしていって、nilでなければ該当したプロパティ名を(場合によっては組み合わせて)返すメソッドになっています。

オプショナルチェインを使ったプロパティへのアクセス

では早速Personクラスインスタンスを作って、オプショナルチェインを使ったプロパティのアクセスについて見ていきます。

オプショナルチェインでプロパティをゲット(取り出す)

まず、プロパティをゲットするケースを見てみます。

// オプショナルチェインを使ったプロパティへのアクセス
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s)")
} else {
    print("Unable to retrieve the number of rooms")
}
//"Unable to retrieve the number of rooms"と表示

実行するとelseで表示している文字列が表示されていることが分かります。これはjohn.residencenilだからです。

最初に述べましたが、オプショナルチェインでは、連結している要素の内どれか1つでもnilになると全体がnilになります。今、john.residencenilですから、全体john.residence?.numberOfRoomsnilになります。

オプショナルチェインでプロパティをセット

次の例では、オプショナルチェインを使ってプロパティに値を代入するケースを見てみます。

// Addressインスタンスを作って代入
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

// Residenceがnilなので代入出来てない
if let address = john.residence?.address {
    print("addressはnilじゃない")
} else {
    print("addressはnil")
}
//"addressはnil"と表示

最後のoptional bindingは自分で付け足しました。実行すると分かりますが、新たに作ったsomeAddressインスタンスは代入されていません。これはjohn.residencenilだからです。

代入もオプショナルチェインの一部という取り扱いで、nilになる場合は代入演算子の右辺は全く実行されません。これが一目で分かるのが次の例です。

// オプショナルチェインが失敗する場合は代入演算子の右辺は実行されない
func createAddress() -> Address {
    print("Function was called")
    
    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"
    
    return someAddress
}
john.residence?.address = createAddress()
//何も表示されない

先程のAddressクラスインスタンス生成部分を関数で包みました。実行すると分かりますが、何も表示されません。もし関数が呼び出されていれば、"Function was called"が表示されるはずです。

オプショナルチェインを使ったメソッドの呼び出し

オプショナルチェインを使ってのメソッド呼び出しは当然可能ですが、面白いのはメソッド越しでもnil判定が可能なことです(メソッドが呼び出されているかどうかのチェック)。これはメソッドが返り値を持ってなくても可能です。

代入演算子のセクションで説明しましたが、Swiftでは「返り値がない」という型Voidがあって、これは空のtupleのタイプエイリアスであることも確認しました。ですので、実際には「何も返していない」ということはなく、「何も返してはない」という型を返していることになります(哲学的)。

オプショナルチェインで呼び出されたメソッドの返り値はオプショナル型

実際に実行してみると

// オプショナルチェインでメソッド呼び出し
if john.residence?.printNumberOfRooms() != nil {
    print("It was possible to print the number of rooms.")
} else {
    print("It was not possible to print the number of rooms.")
}
//"It was not possible to print the number of rooms."と表示

のようになります。

オプショナルチェインでオプショナル型からメソッドを呼び出した場合、その返り値は必ずオプショナル型になります。したがって、printNumberOfRooms()の返り値はVoid?であって、Voidではありません。返り値がオプショナル型ですから、nilとの比較が出来、if文の条件として

if john.residence?.printNumberOfRooms() != nil {

のような記述が可能になります。

代入演算子の返り値はVoid

代入演算子のセクションで少し触れましたが、代入演算子の返り値はVoid型です。オプショナルチェインの中に組み込むことでVoid?になるので、普通のメソッド呼び出しと同様にif文の条件として採用可能です。

// 代入演算子の返り値はVoid型
if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}
//"It was not possible to set the address."と表示

オプショナルチェインを利用するとこのような記述も可能になります。オプショナル型の中間変数を挟みたくない場合などに使えそうです。

オプショナルチェインを使ったサブスクリプトへのアクセス

プロパティ、メソッドと見てきましたが、オプショナルチェインはサブスクリプト(subscript)にも使えます。公式マニュアルに注意書きがありますが、

“NOTE

When you access a subscript on an optional value through optional chaining, you place the question mark before the subscript’s brackets, not after. The optional chaining question mark always follows immediately after the part of the expression that is optional.”

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

オプショナルチェインでサブスクリプトを呼び出す場合は、必ずサブスクリプトの前(変数や定数の直後)にクエスチョンマーク?を置くことに注意です。

オプショナルチェインでサブスクリプトから値をゲット

早速見ていきます。

// オプショナルチェインでサブスクリプトを使う
if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}

この例では、Residenceクラスが持つRoomクラスのArrayにサブスクリプトを使ってアクセスしようとしています。オプショナルチェインを使っているので見た目が複雑ですが、分解すると、

  1. Residenceクラスプロパティ呼び出し: john.residence?
  2. RoomクラスのArrayにサブスクリプトでアクセス: john.residence?[0]
  3. Roomクラスのプロパティname呼び出し: john.residence?[0].name

という具合になります。段階を踏んで意味を捉えると多少は分かりやすいかと思います。それでもややこしい!という場合は、例えば一旦john.residence?という部分を別の変数で置き換えたりすると理解が進むと思います。

オプショナルチェインでサブスクリプトに値をセット

プロパティの場合と同様ですが、サブスクリプトを使って値をセットすることも可能です。

john.residence?[0] = Room(name: "Bathroom")

この例ではサブスクリプトを使った代入には失敗しています。理由は何度も繰り返していますが、residencenilだからです。

実際にresidenceプロパティにResidenceインスタンスを割り当て、かつrooms配列に1つ以上のRoomインスタンスを入れておけば、サブスクリプトを使ってアクセスすることが可能になります。

// Residenceインスタンスを作って、RoomsインスタンスArrayにサブスクリプトでアクセス
let johnHouse = Residence()
johnHouse.rooms.append(Room(name: "Living Room"))
johnHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnHouse

// オプショナルチェインでサブスクリプトを使う
if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
//"The first room name is Living Room."と表示

後半のoptional bindingを使ったチェックは、一つ前の例と全く一緒です。配列要素に値を代入するのにArrayappend()メソッドを使っています。append()メソッドの引数にRoomインスタンスを直接代入していますが、この記述が分かりにくい場合は

let living = Room(name: "Living Room")
johnHouse.rooms.append(living)

と置き換えると良いかもしれません。

サブスクリプトがオプショナル型を返す場合

サブスクリプトがオプショナル型を返す場合(例えば、辞書Dictionarykey)、クエスチョンマーク?をサブスクリプト[]の後ろに置きます。

// Dictionaryを使ってテスト
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91 // 86を91に変更
testScores["Bev"]?[0] += 1 // 79+1 = 80
testScores["Brian"]?[0] = 72 // Brianはないのでnil
print(testScores)
//"["Bev": [80, 94, 81], "Dave": [91, 82, 84]]"と表示

少しややこしいですが、上のDictionarykeyとしてStringvalueとしては配列ArrayInt型)を持っています。形としては2次元配列になっていて、最初のインデックスが文字列、2番目のインデックスが配列要素のインデックスです。

ですから、例えば最初の実行例

testScores["Dave"]?[0] = 91 // 86を91に変更

ではDictionaryの要素"Dave"にアクセス(オプショナルチェイン)して、さらに配列要素の1番目[0]を指定し、そこに91という整数を代入しています。2番目の実行例も同様です。

3番目の実行例は

testScores["Brian"]?[0] = 72 // Brianはないのでnil

"Brian"という要素を参照しようとしていますが、これは存在しませんのでnilを返します。したがって、代入演算子より右側(= 72)は実行されません。

最終結果をプリントすると

//"["Bev": [80, 94, 81], "Dave": [91, 82, 84]]"と表示

となっていて、確かに加えた変更がちゃんと反映されていることが確認出来ます。また、3番目で"Brian"にアクセスしようとしていましたが、これはnilを返していたので、最終的なtestScoresの要素としても存在していないことが見て取れます。

多重オプショナルチェイン | Linking Multiple Levels of Chaining

「多重オプショナルチェイン」と書きましたが、要するにオプショナルチェインを複数個(2個以上)繋ぐことです。この時のルールは明確で、

  • 非オプショナル型はオプショナル型になる
  • オプショナル型はオプショナル型のまま(オプショナルのオプショナル、ということにはならない)

です。具体的な例を文字列型Stringで考えてみると、

String型をオプショナルチェインで取り出す時はString?String?ならそのままString?

ということです。多重オプショナルチェインの出力は必ずオプショナル型になります。

具体例で見てみると、

// 多重オプショナルチェイン
if let johnStreet = john.residence?.address?.street {
    print("John's street name is \(johnStreet).")
} else {
    print("Unable to retrieve the address.")
}
//"Unable to retrieve the address."と表示

という感じです。今最終的に取り出しているのはAddressクラスのプロパティstreetですが、Addressクラス用のプロパティaddressResidenceクラス用プロパティresidenceから呼び出していて、それを呼び出しているのはPersonクラスインスタンスのjohn、という構造になっています。

サブスクリプトのセクションでResidenceクラスインスタンスをjohn.residenceに渡してあるので、john.residencenilではありません。しかしjohn.residence.addressnilですから、全体john.residence?.address?.streetnilになります。

Addressクラスインスタンスを渡すとうまく行くかどうか確認してみましょう。

// 実際にAddressクラスインスタンスをセット
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address")
}
//"John's street name is Laurel Street."と表示

最終出力を見ると、ストリート名が表示されているので、johnsAddressはちゃんと代入されていることが分かります。

オプショナル型を返すメソッドに対するオプショナルチェイン

メソッドがオプショナル型を返す場合、そこからさらにオプショナルチェインすることが可能です。プロパティの多重オプショナルチェインの場合と同様で、最終的に返ってくる値の型はオプショナル型になります。ここではAddressクラスの持つメソッドを使ってテストしてみます。

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
//"John's building identifier is The Larches."と表示

これはメソッドがちゃんと動くかどうかの確認です。

メソッドが返す値をさらにオプショナルチェインする場合は、メソッドのカッコ()の後ろにクエスチョンマーク?を付けます。

// メソッドがオプショナル型を返す場合のオプショナルチェイン
if let beginWithThe = john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
    if beginWithThe {
        print("John's building identifier begins with \"The\".")
    } else {
        print("John's building identifier does not begin with \"The\".")
    }
}

まとめ

  • オプショナルチェイン(optional chaining)はプロパティ、メソッド、サブスクリプト等を連結して呼び出すこと
  • オプショナルチェインの呼び出しに失敗した場合、戻り値がnilになる
  • オプショナルチェインの出力はオプショナル型
  • 多重オプショナルチェイン(2個以上のオプショナルの連結)しても出力はオプショナル型になる
  • オプショナル型を返すサブスクリプトやメソッドのオプショナルチェインでは、カッコ([]or())の後ろに?を付ける