C ++ 11でネットワークゲームのシリアライザーを作成します

ゲームのブログ「パケットの読み取りと書き込み」のGafferの素晴らしい記事と、すべて(すべてC ++でコードを書く!)



問題のステートメントから始めましょう。 ネットワークゲームを作成しています(もちろん、すぐにMMORPGも!)。また、アーキテクチャに関係なく、ネットワークを介してデータを常に送受信する必要があります。 ほとんどの場合、いくつかの異なる種類のパッケージ(プレーヤーのアクション、ゲームワールドの更新、単純に認証など)を送信する必要があります。そして、それぞれに読み取り関数と書き込み関数が必要です。 落ち着いてこれらの2つの関数を落ち着いて記述し、緊張しないことは問題ではないように思えますが、すぐに多くの問題が発生します。





ベンダーがどのように約束を果たし、同時に特定された問題を解決したかに興味がある人は、猫の下でお願いします。



ストリームの読み取りと書き込み



最初の仮定から始めましょう。 テキストとバイナリ形式を読み書きできるようにしたい。 テキスト形式を標準のSTLストリームから読み取り/ std::basic_istream



ます(それぞれstd::basic_istream



およびstd::basic_ostream



)。 バイナリ形式の場合、ストリームと同様のSTLインターフェイスをサポートする独自のBitStream



クラスがあります(少なくとも<<



および>>



演算子、読み取り/書き込みエラーがない場合は0を返し、その他の場合は0を返さないrdstate()



メソッド、およびマニピュレーターを食べる機能) ; また、8ビットの倍数ではない長さのデータを読み書きできるとすばらしいでしょう。

可能なBitStreamクラスインターフェイス
 using byte = uint8_t; class BitStream { byte* bdata; uint64_t position; uint64_t length, allocated; int mode; // 0 = read, other = write int state; // 0 = OK void reallocate(size_t); public: static const int MODE_READ = 0; // ,  ,   static const int MODE_WRITE = 1; // enum class,    inline int get_mode(void) const noexcept { return mode; } BitStream(void); //   BitStream(void*, uint64_t); //   ~BitStream(void); int rdstate(void) const; //   how_much : void write_bits(char how_much, uint64_t bits); //  how_much     : uint64_t read_bits(char how_much); void* data(void); BitStream& operator<<(BitStream&(*func)(BitStream&)); //  BitStream& operator>>(BitStream&(*func)(BitStream&)); //  }; template<typename Int> typename std::enable_if<std::is_integral<Int>::value, BitStream&>::type operator<<(BitStream& out, const Int& arg); //  8*sizeof(Int)    template<typename Int> typename std::enable_if<std::is_integral<Int>::value, BitStream&>::type operator>>(BitStream& in, Int& arg); //  8*sizeof(Int)   
      
      





enable_ifがここにあるのはなぜですか?
std::enable_if<condition, T>



は、条件condition



をチェックし、条件が満たされた場合(つまり、ゼロに等しくない場合)、タイプstd::enable_if<...>::type



決定し、ユーザーが指定したタイプT



またはデフォルト) void



条件が満たされない場合、 std::enable_if<...>::type



への呼び出しは未定義を返します。 このようなエラーはテンプレートのコンパイルを妨げますが、 置換の失敗はエラーではないため(SFINAE) 、テンプレートに引数を代入するときのエラーはコンパイルエラーではないため、プログラムのコンパイルは妨げられません。 operator<<



別の実装が適切なシグネチャでどこかで定義されている場合、または呼び出す適切な関数が単にないという場合、プログラムは正常にコンパイルされます(スマートコンパイラは、試行したことをSFINAEで指定できます)。





シリアライザーインターフェイス



シリアライザーの基本的な「ブリック」が必要であることは明らかです。整数または浮動小数点数をシリアル化および解析できる関数またはオブジェクトです。 ただし、我々は(もちろん!)拡張性、つまり プログラマーが自分のデータ型をシリアル化するための「ブリック」を作成し、それをシリアライザーで使用できるようにします。 そのようなレンガはどのように見えるべきですか? 最も簡単な形式をお勧めします。

 struct IntegerField { template<class OutputStream> static void serialize(OutputStream& out, int t) { out << t; //      ! } //       bool,    template<class InputStream> static bool deserialize(InputStream& in, int& t) { in >> t; //      ! return !in.rdstate(); //  true,       } };
      
      





2つの静的メソッドとおそらく無制限のオーバーロードを持つクラス。 (したがって、1つのテンプレートメソッドの代わりに、 std::basic_ostream



に1つ、 BitStream



に1つ、プログラマーの好みに合わせて他のストリームに無制限の数を書き込むことができます。)



たとえば、要素の動的配列をシリアル化および解析する場合、インターフェイスは次のようになります。

 template<typename T> struct ArrayField { template<class OutputStream> static void serialize(OutputStream& out, size_t n, const T* data); template<class OutputStream> static void serialize(OutputStream& out, const std::vector<T>& data); template<class InputStream> static bool deserialize(InputStream& in, size_t& n, T*& data); template<class InputStream> static bool deserialize(InputStream& in, std::vector<T>& data); };
      
      





ヘルパーパターンcan_serialize



およびcan_deserialize





次に、そのようなフィールドがそのような引数およびそのような引数を使用してシリアル化/解析をトリガーできるかどうかを確認する機能が必要です。 ここで、可変長テンプレートとSFINAEのより詳細な説明に進みます。



コードから始めましょう:
 template<typename... Types> struct TypeList { //   ,  « » static const size_t length = sizeof...(Types); }; template<typename F, typename L> class can_serialize; template<typename F, typename... Ts> class can_serialize<F, TypeList<Ts...>> { template <typename U> static char func(decltype(U::serialize(std::declval<Ts>()...))*); template <typename U> static long func(...); public: static const bool value = ( sizeof(func<F>(0)) == sizeof(char) ); };
      
      





これは何ですか これは、コンパイル段階で、指定されたクラスF



と型のリストL = TypeList<Types...>



によって、これらの型の引数で関数F::serialize



を呼び出すことができるかどうかを決定する構造です。 例えば
 can_serialize<IntegerField, TypeList<BitStream&, int> >::value
      
      



1に等しい
 can_serialize<IntegerField, TypeList<BitStream&, char&> >::value
      
      



char&



完全にint



変換されるため)、しかし、
 can_serialize<IntegerField, TypeList<BitStream&> >::value
      
      



IntegerField



は、出力ストリームのみを受け入れるserialize



メソッドを提供しないため、0です。



どのように機能しますか? より微妙な質問、それを理解しましょう。



TypeList



クラスから始めましょう。 ここでは、Benderが約束する可変個引数テンプレート 、つまり、可変個の引数を持つ テンプレートを使用しますTypeList



クラスのテンプレートは、任意の数の型引数を受け入れます。これらの引数は、 Types



という名前でパラメーターパックに配置されます 。 ( 以前の記事でパラメーターパックの使用方法について詳しく説明しました。) TypeList



クラスは何の役にもTypeList



ませんが、手持ちのパラメーターパックを使って多くのことができます。 たとえば、建設

 std::declval<Ts>()...
      
      



タイプT1, T2, T3, T4



を含む長さ4のパラメーターパックの場合、

 std::declval<T1>(), std::declval<T2>(), std::declval<T3>(), std::declval<T4>()
      
      





次。 クラスF



とタイプL



リストをcan_serialize



テンプレートと、リスト内のタイプ自体へのアクセスを可能にする部分的な特殊化があります。 ( can_serialize<F, L>



を要求すると、 L



型のリストでL



ませんが、コンパイラーは未定義のテンプレートについて文句を言うでしょう。当然のことです。)この部分的な特殊化では、すべての魔法が通じます。



彼女のコードには、 sizeof



内のfunc<F>(0)



呼び出しがあります。 コンパイラーは、戻り値のサイズをバイト単位で計算するために、どのfunc



関数のオーバーロードが呼び出されるかを決定するように強制されますが、コンパイルは試行されないため、「関数の実装が見つかりません」などのエラー(およびエラー「関数の本体にある種のがらくたの種類」(この本体があった場合)。 最初に、彼はfunc



の最初の定義、非常に複雑な外観を使用しようとします。

 template <typename U> static char func( decltype( U::serialize( std::declval<Ts>()... ) )* );
      
      





decltype



コンストラクトdecltype



括弧で囲まれた式decltype



タイプをdecltype



。 たとえば、 decltype(10)



int



と同じです。 ただし、 sizeof



と同様に、コンパイルしません。 これにより、フォーカスをstd::declval



で使用できます。 std::declval



は、必要な型の右辺値参照を返すふりをする関数です。 それは式U::serialize( std::declval<Ts>()... )



意味のあるものにし、引数の半分にデフォルトのコンストラクタがなく、単にU::serialize( Ts()... )



書くことができない場合でも、実際のU::serialize



呼び出しを模倣しますU::serialize( Ts()... )



(この関数は左辺値参照を必要とすることは言うまでもありません!ちなみに、この場合、 declval



は左辺値参照を与えます。これは、C ++ T& &&



declval



T& &&



等しいためです)。 もちろん、実装はありません。 プレーンコードで書く
 int a = std::declval<int>();
      
      



悪い考えです。



だからここに。 decltype



内でのdecltype



不可能な場合(そのようなシグネチャを持つ関数がないか、その置換が何らかの理由でエラーを引き起こす)-コンパイラーは、置換エラーが発生したと見なします。これはご存じのとおり、エラーではありません(SFINAE)。 そして、彼は冷静にさらに進んで、次のfunc



定義を使用しようとします。 ただし、別の関数は異なるサイズの結果を返します。これは、 sizeof



を使用して簡単にキャプチャできます。 (実際、それほど簡単ではなく、 sizeof(long)



は、エキゾチックなプラットフォームではsizeof(char)



に等しくなる可能性がありますが、これらの詳細を省略します-これはすべて修正可能です。)



自己反映の糧として、 can_deserialize



テンプレートのコードも提供します。これは、もう少し複雑ですF::deserialize



が指定されたタイプの引数でcan_deserialize



かどうかをチェックするだけでなく、結果タイプがbool



であることも確認します。

 template<typename F, typename L> class can_deserialize; template<typename F, typename... Ts> class can_deserialize<F, TypeList<Ts...>> { template <typename U> static char func( typename std::enable_if< std::is_same<decltype(U::deserialize(std::declval<Ts>()...)), bool>::value >::type* ); template <typename U> static long func(...); public: using type = can_deserialize; static const bool value = ( sizeof(func<F>(0)) == sizeof(char) ); };
      
      





レンガのパッケージを収集します



最後に、シリアライザのコンテンツに取り組む時が来ました。 つまり、ブリックからアセンブルされた関数をdeserialize



serialize



およびdeserialize



serialize



するSchema



テンプレートクラスを取得する必要があります。
 using MyPacket = Schema<IntegerField, IntegerField, FloatField, ArrayField<float>>; MyPacket::serialize(std::cout, 10, 15, 0.3, 0, nullptr); int da, db; float fc; std::vector<float> my_vector; bool success = MyPacket::deserialize(std::cin, da, db, fc, my_vector);
      
      





単純なものから始めましょう-テンプレートクラスを宣言します(引数の数が可変、ny!)そして再帰の終わりです。

 template<typename... Fields> struct Schema; template<> struct Schema<> { template<typename OutputStream> static void serialize(OutputStream&) { //    ! } template<typename InputStream> static bool deserialize(InputStream&) { return true; //   --  ! } };
      
      





しかし、フィールドの数がゼロでない回路では、 serialize



関数コードはどのように見えるべきでしょうか? 事前に、これらすべてのフィールドのserialize



関数で受け入れられる型を計算することはできず、それらを連結することもできません。これには、標準にまだ含まれていない呼び出し型特性が必要になります 。 残っているのは、可変数の引数を使用して関数を作成し、各フィールドに食べられる限り多くの引数を送信することです。ここでは、苦痛の中で生まれたcan_serialize



を使用できます。



引数の数の観点からこの再帰を行うには、ヘルパークラスが必要です(メインのSchema



クラスは、フィールドの数の再帰を処理します)。 引数を制限せずに定義します:

 template< typename F, //  , serialize     typename NextSerializer, //    «»  typename OS, //    typename TL, //  ,     F::serialize bool can_serialize //       > struct SchemaSerializer;
      
      





次に、 Schema



の部分的な特殊化は、最終的にフィールド数による再帰を実装し、次の形式を取ります

 template<typename F, typename... Fields> struct Schema<F, Fields...> { template< typename OutputStream, //    typename... Types //      > static void serialize(OutputStream& out, Types&&... args) { //   serialize  : SchemaSerializer< F, //   Schema<Fields...>, //     OutputStream&, //    TypeList<Types...>, //     can_serialize<F, TypeList<OutputStream&, Types...>>::value // !!! >::serialize(out, std::forward<Types>(args)...); } // . . . (    deserialize) };
      
      





次に、 SchemaSerializer



再帰をSchemaSerializer



ます。 簡単なものから始めましょう-最後から:

 template<typename F, typename NextSerializer, typename OS> struct SchemaSerializer<F, NextSerializer, OS, TypeList<>, false> { //      ,    . //   (  )  F::serialize //   .  ,     //  --  - ,   //  no such function serialize(...)   . }; template<typename F, typename NextSerializer, typename OS> struct SchemaSerializer<F, NextSerializer, OS, TypeList<>, true> { //        --  ! -- F::serialize //     ! (   ) template<typename... TailArgs> //   static void serialize(OS& out, TailArgs&&... targs) { F::serialize(out); //    ,  // (    out - ) //      : NextSerializer::serialize(out, std::forward<TailArgs>(targs)...); } };
      
      





ここで、ベンダーが約束した2番目の概念、 つまり完全な転送に進みます。 追加の引数(引数はないかもしれませんが、おそらくない)があり、それらをNextSerializer::serialize



さらに送信したいです。 テンプレートの場合、これは完全転送の問題として知られる問題です。



完璧な転送



1つの引数を取るテンプレート関数f



ラッパーを作成するとします。 例えば

 template<typename T> void better_f(T arg) { std::cout << "I'm so much better..." << std::endl; f(arg); }
      
      



見た目は良さそうですが、 f



T&



だけでなく左辺値リンクT&



を入力として受け入れると、すぐに壊れます。元の関数f



は、入力として一時オブジェクトへのリンクを受け取ります。これは、タイプTはリンクのないタイプとして推定されるためです。 解決策は簡単です。

 template<typename T> void better_f(T& arg) { std::cout << "I'm so much better..." << std::endl; f(arg); }
      
      



また、 f



が値で引数を取る場合、すぐに壊れます。リテラルや他の右辺値を元の関数に送信できましたが、新しい関数には送信できませんでした。

コンパイラーが選択でき、両方の場合に完全な互換性が存在するように、両方のオプションを記述する必要があります。

 template<typename T> void better_f(T& arg) { std::cout << "I'm so much better..." << std::endl; f(arg); } template<typename T> void better_f(const T& arg) { std::cout << "I'm so much better..." << std::endl; f(arg); }
      
      



そして、1つの引数を持つ1つの関数のすべてのサーカス。 引数の数が増えると、本格的なラッパーに必要なオーバーロードの数が指数関数的に増えます。



これに対処するために、C ++ 11では右辺値参照と新しい型計算規則が導入されています。 今、あなたは簡単に書くことができます

 template<typename T> void better_f(T&& arg) { std::cout << "I'm so much better..." << std::endl; // ? . . }
      
      



型計算のコンテキストでの&&修飾子には特別な意味があります(ただし、通常の右辺値参照と混同するのは簡単です)。 タイプtype



オブジェクトへの左辺値参照が関数に渡される場合、タイプTはtype&



として推測されます。 タイプtype



右辺値が渡される場合、タイプTはtype&&



として推測されます。 既定の引数を不必要にコピーすることなく、完全に完全に転送するために最後に行うことは、 std::forward



を使用することstd::forward





 template<typename T> void better_f(T&& arg) { std::cout << "I'm so much better..." << std::endl; f(std::forward<T>(arg)); }
      
      



std::forward



は通常のリンクに触れず、値によって渡されたオブジェクトを右辺値リンクに変換します。 したがって、最初のラッパーの後、右辺値リンクはオブジェクト自体の代わりにラッパーのチェーン(存在する場合)をさらに下って、不要なコピーを排除します。



シリアライザーの継続



だから建設

 NextSerializer::serialize(out, std::forward<TailArgs>(targs)...);
      
      



完全な転送を実装し、すべての「余分な」引数を変更せずにシリアライザーのチェーンの下流に送信します。



SchemaSerializer



再帰を書き続けSchemaSerializer



can_serialize = false



再帰ステップ:

 template<typename F, typename NextSerializer, typename OS, typename... Types> struct SchemaSerializer<F, NextSerializer, OS, TypeList<Types...>, false>: //     F::serialize   -- //    ;  ,   //   serialize public SchemaSerializer<F, NextSerializer, OS, typename Head<TypeList<Types...>>::Result, //  ,   can_serialize<F, typename Head<TypeList<OS, Types...>>::Result>::value // !!! > { //      ¯\_(ツ)_/¯ };
      
      



型リストから最後の要素を切り離すヘルパークラスHeadの実装
 template<typename T> struct Head; //      ... template<typename... Ts> struct Concatenate; //       ! template<> struct Concatenate<> { using Result = EmptyList; }; template<typename... A> struct Concatenate<TypeList<A...>> { using Result = TypeList<A...>; }; template<typename... A, typename... B> struct Concatenate<TypeList<A...>, TypeList<B...>> { using Result = TypeList<A..., B...>; }; template<typename... A, typename... Ts> struct Concatenate<TypeList<A...>, Ts...> { using Result = typename Concatenate< TypeList<A...>, typename Concatenate<Ts...>::Result >::Result; }; //  ,  ++   // template<typename T, typename... Ts> // struct Head<TypeList<Ts..., T>>,   //      template<typename T, typename... Ts> struct Head<TypeList<T, Ts...>> { using Result = typename Concatenate<TypeList<T>, typename Head<TypeList<Ts...>>::Result>::Result; }; template<typename T, typename Q> struct Head<TypeList<T, Q>> { using Result = TypeList<T>; }; template<typename T> struct Head<TypeList<T>> { using Result = TypeList<>; }; template<> struct Head<TypeList<>> { using Result = TypeList<>; };
      
      





can_serialize = true



再帰ステップ:

 template<typename F, typename NextSerializer, typename OS, typename... Types> struct SchemaSerializer<F, NextSerializer, OS, TypeList<Types...>, true> { template<typename... TailTypes> //   static void serialize(OS& out, Types... args, TailTypes&&... targs) { F::serialize(out, std::forward<Types>(args)...); // (    out - ) //      : NextSerializer::serialize(out, std::forward<TailTypes>(targs)...); } };
      
      





III ...以上です! これで、シリアライザー(最も一般的な用語で)の準備が整い、最も簡単なコード

 using MyPacket = Schema< IntegerField, IntegerField, CharField >; MyPacket::serialize(std::cout, 777, 6666, 'a');
      
      



正常に表示されます

 7776666a
      
      



しかし、それをデシリアライズする方法は? それでもスペースを追加する必要があります。 これを行うための適切な(つまり、Tru-C ++に十分な抽象)方法は、フィールドセパレータマニピュレータをファイルすることです。
 template< class CharT, class Traits > std::basic_ostream<CharT, Traits>& delimiter( std::basic_ostream<CharT, Traits>& os ) { return os << CharT(' '); //   std::ostream   } template< class CharT, class Traits > std::basic_istream<CharT, Traits>& delimiter( std::basic_istream<CharT, Traits>& is ) { return is; //         } BitStream& delimiter(BitStream& bs) { return bs; //     --   ,   ! // (       , //     ) }
      
      



std::basic_ostream



は、それへのリンクを受け入れて返す関数を食べることができるため( std::endl



std::flush



?がどのように構成されていると思いますか)、すべてのシリアル化コードが
 serialize(OS& out, ...) { F::serialize(out, ...); out << delimiter; //    NextSerializer::serialize(out, ...); }
      
      



その後、自然になります(そして、シリアル化解除の準備ができました)
 777 6666 a
      
      



しかし、まだ細かい部分が残っています...



ネスティング



私たちの回路は単純なフィールドと同じインターフェースを持っているので、なぜ回路から回路を作成しませんか?

 using MyBigPacket = Schema<MyPacket, IntegerField, MyPacket>; MyBigPacket::serialize(std::cout, 11, 22, 'a', 33, 44, 55, 'b');
      
      



iiiiをコンパイルします... 'serialize'の呼び出しに一致する関数がありません。 問題は何ですか?



実際、 Schema::serialize



は与えられたすべての引数を使い果たします。 外部回路は、 Schema::serialize



がスローされたすべての引数で呼び出せることを確認します。 コンパイラーはコンパイルし、最後の4つの引数が機能していないことを確認し候補関数テンプレートは実行不可:1つの引数が必要ですが、5つが提供されました )、エラーを報告します。



SFINAEの利点は、欠点としてここにasいました。 コンパイラは、指定された引数で呼び出すことができるかどうかを判断する前に、関数をコンパイルしません。 彼は彼女のタイプを見ているだけです。 この不要な動作を排除Schema::serialize



には、不適切な引数が渡された場合、 Schema::serialize



強制的に無効な型にSchema::serialize



必要があります。



これがために直接なりやっSchema



SchemaSerializer



-それは簡単です。Schema



これが既に行われ、関数serialize



に無効な引数を持つ無効な型があると仮定します。クラスの特殊化を変更しますSchemaSerializer





 template<typename F, typename NextSerializer, typename OS> struct SchemaSerializer<F, NextSerializer, OS, TypeList<>, true> { template<typename... TailArgs> static auto serialize(OS& out, TailArgs&&... targs) -> decltype(NextSerializer::serialize(out, std::forward<TailArgs>(targs)...)) { F::serialize(out); out << delimiter; NextSerializer::serialize(out, std::forward<TailArgs>(targs)...); } }; template<typename F, typename NextSerializer, typename OS, typename... Types> struct SchemaSerializer<F, NextSerializer, OS, TypeList<Types...>, true> { template<typename... TailTypes> static auto serialize(OS& out, Types... args, TailTypes&&... targs) -> decltype(NextSerializer::serialize(out, std::forward<TailTypes>(targs)...)) { F::serialize(out, std::forward<Types>(args)...); out << delimiter; NextSerializer::serialize(out, std::forward<TailTypes>(targs)...); } };
      
      





どうした 最初に、新しい構文を使用しました。C ++ 11以降では、関数の結果の型を設定するための次のメソッドは同等です。

 type func(...) { ... } auto func(...) -> type { .. }
      
      





なぜこれが必要なのですか?場合によっては、より便利です。たとえば、式c std::declval



の2番目のバージョンの構文で、関数引数が既に使用可能になっtype



おり、最初の-no では、フォーカスcを再度使用せずに目的を達成できましそして、私たちは実際に何を達成しましたか?しかし、次のとおりです。再帰が壊れて、指定された引数で呼び出すことができない場合、呼び出しは置換エラーを引き起こします。戻り値のタイプ(したがって、関数全体のタイプ)は計算できません。このように、私たち呼び出すと、置換エラーが発生します。エラーは最上部まで上昇し、関数のタイプを決定する段階で、そのような引数で呼び出すことは不可能であることをユーザーに伝えます。



NextSerialize::serialize



NextSerialize::serialize(out, std::forward<TailTypes>(targs)...)



SchemaSerializer::serialize



Schema::serialize



同様にスペシャライゼーションを変更しSchema



ます:

 template<typename F, typename... Fields> struct Schema<F, Fields...> { //  using ( , ++11!) template<class OutputStream, typename... Types> using Serializer = SchemaSerializer< F, //   Schema<Fields...>, //     OutputStream&, //    TypeList<Types...>, //     can_serialize<F, TypeList<OutputStream&, Types...>>::value // !!! >; template< typename OS, //    typename... Types //      > static auto serialize(OS& out, Types&&... args) -> decltype(Serializer<OS, Types...>::serialize(out, std::forward<Types>(args)...) ) { Serializer<OS, Types...>::serialize(out, std::forward<Types>(args)...); } // . . . };
      
      





いいね! 少しシンプルなコードになりました

 using MyPacket = Schema< IntegerField, IntegerField, CharField >; using MyBigPacket = Schema< MyPacket, IntegerField, MyPacket >; MyBigPacket::serialize(std::cout, 11, 22, 'a', 33, 44, 55, 'b');
      
      





コンパイルして楽しく印刷する
 11 22 a 33 44 55 b
      
      







やった!



おわりに



C ++は大きな進歩を遂げ、C ++ 11標準は特に大きな一歩を踏み出しました。ほぼすべてのイノベーションを体系的に使用して、クリーンで美しいシリアライザーを実装していますが、これはサポートしていません。各フィールドの任意の数の引数を許容し、各フィールドの任意の数のテンプレートと非標準関数のオーバーロードserialize



許容します。他のシリアライザーをフィールドとして許容します。私の意見では、主なことは、すべての引数を宛先に正確に持ち込むことにより、型キャストを殺さないことです。SchemaDeserializer



関数を実装するヘルパークラスを記述する方法を理解するのは簡単ですdeserialize



— . — ( , , ..), /.



Github .



() . , ! ご清聴ありがとうございました。



All Articles