シングルトンシリアル化またはロックガーデン

画像



開発中に、私はシングルトンをシリアル化したかったのですが、そのフィールドの値を保存および復元しました。 一見したところ、簡単なプロセスは、石と玉石の庭へのエキサイティングな旅に変わりました。2つのシングルトーンの受信からフィールドのシリアル化の欠如まで。



しかし、なぜ庭をフェンスで囲うのですか?


最初の論理的な質問は:もしそうなら、なぜそれが必要なのでしょうか? 実際、そのようなトリックはしばしば必要ではありません。 WPFまたはWinFormsを使用し始めたばかりの多くの人々は、この方法でアプリケーション設定ファイルを実装しようとしますが。 あなたの時間を無駄にせず、車輪を再発明しないでください。これにはアプリケーションとユーザー設定があります(これについてはこちらこちらをご覧ください )。 シリアル化が必要な場合の例を次に示します。

ネットワーク上またはAppDomain間でシングルトンを転送したいと思います。 たとえば、クライアントとサーバーは同じFTPで同時に動作し、それらに関する情報を同期します。 ftpに関する情報は、シングルトンに保存できます(そして、そこで操作するためのメソッドをアタッチします)。

クラスはシリアル化され、さまざまな要素に割り当てられますが、値はすべてのユーザーで同じである必要があります。 そのようなクラスの例はDBNullです。

シングルトン


簡単な例として、このシングルトンを取り上げます。

public sealed class Settings : ISerializable { private static readonly Settings Inst = new Settings(); private Settings() { } public static Settings Instance { get { return Inst; } } public String ServerAddress { get { return _servAddr; } set { _servAddr = value; } } public String Port { get { return _port; } set { _port = value; } } private String _port = ""; }
      
      





すぐにコードについていくつかコメントします。



初見


単純な場合、C#でのシリアル化では、 Serializable属性を追加するだけです。 さて、私たちのケースがどれほど複雑かについてはあまり考えず、この属性を追加します。 では、 SOAPBinary、およびプレーンXMLの 3つの方法でシングルトンをシリアル化してみましょう。

たとえば、バイナリをシリアル化および逆シリアル化します(他の方法も同様です)。

 using (var mem = new MemoryStream()) { var serializer = new BinaryFormatter(); serializer.Serialize(mem, Settings.Instance); mem.Seek(0, SeekOrigin.Begin); object o = serializer.Deserialize(mem); Console.WriteLine((Settings)o == Settings.Instance); }
      
      





(ない)期待されるfalseがコンソールに出力されます。これは、2つのシングルトンオブジェクトを取得したことを意味します。 この結果は、逆シリアル化のプロセスで、リフレクションを使用してプライベートコンストラクターが呼び出され、すべての逆シリアル化された値が新しいオブジェクトに割り当てられることを思い出すと予測できますUPDkekekeksが正しく指摘したように、プライベートコンストラクターは呼び出されませんが、
BinaryFormatterはFormatterServices.GetSafeUninitializedObjectを使用します。これにより、コンストラクターを呼び出さずにオブジェクトをインスタンス化できます。
私たちの庭に最初の石を置くのは、このシングルトンの特徴です。シングルトンはシングルトンではなくなります。



複雑になって...もっと石を入れてください。


すべてを単純にすることは不可能だったので、複雑にする必要があります。ISerializableインターフェイスを介したシリアル化の「手動」プロセスに目を向けると、一見メリットはありません。過去のトラブルは解消されていませんが、複雑さが増しています。 したがって、さらにアクションを実行するために、めったに使用されないIObjectReferenceインターフェイスが必要です。 彼がしていることはすべて、このインターフェイスを実装するクラスのオブジェクトが別のオブジェクトを指していることを示しています。 奇妙に聞こえますか? しかし、別の機能が必要です。そのようなオブジェクトをデシリアライズした後、ポインターはオブジェクトに返されず、オブジェクトが指すオブジェクトに返されます。 この場合、シングルトンへのポインタを返すことは論理的です。 クラスは次のようになります。

 [Serializable] internal sealed class SettingsSerializationHelper : IObjectReference { public Object GetRealObject(StreamingContext context) { return Settings.Instance; } }
      
      





これで、 SettingsSerializationHelperクラスのオブジェクトをシリアル化でき、逆シリアル化時にSettings.Instanceを取得できます。 確かに、さらに2つの石が2つあります。



最初の石について考えてみましょう。これはそれほど重要ではありませんが、明らかに心地よくありません。 この問題の解決策は、 GetObjectData内のシリアル化のためにクラスを置き換えることです 。 次のようになります(シングルトンの内部):

 public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SettingsSerializationHelper)); }
      
      





これで、シングルトンをシリアル化すると、代わりにSettingsSerializationHelperオブジェクトが保存され、逆シリアル化すると、シングルトンが返されます。 前述のシリアル化の例からコンソールへの出力を確認すると、バイナリおよびSOAPの場合、コンソールに出力されるのはtrueですが、XMLシリアル化の場合はfalseです。 したがって、 XMLSerializerGetObjectDataを呼び出さず、すべてのパブリックフィールド/プロパティを独自に処理します。



汚いハッキング


フィールドのシリアル化の問題は、私たちの庭で最大の石です。 残念ながら、私は完全にエレガントで正直なソリューションを見つけることができませんでしたが、あまり正直ではなく、かなり柔軟な「ハック」を構築することが判明しました。

まず、 GetObjectDataメソッドでシングルトンフィールドの保存を追加します。 次のようになります。

 public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SettignsSerializeHelper)); info.AddValue("_servAddr", ServerAddressr); info.AddValue("_port", Port); }
      
      





SOAPシリアル化を行うと、すべてのフィールドが本当にシリアル化されていることがわかります。 ただし、実際には、これらのフィールドを持たないSettignsSerializationHelperをシリアル化しました。これは、逆シリアル化に問題があることを意味します。 2つの解決策があります。



まず、GetObjectDataメソッドを変更します。

 public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof (SettignsSerializeHelper)); var fields = from field in typeof (Settings).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) where field.GetCustomAttribute(typeof (NonSerializedAttribute)) == null select field; foreach (var field in fields) { info.AddValue(field.Name, field.GetValue(Settings.Instance)); } }
      
      





素晴らしい、今、シングルトンにフィールドを追加したいとき、それは私たちの手で作業することなくシリアル化されます。 デシリアライゼーションに移りましょう。

すべてのシングルトンフィールドはSettignsSerializationHelperで繰り返す必要がありますが、実際の重複を避けるために、 サロゲートセレクターを適用し、 SettignsSerializationHelperを変更します

新しいSettignsSerializationHelper

 [Serializable] internal sealed class SettignsSerializeHelper : IObjectReference { public readonly Dictionary<String, object> infos = (from field in typeof (Settings).GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) where field.GetCustomAttribute(typeof (NonSerializedAttribute)) == null select field).ToDictionary(x => x.Name, x => new object()); public object GetRealObject(StreamingContext context) { foreach (var info in infos) { typeof (Settings).GetField(info.Key, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).SetValue(Settings.Instance, info.Value); } return Settings.Instance; } }
      
      





そのため、ハッシュマップはSettignsSerializationHelper内に作成されます 。ここで、キーはシリアル化されるフィールドの名前であり、将来の値は逆シリアル化後にこれらのフィールドの値になります。 ここでは、カプセル化を強化するために、情報をプライベートにしてキーと値のペアにアクセスするメソッドを作成できますが、例を複雑にしません。 GetRealObject内で、シングルトンを非シリアル化されたフィールド値に設定し、そのリンクを返します。

現在は、情報フィールドの値を入力するだけです。 これにはセレクターが使用されます。

 internal sealed class SettingsSurrogate : ISerializationSurrogate { public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) { throw new NotImplementedException(); } public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) { var ssh = new SettignsSerializeHelper(); foreach (var val in info) { ssh.infos[val.Name] = val.Value; } return ssh; } }
      
      





セレクターは逆シリアル化にのみ使用されるため、 SetObjectDataのみを記述します。 obj(デシリアライズ可能なオブジェクト)がセレクター内に到着すると、そのフィールドは状況に関係なく0とnullで埋められます(デシリアライズ中にFormatterServicesからGetUninitializedObjectを呼び出した後にobjが取得されます)。 したがって、この場合、新しいSettignsSerializationHelperを作成して返すのが簡単です(このオブジェクトは逆シリアル化されたと見なされます)。 次に、foreach内で、デシリアライズされたデータで情報を入力します。このデータはシングルトンフィールドに割り当てられます。

そして今、シリアル化/逆シリアル化プロセス自体の例:

      /: using (var mem = new MemoryStream()) { var soapSer = new SoapFormatter(); soapSer.Serialize(mem, Settings.Instance); var ss = new SurrogateSelector(); ss.AddSurrogate(typeof(SettignsSerializeHelper), soapSer.Context, new SettingsSurrogate()); soapSer.SurrogateSelector = ss; mem.Seek(0, SeekOrigin.Begin); var o = soapSer.Deserialize(mem); Console.WriteLine((Settings)o == Settings.Instance); }
      
      





コンソールにTrueが表示され、すべてのフィールドが復元されます。 最後に、私たちはロックガーデンを完成させ、適切な形にしました。



All Articles