自宅でのSwift 2。 別のJSONパーサー

数か月前、Appleは新しい頭脳のスウィフト2のメジャーアップデートをリリースしました。これはアップルウェイと呼ばれ、「すべてが非常に優れているので、何も考えずに使用する」という方法ではなく、別の方法でリリースしました。 「私たちはそれが良いことを知っていますが、以前はもっと悪かったので、すべてを捨ててから使い始めてください」-つまり、後方互換性に顕著な問題がある言語-通常の移行ツールが非常に不安定に動作するという事実から始まり、実際には、開発者のすべてのツールをまだ非常に安定していない最新バージョンに更新しない限り、新しいバージョンの言語で開発することは絶対にできません。 しかし、これはそれについてではありません。 Swift 2が好きなのは事実です。残念ながら、この言語はまだコミュニティで本番環境として認識されていないため、ほとんどの資料は2つのカテゴリに分類できます。Appleのドキュメントと「それは私にとってクールでした。」 もちろん例外もありますが、それだけでは十分ではないので、この言語を使って作業するという観点から、この言語を少し強調してみます。

この記事では、標準のORM JSONデシリアライゼーションタスク、つまり[String:AnyObject]ディクショナリのNSDictionaryオブジェクトからデシリアライズされた構造を取得する方法について説明します。 Swift 2の登場で何が変わったのですか? 以前はどうでしたか? さらに、この問題は、たとえば一度作成されたデータの耐性など、特定の制限を課すほぼ機能的なアプローチの観点から検討します。 したがって、問題の解決策は他の解決策よりもやや複雑かもしれませんが、大丈夫です。

画像










それでは、デシリアライザの要件は何ですか?



Swift 1には、JSONオブジェクトを操作するための次のアプローチがあります。



だから、私たちが取り組んでいるものから始めましょう:

まず、プロトコルを導入します。これは、JSONオブジェクトからこのオブジェクトを取得する機能を示します。



プロトコルDeserializeable {
     func decode()-> Self?
 }


ここでもう1つ小さな余談をしたいと思います。

タイプセーフなコードを使用したいので、一般的にはタイプを実際に保存しません

そして実際のコードでは、人の年齢に対応するIntを区別するのがいいでしょう

およびInt、オーバーボードの温度に対応-したがって、回避するために( 「プリミティブに執着する」

また、たとえば年齢を負にすることはできないなど、タイプレベルでドメイン制限を導入できます。

したがって、コードでは、スカラー型のラッパーは何らかの形式で表示されます



 struct NamedInt {
     let値:Int
 }
 struct NamedString {
     let値:文字列
 }

 struct LeafObject {
     BunchOfIntsを許可します:[NamedInt]
     optionalString:NamedString?
     let someDouble:Double
 }

 struct RootObject {
    残す:[LeafObject]
 }


このために、次の辞書をデシリアライズします。



 [「子供」:
     [
         [
             「Ints」:[1、2、3]、
             「ストリング」:「superString」、
            ダブル:1.25
         ]、
         [
             「Ints」:[]、
            ダブル:2.5
         ]
     ]
 ]


後で説明する完全なコードは、 Githubで見つけることができます:JSON_1.playground。 コンパイルして動作させるために-Xcode 7ですべて同じように開く価値がありますが、完全にSwift 1.2のイデオロギーで書かれています-それで動作します。



したがって、このコードの主な側面を分析します。



カレー



理由:上記の例では、コードには前述の制限が1つあります。この記事のフレームワークで使用するすべての構造は不変であり、言語レベルでは構造のユーザーの一部が潜在的に解決できる問題を解決します。誰かに置き換えられたという事実のために、モデルから得られたデータを操作していません。 一部の人にとっては、このアプローチは不要に思えるかもしれません。 そして、一般に、それは公平です-クラスとは異なり、構造は「値で」転送されます-つまり、この構造がモデルから転送されるとき-コピーされます-追加のメモリとプロセッサ時間につながります。 データ構造の1つのフィールドを変更することも、新しいインスタンスを作成するよりも明らかに速いですが、それにもかかわらず、長期的な妥協は正当であると見なされます。



いずれにせよ、これは意識的な選択であるため、この仮定の下でデシリアライズインフラストラクチャを構築しています。

したがって、不変オブジェクトがあります。 Swiftはどのようなオペレーションを提供しますか? フィールドゲッターに加えて、Swiftはデフォルトのコンストラクターを提供します。これには、作成される構造体のすべてのフィールドが渡されます。

各フィールドには明確で厳密なタイプがあり、これは一方では非常に優れています-間違ったフィールドに間違ったオブジェクトを配置することさえ物理的にできないためです-一方では-ある段階で逆シリアル化が機能しなかった場合(たとえば、ソース辞書には目的のキーを持つフィールドがありません)-プロセスを中断する必要があります。



そして、ここでカレーが助けになります。



ファンクカレー<A、B、C、R>(f:(A、B、C)-> R)-> A-> B-> C-> R {
     {a in {b in {c in {c in f(a、b、c)}}}を返す
 }


LeafObjectコンストラクター(BunchOfInts:[NamedInt]、optionalString:NamedString?、someDouble:Double)を関数のチェーン[NamedInt]->(NamedString?->(Double-> LeafObject))に変換します。



オプションの部分適用



実際、逆シリアル化の2番目の部分は、特定の段階で成功しなかった場合のプロセスの中断です。 ここでは、次のように定義された<*>演算子を使用します。



 func <*> <A、B>(f:(A-> B)?、x:A?)-> B?  {
     f1 = fとすると、x1 = x {
         return f1(x1)
     }
    ゼロを返す
 }


彼は何をしていますか? 彼は、表記法f <*> xは、表記法f(x)と同等であると言っています。 なぜある意味で? f(x)とは異なり-何らかの理由でx(またはf)がnilの場合、式全体の結果もnilになりますが、関数fは呼び出されません。

つまり、関数f(x、y、z)がある場合、カリー化をx-> y-> z-> fに変換し、次のように計算できます。

カレー(f)<*> x <*> y <*> z。 これにより何が得られますか? たとえば、ある段階で(たとえばyを計算するときに)エラーが発生し、yがnilである場合、式(z)の残りの部分は計算されず、さらに、これらすべてが実際に発生することはありません。関数f自体。



同様に、演算子<?>を定義できます。nilは有効な値です



適用する



これはすでに構文糖に起因する可能性があります。これは、記述されたものの宣言性を高めるために必要であり、また、モナドバインディングと呼ばれるもので作業を簡素化するために必要です-関数のグループとして働くための単一のルール

apply >>> =演算子は、構成f(x)とx >>> = fをほぼ同等にします。 バインディングとモナドはそれと何の関係がありますか? そして、このために、多分モナドについて、または少し広い解釈について読むことができます-結果、例えば、 ここで読むことができます

実際、この演算子を使用すると、各関数は単純な入力パラメーターを処理できますが、チェーン内の次の関数に必要な情報より多くの情報を提供できます(エラー)。 そして、このエラーの場合-式の残りの部分を計算する必要なくトップに上がります



したがって、1つの特定の構造を埋める例を見てみましょう



 curriedConstructorを返します
     <*> dict >>> = objectForKey( "ints")>>> = asArray >>> = decodeArray
     <?> dict >>> = objectForKey( "string")>>> = NamedString.decode
     <*> dict >>> = objectForKey( "double")>>> = Double.decode




計算された引数の一貫した適用が確認され、一部の引数が計算できなかった場合、チェーン全体が中断されることがわかりました。



このコードにはいくつかの未選択のポイントがありますが、githubにあるコードからそれらは非常に明確であると思うので、あまり詳しくは述べません。

したがって、このようにして、Swift 1.2の観点からの逆シリアル化の例があります。

その利点は何ですか?



それで、なぜあなたは本当にそれを作り直したいのですか?



これらの欠点を取り除きましょう



So Swift 2



Swift 2でこのコードを書き換えた後の最終ファイルは、 Github :JSON_2.playgroundにあります。 理解できるように、少なくともXcode 7ベータ6では、正常に動作させるために開く必要があります。 (トーゴのマップ関数のシグネチャが追加されたのはベータ6でした。これにより、スローパラメータを使用できるようになりました。したがって、ベータ5でこのコードを実行するには、この関数を自分で記述する必要があります)



swift 2がもたらした最初の主要な変更の1つは、例外のようなエラー処理です。 (まだこのトピックに飛び込んでいない人のために、例えばここを読むことができます )。 このアプローチにはどのような有用な特性がありますか?

要するに、これは非常に使いやすいモナドです。 そして、もう少し詳しく説明すると、言語が提供する操作の結果がいくつかあります。



連鎖



もしあれば



 func foo(a:Int)throws-> Int
 func bar(b:Int)throws-> Int


その後、コードの代わりに



 {
     let q = fooを試す(1)
     let b =バーを試す(q)
 } {}をキャッチ


次のように書くことができます



 {
     let q = try bar(foo(1))
 } {}をキャッチ


さらに、複雑さに関係なく、1回の操作で正確に1回の試行を使用できます。



再スロー



さらに、常に砂糖を必要とするわけではありません... catch-関数がエラー処理を伴わず、内部関数のエラーをさらに上に渡し、独自のエラーをスローする場合



 func foo(a:Int)throws-> Int {
    トライバーを返す(a + 1)
 }


エラーのある機能フロー



throwはreturnとほぼ同じ意味を持ちます。つまり、そのような計画の関数を書くことができます。



 func foo(a:Int)throws-> Int {
     a> 0の場合{
        返す
     } else {
         someErrorを投げる
     }
 }


つまり、リターンを通過しない実行パスがある関数



はい、これは非常に使いやすいモナドです。



それと働くことはまさに絵のように起こります

画像

つまり、1と2の結果として関数呼び出しをチェーンに連結することにより(各中間段階でのエラー処理を気にしないかもしれません)、後者の結果のみを処理できます。 さらに、緑のトラックの成功から赤のトラックのエラーへの移行は最小限の労力で実行されます-スローがどこか(おそらく深く深い)で機能することで十分



だから。 これらの利点を活用して、今後のデシリアライズの実行方法を定式化しようとします。



LeafObjectコンストラクターがあります(BunchOfInts:[NamedInt]、optionalString:NamedString?、someDouble:Double)

では、なぜエンティティにスローを投入しないのですか?



 LeafObject(
     BunchOfInts:getIntsを試してください。
     optionalString:getStringを試してください、
     someDouble:getDoubleを試してください
 )


さらに、上記のように-そのような試みを引き出すことができます



 LeafObject(
     BunchOfInts:getInts、
     optionalString:getString、
     someDouble:getDouble
 )


この場合、計算の段階に関係なく、エラーが発生します-オブジェクトの構築プロセスは停止します。

さて、コードでこれにアクセスしてみましょう。

これを行うには、名前から2つの補助クラス、DictionaryDecoderとArrayDecoderが必要です。一般に、それらが何をするのかは明らかです-デコード用のヘルパークラスのようなものです。 それでは、完全にシンプルなものから始めましょう。 try / catchなし-辞書から値を取得するだけです



 struct DictionaryDecoder {
     private let dict:[文字列:AnyObject]
     init?(_値:AnyObject){
         let dict = value as?  [文字列:AnyObject] {
             self.dict = dict
         } else {
            ゼロを返す
         }
     }
    
     func decode <T:Deserializeable>(forKey key:String)-> T?  {
         T.decode(self.resultForKey(キー))を返します
     }

     private func resultForKey(key:String)-> AnyObject?  {
         self.dictを返す[キー]
     }
 }


ここで、私たちに合わないものを見てみましょう。

  1. decodeはタイプTのオブジェクトを返し、デコードに失敗した場合はnilを返しました。 しかし、出力でTを取得したい場合はどうでしょうか?
  2. オブジェクトのデコードに失敗しました-なぜですか?
  3. DictionaryDecoderの入り口で、無効な辞書を提出しました。 たとえば、デコードされたディクショナリの一部のノードはディクショナリであることが判明し、配列であることが判明します。 またはスカラー値。


throwの明示的な候補のように見えますが、このためにDeserializeableプロトコルをわずかに変更する必要があります。



プロトコルDeserializeable {
     func decode()throws-> Self
 }


そして、DictionaryDecoderに適切な変更を加えます



 struct DictionaryDecoder {
     private let dict:[文字列:AnyObject]
     init(_ value:AnyObject)throws {
        ガードlet dict = value as?  [文字列:AnyObject] else {NotADictErrorをスロー(/ *いくつかの詳細* /)}
         self.dict = dict
     }

     func decode <T:Deserializeable>(forKey key:String)throws-> T {
         T.decode(self.resultForKey(キー))を試してください
     }

     func decode <T:Deserialize>(forKey key:String)throws-> T?  {
         return self.optionalForKey(キー).map(T.decodeJSON)
     }

     private func resultForKey(key:String)throws-> AnyObject {
        ガードlet値= self.dict [key] else {スローKeyMissingError(/ *いくつかの詳細* /)}
        戻り値
     }

     private func optionalForKey(key:String)-> AnyObject?  {
         self.dictを返す[キー]
     }
 }


さらに、以下を含むアトミックエンティティの逆シリアル化



プロトコルScalarDeserializeable:デシリアライズ可能{}
拡張ScalarDeserializeable {
     static func decode(入力:AnyObject)throws-> Self {
        ガードlet値=入力として? それ以外の場合{throw UnexpectedTypeError(/ <em>一部の詳細</ em> /)}
        戻り値
     }
 }

拡張機能Int:ScalarDeserializeable {}
拡張文字列:ScalarDeserializeable {}
拡張機能Double:ScalarDeserializeable {}


上記は、それぞれの具体的な値がスカラーである単純な階層型辞書を逆シリアル化するのに十分です。

次に、先ほど書いたものの重要な機能に注目しましょう。

  1. 最後の部分-ScalarDeserializeableでは、Swift 2のもう1つの重要な革新をフックしました-ミックスインは、プロトコル拡張機能であり、プロトコルメソッドの既定の実装を提供できます。 つまり、このようなコードを記述した後、decodeメソッドを自動的に追加し、スカラークラスを使用してDeserializeableプロトコルに準拠します。 Swift 2について彼らが言うところであればどこでもこれについてもっと読むことができます:-)。 しかし、例えば、 ここで

  2. その前に-デコーダーで-スローのために、オプションのタイプと非オプションのタイプの区別を単純に追加することができます-オプションのパラメーターを解析したい場合は、このキーで何も見つからず、この静脈では、デシリアライズできませんでした。 (resultForKeyの代わりにoptionalForKeyを使用すると、エラーが1つ少なくなります)

  3. 上記のように、トライチェーンを使用しました

    T.decode(self.resultForKey(key))コンストラクトの戻りで、decodeとresultForKeyの両方のメソッドがエラーで失敗する場合がありますが、内部エラーが終了すると、デコードも開始されません。



だから。 最後のステップに進み、エラーに対処しましょう。前のコードのUnexpectedTypeError、KeyMissingError、NotADictErrorは、まだ非常に情報価値がありません。

これを行うために、エラーを説明する2つのタイプを紹介します。



  1.  列挙型SchemeMismatchError {
        ケースNotADict
        ケースKeyMissing
        ケースUnexpectedType(expectedTypeName:String)
     }
    


どのエラーが発生したかをそれ自体に保存します

  1.   struct DecodingError:ErrorType {
         let let:SchemeMismatchError
         let reason:String
         letパス:[文字列]
     }
    


ErrorType-Swift 2の別のイノベーション-プロトコル、継承元-このタイプのオブジェクトをスローで使用する機会を得る(そしてそれらのみ)

この構造では、実際に発生したエラーと特定のエラーへのパスを格納するString配列が表示されます。これにより、ローカライズが容易になります(すべてが実際に開始されました)。 見てわかるように、これも許可されています。ここでは、データの耐性のために講じられたコースから逸脱しないためです。



一般に、上記のコードですべてのスローが置き換えられるのは明らかです-throw DecodingError(エラー:.NotADict、理由:“ expected dictionary”、パス:[])、またはいくつかの省略形のような構造

しかし、今度はパスを順番に入力する必要があります。 これを取得する方法を理解するために、ディクショナリの逆シリアル化の開始から特定の値の逆シリアル化までのステップを書き留めてみましょう。



DictionaryDecoder.init-> resultForKey-> decode-> ...-> resultForKey-> decode


つまり、実際には、1回の呼び出しで再帰的に適用されるのは3つの異なるステップだけです。したがって、エラーが発生したレベルに関係なく、指定されたチェーンに沿って反対方向に上昇し、このチェーンの段階でパスに値を1つ追加するだけです。まったく?いいえ、十分です、デコードレベルでのみ-特定の値までの辞書階層の各レベルで、1つだけのデコードがあります(レベルを下げるため)。これをどうやってやるの?これを行うには、実際にcatchを記述し、デコードは次のようになります。



拡張DecodingError {
    func errorByAppendingPathComponent(コンポーネント:文字列)-> DecodingError {
        DecodingErrorを返します(エラー:エラー、理由:理由、パス:[コンポーネント] +パス)
     }
 }

func decode <T:Deserializeable>(forKey key:String)throws-> T {
    {
        T.decode(self.resultForKey(キー))を試してください
    } letエラーをDecodingErrorとしてキャッチ{
        throw error.errorByAppendingPathComponent(キー)
     }
 }


catchではすべてのエラーを処理するのではなく、DecodingErrorのみを処理することに注意してください。 なんで?

  1. コードが他のエラーを送信しないことがわかっているためです。
  2. コードが他のエラーを送信したとしても-この段階でエラーは失われませんが-さらに先へ進むからです。

    実際、現時点では、スローされたエラーを厳密に類型化する方法がないことは少し残念ですが、今のところこれで生きています。


これで十分な機能が得られ、エッセンスの逆シリアル化は次のようになりました。



拡張LeafObject:デシリアライズ可能{
    static func decode(入力:AnyObject)throws-> LeafObject {
        let dict = JSONDictionaryを試す(入力)
        try LeafObject(
            BunchOfInts:dict.decode(forKey: "ints")、
            optionalString:dict.decode(forKey: "string")、
            someDouble:dict.decode(forKey: "double")、
         )
     }
 }


あなたのことは知りませんが、私は_が好きです。それがおそらくすべてです。

プレイグラウンドで指定されたコードは、ここに示されている拡張機能とは少し異なるため、より複雑なエンティティをデシリアライズして再生できます。宿題として、たとえば、いくつかの複雑な構造で構成される列挙型の逆シリアル化を試してください。たとえば:



列挙グラフ{
    ケースツリー(RootObject)
    ケースフォレスト([RootObject])
    case SimpleNode(LeafObject)
 }


それで、結論として、私たちは何に来ましたか:

  1. try/catch — — Swift 1.2 — :-)
  2. , , — - . , , — .
  3. Swift — , ,


PSあなたがこの場所を読んでいるなら、私の友人であるiOS開発者たち-恐れずに-Swiftに行ってください。(宿題であっても)アプリケーションのメイン言語として、本当に注目に値します。私はまだ待っているので、Objective-Cプロジェクトの一部を実際に必要とするのを待つことはできません(サードパーティの依存関係ではなく、その大部分は単に拒否するのは馬鹿げています。つまり、日々サポートする必要がある内部コードベースの大部分です) 。簡潔でコンパクトでエレガントです。さて、これで十分です。



All Articles