序列化包含字典成员的类

156

在我的早期问题上进行进一步的扩展,我决定(反)序列化我的配置文件类,这个方法非常好用。

现在我想要存储一个关联数组,用于映射驱动器字母(键为驱动器字母,值为网络路径),我已经尝试使用DictionaryHybridDictionaryHashtable,但每次调用ConfigFile.Load()ConfigFile.Save()时都会出现以下错误:

反映类型时出现错误 'App.ConfigFile'。[snip] System.NotSupportedException: 无法序列化成员 App.Configfile.mappedDrives [snip]

从我所读到的内容来看,字典和哈希表都可以被序列化,那么我做错了什么?

[XmlRoot(ElementName="Config")]
public class ConfigFile
{
    public String guiPath { get; set; }
    public string configPath { get; set; }
    public Dictionary<string, string> mappedDrives = new Dictionary<string, string>();

    public Boolean Save(String filename)
    {
        using(var filestream = File.Open(filename, FileMode.OpenOrCreate,FileAccess.ReadWrite))
        {
            try
            {
                var serializer = new XmlSerializer(typeof(ConfigFile));
                serializer.Serialize(filestream, this);
                return true;
            } catch(Exception e) {
                MessageBox.Show(e.Message);
                return false;
            }
        }
    }

    public void addDrive(string drvLetter, string path)
    {
        this.mappedDrives.Add(drvLetter, path);
    }

    public static ConfigFile Load(string filename)
    {
        using (var filestream = File.Open(filename, FileMode.Open, FileAccess.Read))
        {
            try
            {
                var serializer = new XmlSerializer(typeof(ConfigFile));
                return (ConfigFile)serializer.Deserialize(filestream);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message + ex.ToString());
                return new ConfigFile();
            }
        }
    }
}
12个回答

193

Paul Welter的博客上有一个解决方案——XML可序列化的泛型字典。

由于某些原因,.net 2.0中的泛型字典不可进行XML序列化。以下代码片段是一个可XML序列化的泛型字典,通过实现IXmlSerializable接口来使字典可序列化。

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

[XmlRoot("dictionary")]
public class SerializableDictionary<TKey, TValue>
    : Dictionary<TKey, TValue>, IXmlSerializable
{
    public SerializableDictionary() { }
    public SerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary) { }
    public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer) { }
    public SerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer) { }
    public SerializableDictionary(int capacity) : base(capacity) { }
    public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer) { }

    #region IXmlSerializable Members
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();

        if (wasEmpty)
            return;

        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");

            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();

            this.Add(key, value);

            reader.ReadEndElement();
            reader.MoveToContent();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        foreach (TKey key in this.Keys)
        {
            writer.WriteStartElement("item");

            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();

            writer.WriteStartElement("value");
            TValue value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
    }
    #endregion
}

1
+1 太棒了的回答。对于 SortedList 也适用,只需将“SerializableDictionary”更改为“SerializableSortedList”,将“Dictionary<TKey, TValue>”更改为“SortedList<TKey, TValue>”。 - kdmurray
运行良好。上面接受的答案引用了微软的话:“唯一的解决方案是实现一个不实现IDictionary接口的自定义哈希表”。但这不是真的,对吧?上面的解决方案继承了Dictionary(因此实现了IDictionary),但也实现了IXMLSerializable。或者我错过了什么? - Adam
我在我的答案中添加了这个类的一个适应版本,它使用属性来表示键/值,而不是元素。 - Benbob
1
+1 和一个建议。当 SerializableDictionary 对象包含多个元素时,会抛出异常... 应修改 ReadXml() 和 WriteXml() 方法,将 ReadStartElement("item"); 和 WriteStartElement("item"); 及其关联的 ReadEndElement() 和 WriteEndElement() 移出 while 循环。 - MNS
1
那么这是否意味着在后续的框架中IDictionary是可序列化的? - Thomas
1
如果字典存储的是string值,那么这个实现将会起作用,但是如果你尝试在其中存储自定义对象或字符串数组,则会在反序列化时抛出InvalidOperationException,并提到一个意外的包装元素。(请参见我的问题以了解我在此方面遇到的问题的示例。) - Knowledge Cube

78

如果一个类实现了IDictionary接口,就不能进行序列化。可以参考这个链接

问:为什么不能序列化哈希表?

答:XmlSerializer无法处理实现IDictionary接口的类。这在一定程度上是由于时间限制,另一方面是因为hashtable在XSD类型系统中没有对应项。唯一的解决方法是实现一个自定义的hashtable,而不是实现IDictionary接口。

所以我认为你需要创建自己的Dictionary版本来解决这个问题。可以查看这个其他问题


4
想知道 DataContractSerializer 类是否能做到这一点。只是输出结果有些丑陋。 - rekire

60

DataContractSerializer的问题在于它按字母顺序进行序列化和反序列化,因此如果您尝试以错误的顺序反序列化某些内容,则会在对象中产生空属性而默默失败。 - superjugy

18
创建序列化代理。
例如,您有一个具有公共属性Dictionary类型的类。
为了支持此类型的Xml序列化,请创建一个通用的键值对类:
public class SerializeableKeyValue<T1,T2>
{
    public T1 Key { get; set; }
    public T2 Value { get; set; }
}

在原始属性上添加XmlIgnore属性:

    [XmlIgnore]
    public Dictionary<int, string> SearchCategories { get; set; }

暴露一个数组类型的公共属性,该属性持有SerializableKeyValue实例数组,用于将SearchCategories属性序列化和反序列化:

    public SerializeableKeyValue<int, string>[] SearchCategoriesSerializable
    {
        get
        {
            var list = new List<SerializeableKeyValue<int, string>>();
            if (SearchCategories != null)
            {
                list.AddRange(SearchCategories.Keys.Select(key => new SerializeableKeyValue<int, string>() {Key = key, Value = SearchCategories[key]}));
            }
            return list.ToArray();
        }
        set
        {
            SearchCategories = new Dictionary<int, string>();
            foreach (var item in value)
            {
                SearchCategories.Add( item.Key, item.Value );
            }
        }
    }

我喜欢这个方法,因为它将序列化与字典成员解耦。如果我想要给一个经常使用的类添加序列化功能,那么包装字典可能会导致继承类型的中断。 - VoteCoffee
2
对于任何实现此功能的人,需要注意:如果您尝试将代理属性设置为List(或任何其他集合),XML序列化程序不会调用setter(而是调用getter并尝试添加到返回的列表中,这显然不是您想要的)。在此模式下使用数组。 - ashastral

9
你应该探索Json.Net,它非常易于使用,并允许将Json对象直接反序列化为字典。 james_newtonking 例子:
string json = @"{""key1"":""value1"",""key2"":""value2""}";
Dictionary<string, string> values = JsonConvert.DeserializeObject<Dictionary<string, string>>(json); 
Console.WriteLine(values.Count);
// 2
Console.WriteLine(values["key1"]);
// value1

7

字典和哈希表无法使用 XmlSerializer 进行序列化。因此,您不能直接使用它们。解决方法是使用 XmlIgnore 属性将这些属性隐藏在序列化器中,并通过可序列化的键值对列表公开它们。

附注:构建一个 XmlSerializer 非常昂贵,因此,如果有可能重复使用,请始终缓存它。


5
我需要一个SerializableDictionary类,使用xml属性进行键值对存储,所以我改编了Paul Welter的类。
这将生成以下xml:
<Dictionary>
  <Item Key="Grass" Value="Green" />
  <Item Key="Snow" Value="White" />
  <Item Key="Sky" Value="Blue" />
</Dictionary>"

代码:

using System.Collections.Generic;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace DataTypes {
    [XmlRoot("Dictionary")]
    public class SerializableDictionary<TKey, TValue>
        : Dictionary<TKey, TValue>, IXmlSerializable {
        #region IXmlSerializable Members
        public System.Xml.Schema.XmlSchema GetSchema() {
            return null;
        }

        public void ReadXml(XmlReader reader) {
            XDocument doc = null;
            using (XmlReader subtreeReader = reader.ReadSubtree()) {
                doc = XDocument.Load(subtreeReader);
            }
            XmlSerializer serializer = new XmlSerializer(typeof(SerializableKeyValuePair<TKey, TValue>));
            foreach (XElement item in doc.Descendants(XName.Get("Item"))) {
                using(XmlReader itemReader =  item.CreateReader()) {
                    var kvp = serializer.Deserialize(itemReader) as SerializableKeyValuePair<TKey, TValue>;
                    this.Add(kvp.Key, kvp.Value);
                }
            }
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer) {
            XmlSerializer serializer = new XmlSerializer(typeof(SerializableKeyValuePair<TKey, TValue>));
            XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
            ns.Add("", "");
            foreach (TKey key in this.Keys) {
                TValue value = this[key];
                var kvp = new SerializableKeyValuePair<TKey, TValue>(key, value);
                serializer.Serialize(writer, kvp, ns);
            }
        }
        #endregion

        [XmlRoot("Item")]
        public class SerializableKeyValuePair<TKey, TValue> {
            [XmlAttribute("Key")]
            public TKey Key;

            [XmlAttribute("Value")]
            public TValue Value;

            /// <summary>
            /// Default constructor
            /// </summary>
            public SerializableKeyValuePair() { }
        public SerializableKeyValuePair (TKey key, TValue value) {
            Key = key;
            Value = value;
        }
    }
}
}

单元测试:

using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DataTypes {
    [TestClass]
    public class SerializableDictionaryTests {
        [TestMethod]
        public void TestStringStringDict() {
            var dict = new SerializableDictionary<string, string>();
            dict.Add("Grass", "Green");
            dict.Add("Snow", "White");
            dict.Add("Sky", "Blue");
            dict.Add("Tomato", "Red");
            dict.Add("Coal", "Black");
            dict.Add("Mud", "Brown");

            var serializer = new System.Xml.Serialization.XmlSerializer(dict.GetType());
            using (var stream = new MemoryStream()) {
                // Load memory stream with this objects xml representation
                XmlWriter xmlWriter = null;
                try {
                    xmlWriter = XmlWriter.Create(stream);
                    serializer.Serialize(xmlWriter, dict);
                } finally {
                    xmlWriter.Close();
                }

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);

                XDocument doc = XDocument.Load(stream);
                Assert.AreEqual("Dictionary", doc.Root.Name);
                Assert.AreEqual(dict.Count, doc.Root.Descendants().Count());

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);
                var outDict = serializer.Deserialize(stream) as SerializableDictionary<string, string>;
                Assert.AreEqual(dict["Grass"], outDict["Grass"]);
                Assert.AreEqual(dict["Snow"], outDict["Snow"]);
                Assert.AreEqual(dict["Sky"], outDict["Sky"]);
            }
        }

        [TestMethod]
        public void TestIntIntDict() {
            var dict = new SerializableDictionary<int, int>();
            dict.Add(4, 7);
            dict.Add(5, 9);
            dict.Add(7, 8);

            var serializer = new System.Xml.Serialization.XmlSerializer(dict.GetType());
            using (var stream = new MemoryStream()) {
                // Load memory stream with this objects xml representation
                XmlWriter xmlWriter = null;
                try {
                    xmlWriter = XmlWriter.Create(stream);
                    serializer.Serialize(xmlWriter, dict);
                } finally {
                    xmlWriter.Close();
                }

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);

                XDocument doc = XDocument.Load(stream);
                Assert.AreEqual("Dictionary", doc.Root.Name);
                Assert.AreEqual(3, doc.Root.Descendants().Count());

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);
                var outDict = serializer.Deserialize(stream) as SerializableDictionary<int, int>;
                Assert.AreEqual(dict[4], outDict[4]);
                Assert.AreEqual(dict[5], outDict[5]);
                Assert.AreEqual(dict[7], outDict[7]);
            }
        }
    }
}

1
看起来不错,但是在空字典的情况下会失败。你需要在ReadXML方法中使用reader.IsEmptyElement测试。 - AnthonyVO

3
您可以使用ExtendedXmlSerializer。如果您有一个类:
public class ConfigFile
{
    public String guiPath { get; set; }
    public string configPath { get; set; }
    public Dictionary<string, string> mappedDrives {get;set;} 

    public ConfigFile()
    {
        mappedDrives = new Dictionary<string, string>();
    }
}

并创建此类的实例:

ConfigFile config = new ConfigFile();
config.guiPath = "guiPath";
config.configPath = "configPath";
config.mappedDrives.Add("Mouse", "Logitech MX Master");
config.mappedDrives.Add("keyboard", "Microsoft Natural Ergonomic Keyboard 4000");

您可以使用ExtendedXmlSerializer对此对象进行序列化:
ExtendedXmlSerializer serializer = new ExtendedXmlSerializer();
var xml = serializer.Serialize(config);

输出的XML将如下所示:

<?xml version="1.0" encoding="utf-8"?>
<ConfigFile type="Program+ConfigFile">
    <guiPath>guiPath</guiPath>
    <configPath>configPath</configPath>
    <mappedDrives>
        <Item>
            <Key>Mouse</Key>
            <Value>Logitech MX Master</Value>
        </Item>
        <Item>
            <Key>keyboard</Key>
            <Value>Microsoft Natural Ergonomic Keyboard 4000</Value>
        </Item>
    </mappedDrives>
</ConfigFile>

您可以从nuget安装ExtendedXmlSerializer,或者运行以下命令:
Install-Package ExtendedXmlSerializer

这里是在线示例

2

字典类实现了ISerializable接口。以下是Dictionary类的定义。

[DebuggerTypeProxy(typeof(Mscorlib_DictionaryDebugView<,>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
[System.Runtime.InteropServices.ComVisible(false)]
public class Dictionary<TKey,TValue>: IDictionary<TKey,TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback  

我认为这不是问题所在。请参考下面的链接,它说如果你有任何其他不能序列化的数据类型,则字典将无法进行序列化。 http://forums.asp.net/t/1734187.aspx?Is+Dictionary+serializable+


这在最新版本中是正确的,但在.NET 2中,Dictionary是不可序列化的,即使今天也是如此。我今天刚刚确认了一个针对.NET 3.5的项目,这就是我找到这个线程的原因。 - Bruce

0

在参考了Paul Welter的博客之后,这里提供了一个更新的代码集,与最新的C#兼容,用于处理可空类型(因为你知道,我很注重编写没有警告的代码!):

/*-----------------------------------------------------------------------------------------------------
 * Class borrowed from Paul Welter's blog https://weblogs.asp.net/pwelter34/444961
 * Thank you for your invaluable contribution!
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace {Your-Namespace}
{

    [XmlRoot("dictionary")]
    public class SerializableDictionary<TKey, TValue>
        : Dictionary<TKey, TValue>, IXmlSerializable
        where TKey : notnull
    {
        public SerializableDictionary() { }
        public SerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary) { }
        public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer) { }
        public SerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer) { }
        public SerializableDictionary(int capacity) : base(capacity) { }
        public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer) { }

        #region IXmlSerializable Members
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null!;
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            XmlSerializer keySerializer = new(typeof(TKey));
            XmlSerializer valueSerializer = new(typeof(TValue));

            bool wasEmpty = reader.IsEmptyElement;
            reader.Read();

            if (wasEmpty)
                return;

            while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
            {
                reader.ReadStartElement("item");

                reader.ReadStartElement("key");
                TKey key = (TKey)keySerializer.Deserialize(reader)!;
                reader.ReadEndElement();

                reader.ReadStartElement("value");
                TValue value = (TValue)valueSerializer.Deserialize(reader)!;
                reader.ReadEndElement();

                this.Add(key!, value!);

                reader.ReadEndElement();
                reader.MoveToContent();
            }
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            XmlSerializer keySerializer = new(typeof(TKey));
            XmlSerializer valueSerializer = new(typeof(TValue));

            foreach (TKey key in this.Keys)
            {
                writer.WriteStartElement("item");

                writer.WriteStartElement("key");
                keySerializer.Serialize(writer, key);
                writer.WriteEndElement();

                writer.WriteStartElement("value");
                TValue value = this[key];
                valueSerializer.Serialize(writer, value);
                writer.WriteEndElement();

                writer.WriteEndElement();
            }
        }
        #endregion
    }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接