Swift初期化子/オブジェクトパートナーの探索

この記事では、Swift初期化子のざらざらした詳細をいくつか紹介します。 なぜそのような刺激的なトピックで取るか。 主に、UIViewのサブクラスを作成しようとしたときや、古いObjective-CクラスのSwiftバージョンを作成しようとしたときに、コンパイルエラーによって混乱することがあ そこで、Swift初期化子のニュアンスをより深く理解するために、もっと深く掘り下げてみることにしました。 私の背景/経験は主にGroovy、Java、およびJavascriptでの作業に関与しているため、Swiftの初期化メカニズムのいくつかをJava&Groovyのものと比較します。 XCodeでPlaygroundを起動し、コード例を自分でプレイすることをお勧めします。 また、この記事の主な情報源であるSwift2.1言語ガイドの初期化セクションを読むことをお勧めします。 最後に、値型(構造体)ではなく参照型(クラス)に焦点を当てます。

初期化子とは何ですか?

初期化子は、Swiftの構造体、クラス、または列挙型の特別なタイプのメソッドで、構造体、クラス、または列挙型の新しく作成されたインスタンスが使用される準備ができている前に完全に初期化されていることを確認する責任があります。 これらは、JavaやGroovyの”コンストラクタ”の役割と同じ役割を果たします。 Objective-Cに精通している場合は、Swift初期化子は値を返さないという点でObjective-C初期化子とは異なることに注意してください。

サブクラスは一般的に初期化子を継承しません

最初に留意すべきことの一つは、言語ガイドによると、”Swiftサブクラスはデフォルトでスーパークラスの初期化子を継承しない”ということです。 (このガイドでは、スーパークラス初期化子が自動的に継承されるシナリオがあることを説明しています。 これらの例外的なシナリオについては、後で説明します)。 これは、Java(および拡張機能ではGroovy)の動作と一致しています。 次の点を考慮してください:Java&Groovyと同様に、これは許可されていないことは理にかなっています(ほとんどの場合と同様に、この点についていくつかの議論があるかもしれません。 このStackOverflowの投稿を参照してください)。 許可されている場合、Musicianの”instrument”プロパティの初期化はバイパスされ、Musicianインスタンスが無効な状態になる可能性があります。 しかし、Groovyでは、通常、初期化子(つまりコンストラクタ)を書くことは気にしません。 むしろ、Groovyが暗黙的に提供するマップコンストラクタを使用するだけで、構築時に設定するプロパティを自由に選択して選択することができます。 たとえば、次は完全に有効なGroovyコードです。

スーパークラスによって提供されるプロパティを含む任意のプロパティを含めることができますが、それらのすべ この種の超柔軟な初期化子はSwiftでは提供されていません。 Swiftで最も近いのは、構造体のための自動的に提供されるmemberwise初期化子です。 しかし、メンバワイズ初期化子の引数の順序は、名前が付けられていても重要であり、定義されている順序に依存します。
とにかく、クラスに戻る–構築後のオブジェクトの妥当性に関するGroovyの哲学は、明らかにSwiftとは非常に異なっています。これは、GroovyがSwiftと異なる多くの方法の一つに過ぎません。

ウサギの穴をあまりにも遠くに行く前に、重要な概念を明確にする必要があります: Swiftでは、初期化子は、指定された初期化子または便利な初期化子のいずれかに分類されます。 私は概念的にも構文的にも違いを説明しようとします。 各クラスは、少なくとも1つの指定された初期化子を持たなければなりませんが、複数を持つことができます(「指定された」は「単一」を意味しません)。 指定された初期化子は、プライマリ初期化子と見なされます。 彼らは頭のホンチョです。 最終的には、すべてのプロパティが初期化されていることを確認する責任があります。 その責任のために、指定された初期化子を常に使用することは、いくつかの引数を必要とする可能性があるため、時には苦痛になることがあります。 いくつかのプロパティを持つクラスを操作すると、1つまたは2つのプロパティを除いてほぼ同じクラスのいくつかのインスタンスを作成する必 (引数のために、プロパティが宣言されたときにプロパティに割り当てられた可能性のある賢明なデフォルトがないと仮定しましょう)。 たとえば、PersonクラスにもeatsFoodプロパティとenjoysMusicプロパティがあるとします。 もちろん、これら2つのことはほとんどの場合真実に設定されますが、あなたは決して知りません。 見てみましょう:

今、私たちのPersonクラスには設定する必要がある4つのプロパティがあり、ジョブを実行できる指定された初期化子があります。 指定された初期化子は、最初のもので、4つの引数を取るものです。 ほとんどの場合、これらの最後の2つの引数は値”true”になります。 私たちが典型的な人を作りたいと思うたびに、それらを指定し続けなければならないのは苦痛です。 ここで、最後の2つの初期化子、convenience修飾子でマークされた初期化子が入ります。 このパターンは、Java開発者にはよく知られているはずです。 常に実際に処理する必要があるよりも多くの引数を取るコンストラクタがある場合は、それらの引数のサブセットを取り、他の引数に適切なデフ 便利な初期化子は、別の、おそらくあまり便利ではない便利な初期化子または指定された初期化子のいずれかに委任する必要があります。 最終的には、指定された初期化子が関与する必要があります。 さらに、これがサブクラスの場合、指定された初期化子は、その即時スーパークラスから指定された初期化子を呼び出す必要があります。

convenience修飾子を使用するための実際の例の1つは、UIKitのUIBezierPathクラスから来ています。 私はあなたがパスを指定するいくつかの方法があると想像できると確信しています。 そのため、UIBezierPathにはいくつかの便利な初期化子が用意されています。

public convenience init(rect:CGRect)
public convenience init(ovalInRect rect:CGRect)
public convenience init(roundedRect rect:CGRect,cornerRadius:CGFloat)
public convenience init(roundedRect rect:cgrect,byRoundingCorners corners:UIRectCorner,cornerRadii:CGSize)<3491>公共の利便性(アークセンターセンター): Cgpoint,radius:CGFloat,startAngle:cgfloat,endAngle:CGFloat,clockwrite:Bool)
public convenience init(CGPath:CGPath)

以前、クラスには複数の指定された初期化子がある可能性があると述べました。 それで、それはどのように見えるのですか? 指定された初期化子と便利な初期化子の間のコンパイラによって強制される重要な違いの1つは、指定された初期化子が同じクラス内の別の初期化子に委譲することができないことです(ただし、その即時スーパークラス内の指定された初期化子に委譲する必要があります)。 Personの第二の初期化子、”unDead”という名前の単一の引数を取るものを見てください。 この初期化子はconvenience修飾子でマークされていないため、Swiftは指定された初期化子として扱います。 そのため、別の初期化子に直接委譲することはできません。 最初の4行をコメントアウトし、最後の行をコメント解除してみてください。 コンパイラは文句を言うでしょう、そしてXCodeはあなたが便利な修飾子を追加することによってそれを修正することを提案することによってあな

今、人のミュージシャンのサブクラスを考えてみましょう。 これは単一の初期化子を持っているため、指定された初期化子でなければなりません。 そのため、直接のスーパークラスPersonの指定された初期化子を呼び出す必要があります。 注意:指定された初期化子は同じクラスの別の初期化子に委譲することはできませんが、便利な初期化子はそうする必要があります。 また、指定された初期化子は、その即時スーパークラスの指定された初期化子を呼び出す必要があります。 詳細(およびかなりのグラフィック)については、言語ガイドを参照してくださ

初期化フェーズ

Swift言語ガイドで説明されているように、2つの初期化フェーズがあります。 フェーズは、初期化子として指定されたスーパークラスへの呼び出しによって境界が設定されます。 フェーズ1は初期化子として指定されたスーパークラスへの呼び出しの前であり、フェーズ2は後です。 サブクラスはフェーズ1ですべてのプロパティを初期化する必要があり、フェーズ2まではスーパークラスで定義されたプロパティを設定できません。
ここでは、指定された初期化子スーパークラスを呼び出す前に、サブクラスの独自のプロパティを初期化する必要があることを示す、言語ガイドで提供されているサンプルから適応されたコードサンプルを示します。 スーパークラスによって指定された初期化子を呼び出した後まで、スーパークラスによって提供されるプロパティにアクセスすることはできません。 最後に、スーパークラス指定された初期化子が呼び出された後で、定数格納されたプロパティを変更することはできません。

初期化子のオーバーライド

サブクラスは一般的に初期化子を継承しないと確信しており、指定された初期化子と便利な初期化子の意味と区別が明確になっているので、サブクラスが即時のスーパークラスから初期化子をオーバーライドするときに何が起こるかを考えてみましょう。 初期化子には2つのタイプがあることを考えると、私がカバーしたい4つのシナリオがあります。 それでは、それぞれのケースのための簡単なコード例で、それらを一つずつ見てみましょう:

スーパークラス指定された初期化子
に一致する指定された初期化子これは典型的なシナリオです。 これを行うときは、override修飾子を適用する必要があります。 このシナリオは、自動的に提供されるデフォルトの初期化子を”オーバーライド”している場合(つまり、スーパークラスが明示的な初期化子を定義していない場合)でも有効であることに注意してください。 この場合、Swiftは暗黙的に1つを提供します。 Java開発者はこの動作に精通している必要があります)。 この自動的に提供されるデフォルトの初期化子は、常に指定された初期化子です。
スーパークラスの便利な初期化子に一致する指定された初期化子
ここで、親の便利な初期化子に一致する指定された初期化子をサブクラスに追加 言語ガイドに記載されている初期化子委任の規則により、サブクラス指定の初期化子は、即時スーパークラスの指定された初期化子まで委任する必要が つまり、親の一致する便利な初期化子に委譲することはできません。 引数のために、subclassがスーパークラス初期化子を継承する資格がないと仮定します。 次に、スーパークラスの便利な初期化子を直接呼び出すことでサブクラスのインスタンスを作成することはできないので、一致する便利な初期化子は したがって、あなたは実際にそれをオーバーライドしていないし、override修飾子は適用されません。

指定されたスーパークラスに一致する便利な初期化子initializer
このシナリオでは、デフォルト値を1つ以上の親クラスプロパティに割り当てられた値から計算できる(ただし、そうである必要はありません)独自のプロパティを追加するサブクラスがあるとします。 また、サブクラスに指定された初期化子を1つだけ持ちたいとします。 親クラスの指定された初期化子のシグネチャと一致する便利な初期化子をサブクラスに追加することができます。 この場合、新しい初期化子には利便性とオーバーライド修飾子の両方が必要になります。 このケースを説明するための有効なコードサンプルは次のとおりです。
スーパークラスに一致する便利な初期化子便利な初期化子
スーパークラスの便利な初期化子のシグネチャと一致する便利な初期化子をサブクラスに追加したい場合は、すぐに進んでください。 上記で説明したように、とにかく便利な初期化子を実際にオーバーライドすることはできません。 したがって、convenience修飾子を含めますが、override修飾子を省略し、他のconvenience初期化子と同じように扱います。

このセクションで重要なのは、スーパークラス指定の初期化子をオーバーライドする場合は、override修飾子のみが使用され、使用する必要があるということです。 (ここでは、必要な初期化子をオーバーライドする場合は、override修飾子の代わりにrequired修飾子を使用します。 必要な修飾子は、override修飾子を意味します。 後述の必要な初期化子のセクションを参照してください)。

初期化子が継承されている場合

スーパークラス初期化子が継承されている前述のシナリオのためになりました。 Swift言語ガイドで説明されているように、サブクラスが宣言時にすべてのプロパティのデフォルト値を提供し、独自の指定された初期化子を定義し または、サブクラスが指定されたすべてのスーパークラスの初期化子の実装を提供する場合、すべてのスーパークラスの便利な初期化子を自動的に継承し これは、クラス(および構造体)の初期化が格納されたプロパティを不確定な状態にしてはならないというSwiftの規則と一致しています。

便利な初期化子、指定された初期化子、および継承ルールを試している間、私はいくつかの”興味深い”動作に遭遇しました。 私はそれが誤って悪循環を設定することが可能であることを発見しました。 次の例を考えてみましょう。

RecipeIngredientクラスは、指定されたFoodクラスのすべての初期化子をオーバーライドするため、すべてのスーパークラスのconvenience初期化子を自 しかし、Food convenience初期化子は、RecipeIngredientサブクラスによってオーバーライドされた独自の指定された初期化子に合理的に委任します。 したがって、呼び出されるのはinit(name:String)初期化子の食品バージョンではなく、recipeingredientのオーバーライドされたバージョンです。 オーバーライドされたバージョンは、サブクラスがFoodのconvenience initializerを継承しているという事実を利用しています。 私はこれがプログラマの間違いかコンパイラのバグとみなされるかどうかはわかりません(私はバグとして報告しました:https://bugs.swift.org/browse/SR-512)。 Foodがサードパーティのクラスであり、ソースコードにアクセスできないため、実際にどのように実装されているかわからないとします。 この例に示されている方法でそれを使用すると、サイクルに閉じ込められることは(実行時まで)わかりません。 だから私はコンパイラがここで私たちを助けた方が良いと思います。

Failable Initializers

特定の不変式を持つクラスを設計し、クラスのインスタンスが作成された瞬間からそれらの不変式を強制したいとします。 たとえば、請求書をモデル化していて、金額が常に負でないことを確認したい場合があります。 Double型のamount引数を取る初期化子を追加した場合、不変式に違反していないことを確認するにはどうすればよいですか? 一つの戦略は、引数が非負であるかどうかを単にチェックすることです。 そうであれば、それを使用してください。 それ以外の場合は、デフォルトは0です。 たとえば、

これは機能し、初期化子が何をしているかを文書化する場合(特に、クラスを他の開発者が利用できるようにする場合)には許容されます。 しかし、それは敷物の下で問題を掃引するようなものなので、あなたはその戦略を守るのに苦労するかもしれません。 Swiftでサポートされている別のアプローチは、初期化を失敗させることです。 つまり、初期化子を失敗可能にすることになります。

この記事で説明しているように、Failable初期化子は、オブジェクトの構築中に”以前は失敗を報告する唯一の方法でした”ファクトリメソッドの必要性を 初期化子を失敗可能にするには、単に?を追加しますか? または! initキーワードの後の文字(つまり、init? またはinit! ). 次に、すべてのプロパティが設定され、委任に関する他のすべてのルールが満たされた後、引数が有効であることを確認するロジックを追加します。 それらが有効でない場合は、return nilで初期化失敗をトリガーします。 これは、初期化子が何かを返していることを意味するものではないことに注意してください。 Invoiceクラスが失敗可能な初期化子でどのように見えるかは次のとおりです。
オブジェクト作成の結果をどのように使用しているかについて何か異な それは我々が右、オプションとしてそれを扱っているようなものですか? まあ、それはまさに私たちがやっていることです! 失敗可能な初期化子を使用すると、nil(初期化失敗がトリガーされた場合)またはOptional(請求書)のいずれかが取得されます。 つまり、初期化が成功した場合、目的のInvoiceインスタンスをラップするOptionalになるため、ラップを解除する必要があります。 (余談ですが、JavaにはJava8以降のオプションもあることに注意してください)。

失敗可能な初期化子は、オーバーライドと委任、指定されたvs利便性などに関して説明した他のタイプの初期化子と同じです…実際には、失敗可能な初期化子 ただし、失敗可能な初期化子では、失敗可能な初期化子をオーバーライドすることはできません。

失敗可能な初期化子がUIViewまたはUIViewControllerを扱うことに気づいたかもしれませんが、どちらも失敗可能な初期化子initを提供しますか?(コーダー aDecoder:NSCoder)。 この初期化子は、ViewまたはViewControllerがnibからロードされたときに呼び出されます。 失敗可能な初期化子がどのように機能するかを理解することが重要です。 詳細な説明については、Swift言語ガイドのFailable Initializersセクションを読むことを強くお勧めします。

Required Initializers

required修飾子は、すべてのサブクラスが影響を受ける初期化子を実装する必要があることを示すために使用されます。 それに直面して、それは非常にシンプルで簡単に聞こえます。 上記で説明した初期化子の継承に関する規則がどのように機能するかを理解していないと、時には少し混乱することがあります。 サブクラスがスーパークラス初期化子を継承する基準を満たしている場合、継承された初期化子のセットには必須とマークされた初期化子が含まれます。 したがって、サブクラスは、必要な修飾子によって課された契約を暗黙的に満たします。 必要な初期化子を実装していますが、ソースコードには表示されません。

サブクラスが必要な初期化子の明示的な(つまり継承されていない)実装を提供する場合、スーパークラスの実装もオーバーライドします。 必要な修飾子はoverrideを意味するため、override修飾子は使用されません。 必要に応じて含めることもできますが、そうすることは冗長になり、XCodeはそれについてあなたに迷惑をかけます。

Swift language guideは必要な修飾子についてあまり言及していないので、コードサンプル(以下を参照)にコメントを用意して、その目的を説明し、その仕組みを説 詳細については、Anthony Levingsによるこの記事を参照してください。

特殊なケース:UIViewの拡張

Swift初期化子を深く掘り下げるように促したことの一つは、初期化ロジックを複製せずに指定された初期化子のセットを作 たとえば、Ray WenderlichによるこのUIViewチュートリアルでは、Objective-CコードをSwiftに変換していました(ここで私のSwiftバージョンを見ることができます)。 そのチュートリアルを見ると、UIViewのRateeviewサブクラスには、指定された初期化子の両方が共通の初期化タスクを実行するために使用するbaseInitメソッドがあるこ それは私にとって良いアプローチのようです–あなたはそれらの初期化子のそれぞれにそのものを複製したくありません。 私はRateviewの私のSwiftバージョンでその技術を再現したかったのです。 しかし、指定された初期化子は同じクラス内の別の初期化子に委譲することができず、スーパークラス初期化子に委譲するまで独自のクラスのメソッ その時点で、定数プロパティを設定するには遅すぎます。 もちろん、定数を使用しないことでこの制限を回避することもできますが、それは良い解決策ではありません。 だから私はそれがちょうど彼らが宣言されている格納されたプロパティのデフォルト値を提供するのが最善だと思いました。 それはまだ私が現在知っている最善の解決策です。 しかし、私は初期化子を使用する別の手法を見つけました。

次の例を見てみましょう。 これはRateViewWithConvenienceInitsからのスニペットです。swiftは、Rateviewの私のSwiftポートの代替バージョンです。 宣言時に格納されているすべてのプロパティのデフォルト値を提供しないUIViewのサブクラスであるため、この代替バージョンのRateViewは、少なくともUIViewの必(coder aDecoder:NSCoder)初期化子。 また、UIViewのinit(frame)の明示的な実装も提供したいと考えています: CGRect)初期化プロセスが一貫していることを確認するための初期化子。 どの初期化子が使用されているかに関係なく、格納されたプロパティを同じ方法で設定したいと考えています。
uiviewの初期化子のオーバーライドされたバージョンにconvenience修飾子を追加したことに注意してください。 私はまた、オーバーライドされた(便利な)初期化子の両方が委任するサブクラスに失敗可能な指定された初期化子を追加しました。 この単一の指定された初期化子は、保存されているすべてのプロパティ(定数を含む–すべてにvarを使用する必要はありませんでした)を設定します。 それは動作しますが、私はそれがかなりkludgyだと思います。 格納されたプロパティが宣言されている場所にデフォルト値を提供するだけですが、必要に応じてこのオプションが存在することを知っておくと

You might also like

コメントを残す

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