見出し画像

Swiftでプログラミング- Initialization 3

Failable Initializers  失敗のあるイニシャライザ

初期化に失敗する可能性のあるクラス、構造体、列挙を定義しておくと便利な場合があります。この失敗は、初期化パラメータの値が無効な場合や、必要な外部リソースが存在しない場合など、初期化が成功しない条件によって引き起こされます。

失敗する可能性のある初期化条件に対処するには、クラス、構造体、または列挙の定義の一部として、1 つまたは複数の失敗可能な初期化子を定義します。失敗可能なイニシャライザを記述するには、initキーワードの後にクエスチョンマークを付けます(init?)

フェイル可能なイニシャライザとフェイル不可能なイニシャライザを、同じパラメータタイプと名前で定義することはできません。

失敗のあるイニシャライザは、初期化する型のoptional valueを作成します。失敗のあるイニシャライザの中では、初期化の失敗(できない)の場合は"return nil"とします。

厳密に言えば、イニシャライザは値を返しません。むしろその役割は、初期化が終了するまでにselfが完全かつ正しく初期化されていることを確認することです。初期化の失敗の場合に return nil と書きますが、初期化の成功をの場合に return キーワードを使うことはありません。

例えば,数値型の変換のために,失敗する初期化器が実装されています。数値型の変換で値を正確に維持するためには、init(actly:)イニシャライザを使用します。型変換で値を維持できない場合は、初期化は失敗します。

    let wholeNumber: Double = 12345.0
   let pi = 3.14159
   if let valueMaintained = Int(exactly: wholeNumber) {
       print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
   }
   // Prints "12345.0 conversion to Int maintains value of 12345"
   let valueChanged = Int(exactly: pi)
   // valueChanged is of type Int?, not Int
   if valueChanged == nil {
       print("\(pi) conversion to Int doesn't maintain value")
   }
   // Prints "3.14159 conversion to Int doesn't maintain value"

以下の例では、Animal という構造体に、species という定数の String プロパティを定義しています。また、Animal構造体は、speciesという1つのパラメータを持つフェイルセーフのイニシャライザを定義しています。このイニシャライザは、イニシャライザに渡されたspeciesの値が空の文字列であるかどうかをチェックします。空の文字列が見つかった場合、初期化の失敗が発生します。それ以外の場合は、speciesプロパティの値が設定され、初期化が成功します。

    struct Animal {
       let species: String
       init?(species: String) {
           if species.isEmpty { return nil }
           self.species = species
       }
   }

このフェイルセーフなイニシャライザを使用して、新しいAnimalインスタンスの初期化を試み、初期化が成功したかどうかをチェックすることができます。

   let someCreature = Animal(species: "Giraffe")
   // someCreature is of type Animal?, not Animal
   if let giraffe = someCreature {
       print("An animal was initialized with a species of \(giraffe.species)")
   }
   // Prints "An animal was initialized with a species of Giraffe"

失敗のあるイニシャライザのspeciesパラメータに空の文字列値を渡すと、イニシャライザは初期化の失敗を引き起こします。

   let anonymousCreature = Animal(species: "")
   // anonymousCreature is of type Animal?, not Animal
   
   if anonymousCreature == nil {
       print("The anonymous creature couldn't be initialized")
   }
   // Prints "The anonymous creature couldn't be initialized"
空の文字列値("Giraffe "ではなく""(空欄)であれば)をチェックすることは、オプションの String 値がないことを示す nil をチェックすることとは異なります。上の例では、空の文字列("")は、オプションではない有効な String です。しかし、動物がそのspeciesプロパティの値として空の文字列を持つことは適切ではありません。この制限をモデル化するために、空の文字列が見つかった場合、失敗のあるイニシャライザは初期化が失敗したとして処理します。

Failable Initializers for Enumerations

enumで失敗のあるイニシャライザを使用すると、1 つまたは複数のパラメータに基づいて適切な列挙ケースを選択できます。イニシャライザは、提供されたパラメータが適切なcaseと一致しない場合に失敗します。

以下の例では、TemperatureUnitというenumを定義し、3つの状態(kelvin、celsius、fahrenheit)を設定しています。温度記号を表すCharacter値の適切なcaseを見つけるために、defaul:tで"return nil"として失敗のあるイニシャライザを使用しています。

    enum TemperatureUnit {
       case kelvin, celsius, fahrenheit
       init?(symbol: Character) {
           switch symbol {
           case "K":
               self = .kelvin
           case "C":
               self = .celsius
           case "F":
               self = .fahrenheit
           default:
               return nil
           }
       }
   }

この失敗のあるイニシャライザを使用して、3つの可能な状態に対して適切なcaseを選択し、パラメータがこれらの状態のいずれかに一致しない場合に初期化が失敗します。

   let fahrenheitUnit = TemperatureUnit(symbol: "F")
   if fahrenheitUnit != nil {
       print("This is a defined temperature unit, so initialization succeeded.")
   }
   // Prints "This is a defined temperature unit, so initialization succeeded."
   let unknownUnit = TemperatureUnit(symbol: "X")
   if unknownUnit == nil {
       print("This isn't a defined temperature unit, so initialization failed.")
   }
   // Prints "This isn't a defined temperature unit, so initialization failed."

Failable Initializers for Enumerations with Raw Values

生の値を持つ列挙は、自動的に init?(rawValue:)という失敗のあるイニシャライザを受け取ります。このイニシャライザは、適切な生の値の型の rawValue を受け取り、一致するcaseがあればそれを選択し、一致する値がなければ初期化に失敗します。

上のTemperatureUnitの例を書き換えて、Character型の生値を使い、init?(rawValue:)イニシャライザを利用することができます

    enum TemperatureUnit: Character {
       case kelvin = "K", celsius = "C", fahrenheit = "F"
   }
   let fahrenheitUnit = TemperatureUnit(rawValue: "F")
   if fahrenheitUnit != nil {
       print("This is a defined temperature unit, so initialization succeeded.")
   }
   // Prints "This is a defined temperature unit, so initialization succeeded."
   let unknownUnit = TemperatureUnit(rawValue: "X")
   if unknownUnit == nil {
       print("This isn't a defined temperature unit, so initialization failed.")
   }
   // Prints "This isn't a defined temperature unit, so initialization failed."

Propagation of Initialization Failure

クラス、構造体、enumの失敗のあるイニシャライザは、同じクラス、構造体、enumの別の失敗のあるイニシャライザにまたがってデリゲートすることができます。同様に、サブクラスのフェイルセーフ・イニシャライザは、スーパークラスのフェイルセーフ・イニシャライザにデリゲートできます。

いずれの場合も、初期化を失敗させる別のイニシャライザにデリゲートした場合、初期化プロセス全体が直ちに失敗し、それ以上の初期化コードは実行されません。

失敗のあるイニシャライザは、失敗のあるイニシャライザにデリゲートすることもできます。この方法は、他の方法では失敗しない既存の初期化プロセスに、失敗の可能性のある状態を追加する必要がある場合に使用します。

以下の例では、Product のサブクラスである CartItem を定義しています。CartItem クラスは、オンライン・ショッピング・カートのアイテムをモデル化します。CartItemは、quantityと呼ばれる保存された定数プロパティを導入し、このプロパティが常に少なくとも1の値を持つようにします。

    class Product {
       let name: String
       init?(name: String) {
           if name.isEmpty { return nil }
           self.name = name
       }
   }
   class CartItem: Product {
       let quantity: Int
       init?(name: String, quantity: Int) {
           if quantity < 1 { return nil }
           self.quantity = quantity
           super.init(name: name)
       }
   }

CartItemのfailableイニシャライザは、1以上の数量値を受け取ったかどうかを検証することから始まります。数量が無効であれば、初期化プロセス全体が直ちに失敗し、それ以上の初期化コードは実行されません。同様に、Product用の失敗可能なイニシャライザはnameの値をチェックし、nameが空の文字列の場合、イニシャライザプロセスは直ちに失敗します。

空でない名前と1以上の数量を持つCartItemインスタンスを作成した場合、初期化は成功します。

    if let twoSocks = CartItem(name: "sock", quantity: 2) {
       print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
   }
   // Prints "Item: sock, quantity: 2"

数量値が0のCartItemインスタンスを作成しようとすると、CartItemイニシャライザは初期化に失敗します。

    if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
       print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
   } else {
       print("Unable to initialize zero shirts")
   }
   // Prints "Unable to initialize zero shirts"

同様に、CartItemインスタンスを空のname値で作成しようとすると、スーパークラスのProductイニシャライザにより初期化が失敗します。

   if let oneUnnamed = CartItem(name: "", quantity: 1) {
       print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
   } else {
       print("Unable to initialize one unnamed product")
   }
   // Prints "Unable to initialize one unnamed product"

Overriding a Failable Initializer

他のイニシャライザと同様に、スーパークラスの失敗のあるイニシャライザをサブクラスでオーバーライドすることができます。また、スーパークラスの失敗のあるイニシャライザをサブクラスの nonfailable イニシャライザでオーバーライドすることもできます。これにより、スーパークラスの初期化が失敗しても、初期化に失敗しないサブクラスを定義することができます。

失敗するスーパークラスのイニシャライザを失敗しないサブクラスのイニシャライザでオーバーライドした場合、スーパークラスのイニシャライザにデリゲートする唯一の方法は、失敗するスーパークラスのイニシャライザの結果を強制的にアンラップすることであることに注意してください。

失敗するイニシャライザを失敗しないイニシャライザでオーバーライドすることはできますが、その逆はできません。

以下の例では、Documentというクラスを定義しています。このクラスは、空ではない文字列値または nil である name プロパティで初期化できるドキュメントをモデルとしていますが、空の文字列にはできません。

    class Document {
       var name: String?
       // this initializer creates a document with a nil name value
       init() {}
       // this initializer creates a document with a nonempty name value
       init?(name: String) {
           if name.isEmpty { return nil }
           self.name = name
       }
   }

次の例では、DocumentのサブクラスであるAutomaticallyNamedDocumentを定義します。AutomaticallyNamedDocumentのサブクラスは、Documentで導入された指定の初期化子を両方ともオーバーライドします。これらのオーバーライドにより、AutomaticallyNamedDocumentインスタンスが、名前なしで初期化された場合や、空の文字列がinit(name:)イニシャライザに渡された場合に、"[Untitled]"という初期の名前の値を持つことが保証されます。

    class AutomaticallyNamedDocument: Document {
       override init() {
           super.init()
           self.name = "[Untitled]"
       }
       override init(name: String) {
           super.init()
           if name.isEmpty {
               self.name = "[Untitled]"
           } else {
               self.name = name
           }
       }
   }

AutomaticallyNamedDocumentは、そのスーパークラスの失敗するinit?(name:)イニシャライザを失敗しないinit(name:)イニシャライザで上書きします。AutomaticallyNamedDocument は、空の文字列の場合にスーパークラスとは異なる方法で対処するので、そのイニシャライザは失敗する必要がなく、代わりにイニシャライザの失敗しないバージョンを提供します。

イニシャライザで強制アンラップを使用すると、サブクラスの失敗しないイニシャライザの実装の一部として、スーパークラスから失敗するイニシャライザを呼び出すことができます。例えば、以下のUntitledDocumentサブクラスは、常に"[Untitled]"という名前で、初期化時にスーパークラスの失敗 init(name:)イニシャライザを使用しています。

    class UntitledDocument: Document {
       override init() {
           super.init(name: "[Untitled]")!
       }
   }

この場合、スーパークラスのinit(name:)イニシャライザが、空の文字列をnameに指定して呼び出されることがあれば、強制的なアンラッピング操作によりランタイムエラーが発生してしまいます。しかし、文字列定数で呼び出されているので、イニシャライザが失敗しないことがわかりますので、この場合はランタイムエラーは発生しません。

The init! Failable Initializer

一般的には、initキーワードの後にクエスチョンマークを置くことで、適切な型のオプションのインスタンスを作成する失敗可能なイニシャライザを定義します(init?)。代わりに、適切な型の暗黙的にアンラップされたオプションのインスタンスを作成する失敗可能なイニシャライザを定義することもできます。これを行うには、initキーワードの後にクエスチョンマークの代わりに感嘆符を置きます(init!)。

init? から init! へ、またはその逆に委譲することができ、また init? initからinit! へのデリゲートも可能ですが、そうすると、init!

Required Initializers

クラスのイニシャライザの定義の前に必要な修飾子を書き、そのクラスのすべてのサブクラスがそのイニシャライザを実装しなければならないことを示します。

    class SomeClass {
       required init() {
           // initializer implementation goes here
       }
   }

また、必須イニシャライザの各サブクラスの実装の前にrequired修飾子を記述し、イニシャライザの要件が連鎖的にさらにサブクラスにも適用されることを示す必要があります。必須の指定イニシャライザをオーバーライドする場合は、override修飾子を記述しません。

    class SomeSubclass: SomeClass {
       required init() {
           // subclass implementation of the required initializer goes here
       }
   }
継承したイニシャライザで要件を満たすことができる場合は、必須のイニシャライザを明示的に実装する必要はありません。

Setting a Default Property Value with a Closure or Function  クロージャや関数でデフォルトのプロパティ値を設定する

保存プロパティのデフォルト値にカスタマイズや設定が必要な場合、クロージャやグローバル関数を使用して、そのプロパティのデフォルト値をカスタマイズすることができます。そのプロパティが属する型の新しいインスタンスが初期化されるたびに、クロージャまたは関数が呼び出され、その戻り値がプロパティのデフォルト値として割り当てられます。

この種のクロージャや関数は、通常、プロパティと同じ型の一時的な値を作成し、その値を仕立てて希望の初期状態を表現し、その一時的な値を返してプロパティのデフォルト値として使用します。

ここでは、プロパティのデフォルト値としてクロージャを使用する方法の概要を説明します。

    class SomeClass {
       let someProperty: SomeType = {
           // create a default value for someProperty inside this closure
           // someValue must be of the same type as SomeType
           return someValue
       }()
   }

クロージャの終わりの中括弧の後には、空の括弧のペアが続いていることに注意してください。これは、Swift にクロージャを直ちに実行するように指示します。これらの括弧を省略した場合、クロージャの戻り値ではなく、クロージャ自体をプロパティに割り当てようとしています。

クロージャを使用してプロパティを初期化する場合、クロージャが実行される時点では、インスタンスの残りの部分はまだ初期化されていないことを覚えておいてください。つまり、プロパティがデフォルト値であっても、クロージャ内から他のプロパティ値にアクセスすることはできません。また、暗黙の自己プロパティを使用したり、インスタンスのメソッドを呼び出したりすることもできません。

以下の例では、Chessboardという構造体を定義しています。この構造体は、チェスというゲームのボードをモデルにしています。チェスは、黒と白のマス目が交互に並んだ8×8のボードで行われます。

このゲームボードを表現するために、Chessboard構造体は、64個のBool値の配列であるboardColorsという1つのプロパティを持っています。配列の値が true のときは黒いマス、false のときは白いマスを表します。配列の最初の項目は、ボードの左上の正方形を表し、配列の最後の項目は、ボードの右下の正方形を表します。

boardColors配列は、クロージャーで初期化され、色の値が設定されます。

    struct Chessboard {
       let boardColors: [Bool] = {
           var temporaryBoard: [Bool] = []
           var isBlack = false
           for i in 1...8 {
               for j in 1...8 {
                   temporaryBoard.append(isBlack)
                   isBlack = !isBlack
               }
               isBlack = !isBlack
           }
           return temporaryBoard
       }()
       func squareIsBlackAt(row: Int, column: Int) -> Bool {
           return boardColors[(row * 8) + column]
       }
   }

チェスボードのインスタンスが生成されるたびに、このクロージャが実行され、boardColorsのデフォルト値が計算されて返されます。上の例のクロージャーでは、ボード上の各マスに適切な色を計算してtemporaryBoardという一時的な配列に設定し、設定が完了したらこの一時的な配列をクロージャーの戻り値として返しています。返された配列の値はboardColorsに格納され、squareIsBlackAt(row:column:)ユーティリティー関数で照会することができます。

   let board = Chessboard()
   print(board.squareIsBlackAt(row: 0, column: 1))
   // Prints "true"
   print(board.squareIsBlackAt(row: 7, column: 7))
   // Prints "false"

この記事が気に入ったらサポートをしてみませんか?