LoginSignup
5
2

More than 1 year has passed since last update.

複雑な構造を持つデータのシリアライズ

Last updated at Posted at 2022-03-27

はじめに

循環参照があり、複数のクラスが含まれる複雑な構造のデータをシリアライズしたいと思いました。
しかし、調べてみても単純なクラスが1個だけあるサンプルが殆どでした。

唯一見つかった循環参照を扱ったサンプルも、シリアライズするクラスは一つです。
こちらはDataContractSerializerを使用しています。

果たして、私のやりたいことはDataContractSerializerを使ってできるのでしょうか?

注意

.NET Frameworkです。.NET Coreではビルドできません。

循環参照をシリアライズするのにDataContractSerializerのコンストラクタでpreserveObjectReferences引数をtrueにする必要があるそうです。

実際、後述のサンプルでこれをfalseにすると例外が発生します。
循環参照で例外.png

コンストラクタの数が違う

.NET FrameworkだとDataContractSerializerのコンストラクタは13個あるのに、
DataContractSerializer_Framework.png

.NET Coreだと7個で、preserveObjectReferences引数を持つものはありません。
DataContractSerializer_Core.png

試行

2つの異なるクラスが参照し合うパターン

問題なく動作しました。
Part, Moduleに付けているDataContract, KnownTypeは必須で、外すと例外が発生します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;

namespace DataContractSerializerSample1
{
    [DataContract]
    [KnownType(typeof(Module))]
    class Part
    {
        [DataMember]
        public Module Owner;

		[DataMember]
        public Part AnotherPart { get; set; }

		public Part(Module owner)
		{
			Owner = owner;
		}
    }

    [DataContract]
    [KnownType(typeof(Part))]
    class Module
    {
        [DataMember]
        public List<Part> Parts { get; private set; } = new List<Part>();
    }

    static class Program
    {
        static void Main()
        {
            Module module = new Module();
            Part a = new Part(module);
            Part b = new Part(module);

            // 循環参照あり
            a.AnotherPart = b;
            b.AnotherPart = a;
            module.Parts.Add(a);
            module.Parts.Add(b);

            DataContractSerializer dcs = new DataContractSerializer(
                typeof(Product), null, int.MaxValue, false, true, null);
            using (MemoryStream ms = new MemoryStream())
            {
                // シリアライズ
                dcs.WriteObject(ms, module);

                // デシリアライズ
                ms.Position = 0;
                Module c = (Module)dcs.ReadObject(ms);
                Console.WriteLine(ReferenceEquals(c.Parts[0], c.Parts[1].AnotherPart));
                Console.WriteLine(ReferenceEquals(c.Parts[1], c.Parts[0].AnotherPart));
                Console.WriteLine(ReferenceEquals(c.Parts[0].Owner, c.Parts[1].Owner));
                Console.WriteLine(ReferenceEquals(c, c.Parts[0].Owner));
            }
        }
    }
}

継承を含むパターン

これも問題なく動作しました。
しかし、Part, Moduleに付けているKnownTypeを外しても例外が発生しませんでした。(?)
また、Productに継承元のModuleについてのKnownTypeが無くても動きました。

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;

namespace DataContractSerializerSample2
{
    [DataContract]
    [KnownType(typeof(Module))]
    class Part
    {
        [DataMember]
        public Module Owner;

		[DataMember]
        public Part AnotherPart { get; set; }

		public Part(Module owner)
		{
			Owner = owner;
		}
    }

    [DataContract]
    [KnownType(typeof(Part))]
    class Module
    {
        [DataMember]
        public List<Part> Parts { get; private set; } = new List<Part>();
    }

    [DataContract]
    class Product : Module
    {
        [DataMember]
        public string SN;
    }

    static class Program
    {
        static void Main()
        {
            Product product = new Product();
            product.SN = "SN0123456789";
            Part a = new Part(product);
            Part b = new Part(product);

            // 循環参照あり
            a.AnotherPart = b;
            b.AnotherPart = a;
            product.Parts.Add(a);
            product.Parts.Add(b);

            DataContractSerializer dcs = new DataContractSerializer(
                typeof(Product), null, int.MaxValue, false, true, null);
            using (MemoryStream ms = new MemoryStream())
            {
                // シリアライズ
                dcs.WriteObject(ms, product);

                // デシリアライズ
                ms.Position = 0;
                Product c = (Product)dcs.ReadObject(ms);
                Console.WriteLine(ReferenceEquals(c.Parts[0], c.Parts[1].AnotherPart));
                Console.WriteLine(ReferenceEquals(c.Parts[1], c.Parts[0].AnotherPart));
                Console.WriteLine(ReferenceEquals(c.Parts[0].Owner, c.Parts[1].Owner));
                Console.WriteLine(ReferenceEquals(c, c.Parts[0].Owner));
                Console.WriteLine(c.SN);
            }
        }
    }
}

結論

不明点はありますが、現時点でやりたいことは出来そうです。

まとめ

  • シリアライズするクラス全てにDataContract付けるべし
  • .NET Coreでは不可
  • 循環参照可
  • 継承しててもOK
  • (KnownTypeは何か問題があれば調べる)

終わりに

改善点やツッコミ処等があればご指摘お願いします。

5
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2