2019/12/17 Swift オプショナル型 パート3(オプショナルチェイニング)

オプショナルチェイニング(Optional chaining)はnilかもしれないオプショナル型インスタンスのプロパティ・メソッド・サブスクリプトへアクセスする方法です。オプショナル型インスタンスに値がある場合、プロパティ・メソッド・サブスクリプの呼び出しは成功します。オプショナル型インスタンスがnilの場合、プロパティ・メソッド・サブスクリプトの呼び出しはnilを返します。

強制アンラップの代わりとしてのオプショナルチェイニング

オプショナルチェイニングの記述方法

 

オプショナルチェイニングの書き方は、値がnilでない場合に呼び出したいプロパティ・メソッド・サブスクリプトの属するオプショナル型インスタンスの後ろに?を記述します。強制アンラップの記述法とよく似ています。この二つの一番の違いは、値がnilの場合の挙動です。強制アンラップの場合値がnilだと実行時エラーになりますが、オプショナルチェイニングの場合正常に失敗します。

 

問い合わせたプロパティ・メソッド・サブスクリプトが非オプショナル型の値を返す場合であっても、「オプショナルチェイニングはnil値を呼び出せる」という事実を反映するために、オプショナルチェイニングの結果は常にオプショナル型の値となります。オプショナルチェイニングが成功したか(返されたオプショナル型が値を保持している)、オプショナルチェイニングの連鎖の中にnilがあることで失敗した(返されたオプショナル型がnilである)かを調べるために、返されたオプショナル型の値を使うことができます。

 

呼び出したいプロパティ・メソッド・サブスクリプトが非オプショナルの値であっても、オプショナルチェイニングの結果は常にオプショナル型の値となります。値がnilの場合でもオプショナルチェイニングを呼び出せるということになので、失敗可能でなければならない、ということ。オプショナルチェイニングが成功(結果のオプショナル型が値を保持している)したか、あるいは失敗したかを調べることで、オプショナルチェイニングの結果を使うことができます。

具体的には、オプショナルチェイニングの結果は返される値の型と同じ型であるが、オプショナル型である。Int型のプロパティにオプショナルチェイニングでアクセスするとInt?型を返します。

次のコードは、オプショナルチェイニングと強制アンラップの違い、そしてオプショナルチェイニングによる参照が成功したかを調べる方法を示します。

まずPersonとResidenceという名前の二つのクラスを定義します。

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

ResidenceインスタンスはnumberOfRoomsという名前で初期値が1のInt型のプロパティを持っています。PersonインスタンスはResidence?型の、residenceという名前のプロパティを持っています。

新たにPersonインスタンスを作った時、そのインスタンスのresidenceプロパティは(自動的に)nilで初期化されます(residenceがオプショナル型だから)。下のコードでjohnは値がnilであるresidenceプロパティを持っています。

let john = Person()

定数johnのresidenceプロパティのnumberOfRoomsプロパティに、強制アンラップ(residenceの後ろにエクスクラメーションマーク( ! )を記述する)でアクセスしようとすると実行時エラーが発生します。residenceの値がnilだからです。

let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

上のコードでjohn.residenceがnilでない値を持っていれば、適切な部屋数のInt型の値をroomCountにセットすることができます。しかし上記のコードではresidenceはnilですので実行時エラーが発生します。

オプショナルチェイニングは、numberOfRoomsの値にアクセスする(強制アンラップの)代わりの方法を提供します。オプショナルチェイニングを使うには、エクスクラメーションマークの代わりにクエスチョンマーク( ? )を記述します。

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

こう記述することで、residenceに値がある場合にnumberOfRoomsの値を取得できます。

numberOfRoomsにアクセスしようとした場合失敗する可能性がありますので、オプショナルチェイニングはInt?(オプショナルInt)型の値を返そうとします。上のサンプルでresidenceがnilの時、numberOfRoomsにアクセスできない事実を反映するために、(オプショナルチェイニングの結果である)オプショナルInt型の値もnilを返します。(多分→の文は、residenceが非nilの場合の話をしている。)オプショナルInt値は整数値にアンラップするためオプショナルバインディングでアクセスされ、非オプショナル型の値が変数roomCountに代入されます。

注目していただきたいのは、numberOfRoomsが非Int?であっても(residenceがnilなら「john.residence?.numberOfRooms」もInt?を返すということは)真実である、ということです。 「オプショナルチェイニングによってアクセスする」ということは、numberOfRoomsは常に(Int型ではなく)Int?型を返すことを意味します。

john.residenceにResidenceインスタンスを代入することができます。

john.residence = Residence()

john.residenceは今nilではなくResidenceインスタンスを保持しています。先ほどと同じようにオプショナルチェイニングによりnumberOfRoomsにアクセスしようとした場合、numberOfRoomsの初期値である1を保持しているInt?型の値(つまりOptional(1))を返します。

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."

オプショナルチェイニングのモデルのためのクラスを定義する

オプショナルチェイニングを使ってもっと深いレベルのプロパティ・メソッド・サブスクリプトへアクセスすることもできます。

例えばjohn.residence?.furniture.colorとか。

相互に関連するタイプの複雑なモデル内のサブプロパティにドリルダウンし、それらのサブプロパティのプロパティ、メソッド、およびサブスクリプトにアクセスできるかどうかを確認できます。

次のコードはこの後のサンプルで使用する四つのクラスを定義します。Room クラスとAddressクラス、それに付随するプロパティ、メソッド、およびサブスクリプトを加えて、先ほど示した「PersonとResidenceクラスのモデル」を拡張します。

Personクラスは先ほどの例と同じです。

class Person {
    var residence: Residence?
}

Residenceクラスは先ほどより複雑になります。今回、Residenceクラスに[Room]型の空の配列で初期化したroomsという名前の変数のプロパティを宣言します。

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クラスはRoomインスタンスを要素に持つ配列をプロパティとして持つので、numberOfRoomsプロパティはストアドプロパティではなく、コンピューテドプロパティとして定義されています。numberOfRoomsは、配列roomsのcountプロパティを返すだけのシンプルなプロパティです。

配列roomsへアクセスするショートカットとして、このバージョンのResidenceクラスには、インデックスを指定して配列roomsの要素へアクセスする読み書き可能なサブスクリプトを用意しています。

さらにResidenceクラスはprintNumberOfRoomsメソッドも用意しています。このメソッドはシンプルにコンピューテドプロパティnumberOfRoomsの値を表示します。

最後にResidenceクラスはオプショナル型のプロパティaddressを定義しています。Addressクラスは後で定義します。

配列roomsに格納される要素のクラスであるRoomクラスはnameプロパティと部屋名を初期化するイニシャライザを持つシンプルなクラスです。

class Room {
    let name: String
    init(name: String) { self.name = name }
}

最後のクラスがAddressクラスです。このクラスは三つのString?型のプロパティ

「buildingName」

「buildingNumber」

「street」

を持ちます。住所の一部分として「buildingName」と「buildingNumber」のどちらかで特定の建物を識別します。三つ目のプロパティ「street」は住所の通りの名前です。

class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if let buildingNumber = buildingNumber, let street = street {
            return "\(buildingNumber) \(street)"
        } else if buildingName != nil {
            return buildingName
        } else {
            return nil
        }
    }
}

AddressクラスにはString?型を返すbuildingIdentifier()メソッドも用意されています。このメソッドの挙動、

(1)buildingNumberとstreet、両方が値を持つ場合は、両方を繋げた文字列を返す。

(2)(1)に当てはまらず、buildingNameがnilでない場合はbuildingNameを返す。

(3)(2)に当てはまらない場合はnilを返す。

オプショナルチェイニングを用いてプロパティにアクセスする

「強制アンラップの代わりとしてのオプショナルチェイニング」で示したように、オプショナル型に値であるインスタンスのプロパティにアクセスするためにオプショナルチェイニングを使うことができ、さらにオプショナルチェイニングによるアクセスが成功したかをチェックすることができます。

Personクラスのインスタンスを生成するために先ほど定義したクラスを使用し、先ほどと同じようにnumberOfRoomsプロパティにアクセスを試みます。

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.")
}
// Prints "Unable to retrieve the number of rooms."

john.residenceはnilですから、先ほどと同じようにオプショナルチェイニングによるアクセスは失敗します。

オプショナルチェイニングを利用してプロパティに値をセットすることもできます。

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

この例では、john.residence?.addressに「AddressクラスのインスタンスsomeAddress」をセットしようとしてますが、失敗します。なぜならjohn.residenceがnilだからです。

代入はオプショナルチェイニングの一部なので、=演算子の右側のコードは評価されません。(オプショナルチェイニングの一部として代入操作があるので、オプショナルチェイニング失敗=代入操作の一部である右辺の評価もストップする、みたいなこと。)「定数へアクセス」しても特に何も起こらないので、上記のサンプルでは、「someAddressが評価されていない」ことを確認することができません。下のサンプルのようにAddressクラスのインスタンスを作る関数createAddressを定義して利用すれば、メッセージが表示されたら=演算子の右辺が評価されたことが確認できる。

func createAddress() -> Address {
    print("Function was called.")

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"

    return someAddress
}
john.residence?.address = createAddress()

“Function was called.”のメッセージは表示されませんので、右辺は評価されていないことが確認できます。

オプショナルチェイニングでメソッドを呼び出す

オプショナルチェイニングを利用してオプショナル型のインスタンスのメソッドを呼び出すことができます。さらにメソッド呼び出しが成功したか確認することができます。メソッドが全く値を返さないメソッドでも確認することができます。

ResidenceクラスprintNumberOfRooms()メソッドはnumberOfRoomsの値を表示します。

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

このメソッドは返り値がありません。しかしFunctions Without Return Values.で説明されているように、返り値のない関数やメソッドは暗黙的にVoid型を返します。Void型は(),あるいは「空のタプル」です。

オプショナルチェイニングを利用した呼び出しの結果は常にオプショナル型ですので、オプショナル型のインスタンスのこのメソッド(printNumberOfRooms)をオプショナルチェイニングを利用して呼び出すと返り値のは(Void型ではなく)Void?型です。ということで、printNumberOfRooms()内で返り値を定義していなくても、if文を用いて、printNumberOfRooms()メソッドの呼び出しが成功したかを確認することができます。printNumberOfRooms()メソッドの返り値をnilと比較することで、メソッド呼び出しが成功したか確認できます。

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.")
}
// Prints "It was not possible to print the number of rooms."

結局上記サンプルではjohn.residence?.printNumberOfRooms()はnilを返す(residenceがnilだから。)。結局プロパティでもメソッドでも呼び出し失敗(上記サンプルで言えばresidenceがnil)の場合、オプショナルチェイニングが返すのはnil。

オプショナルチェイニングを利用してプロパティに値をセットする場合も同じです。上のAccessing Properties Through Optional Chainingの例では、john.residenceがnilであるにも関わらず、john.residenceのaddressプロパティにインスタンスをセットしようとしました。オプショナルチェイニングを利用したプロパティへの値のセットはVoid?型を返します。ですから以下のようにnilと比較することにより値のセットが成功したかを確認できます。

if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}
// Prints "It was not possible to set the address."

オプショナルチェイニングを用いたサブスクリプとへのアクセス

オプショナル型のインスタンスのサブスクリプトによる値のセットと値の取得にオプショナルチェイニングを利用することができます。そしてサブスクリプとによる呼び出しが成功したかを確認することができます。

NOTE

?は必ずサブスクリプションのブラケット(例えば[0])の前(後ろではなく前!)に記述します。

下のサンプルは、Residenceクラスに定義されているサブスクリプションを使って、配列roomsの最初の要素のnameプロパティを取得しようとしています。現時点でjohn.residenceはnilなので、サブスクリプションによる呼び出しは失敗します。

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// Prints "Unable to retrieve the first room name."

オプショナルチェイニングで取得しようとしているオプショナル型のインスタンスはjohn.residenceですから、クエスチョンマークはjohn.residenceの直後に記述します。(サブスクリプとのブラケット([0])の前)

オプショナルチェイニングを利用してサブスクリプションにより新しい値をセットする場合も同様です。

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

john.residenceはnilですから、上記の値のセットも失敗します。

 


john.residenceにResidence型のインスタンスを生成・代入して、配列roomsに一つ以上のRoom型のインスタンスをセットすれば、オプショナルチェイニングを利用して配列roomsの実際の要素に(サブスクリプションにより)アクセスすることができます。

let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// Prints "The first room name is Living Room."

 

 

 

参考

https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html

コメントを残す

メールアドレスが公開されることはありません。