为什么XML可序列化类需要一个无参构造函数。

180

我正在编写用于XML序列化的代码。使用以下函数:

public static string SerializeToXml(object obj)
{
    XmlSerializer serializer = new XmlSerializer(obj.GetType());
    using (StringWriter writer = new StringWriter())
    {
        serializer.Serialize(writer, obj);
        return writer.ToString();
    }
}
如果参数是一个没有无参构造函数的类的实例,它将抛出一个异常。
未处理的异常:System.InvalidOperationException: CSharpConsole.Foo 无法序列化,因为它没有无参构造函数。at System.Xml.Serialization.TypeDesc.CheckSupported() at System.Xml.Serialization.TypeScope.GetTypeDesc(Type type, MemberInfo source, Boolean directReference, Boolean throwOnError) at System.Xml.Serialization.ModelScope.GetTypeModel(Type type, Boolean directReference) at System.Xml.Serialization.XmlReflectionImporter.ImportTypeMapping(Type type, XmlRootAttribute root, String defaultNamespace) at System.Xml.Serialization.XmlSerializer..ctor(Type type, String defaultNamespace) at System.Xml.Serialization.XmlSerializer..ctor(Type type)
为什么必须有一个无参构造函数才能使 XML 序列化成功?
编辑:感谢 cfeduke 的答案。该无参构造函数可以是私有的或者是内部的。

1
如果您感兴趣,我找到了如何在不需要构造函数的情况下创建对象(请参见更新)-但这对于 XmlSerializer 没有任何帮助 - 它仍然需要它。也许对自定义代码有用。 - Marc Gravell
1
XmlSerializer在反序列化时需要一个默认的无参构造函数。 - Amit Kumar Ghosh
不需要声明无参数构造函数。一旦声明了带参数的构造函数,就必须同时声明无参数构造函数。 - H2ONaCl
5个回答

258
在对象反序列化期间,负责反序列化对象的类会创建序列化类的实例,然后在获取实例之后才开始填充序列化字段和属性。
如果您想要,您可以将构造函数设置为privateinternal,只需确保它没有参数。

1
哦,那么我可以将无参数构造函数设置为私有或内部的,但序列化仍然有效。感谢您的回答。 - Morgan Cheng
2
是的,我经常这样做,尽管我已经接受了公共无参数构造函数的优点,因为它们允许您在泛型中使用“new()”和新的初始化语法。对于带参数的构造函数,请使用静态工厂方法或生成器模式实现。 - cfeduke
17
这个可访问性提示很好,但你的解释对于序列化毫无意义。只有在反序列化时才需要创建对象。我猜测类型检查代码嵌入了XmlSerializer构造函数,因为单个实例可以双向使用。 - Tomer Gabel
8
一个例子是当你将XML发送到某种Web服务时,并不想在自己的组件中接收那些对象。 - Tomer Gabel
5
请记住,即使您将无参数构造函数设置为privateinternal,所有值已序列化的属性都必须具有public的setter。 - chrnola
显示剩余3条评论

77
这是XmlSerializer的一个限制。请注意,BinaryFormatterDataContractSerializer不需要这样做 - 它们可以从外部创建一个未初始化的对象并在反序列化期间对其进行初始化。
由于您正在使用xml,您可以考虑使用DataContractSerializer并将您的类标记为[DataContract]/[DataMember],但请注意,这会更改模式(例如,没有等效于[XmlAttribute]的内容 - 所有内容都变成元素)。
更新:如果您真的想知道,BinaryFormatter等使用FormatterServices.GetUninitializedObject()来创建对象而不调用构造函数。这可能很危险;我不建议经常使用它 ;-p 另请参阅MSDN上的备注:

因为对象的新实例被初始化为零且不运行任何构造函数,所以该对象可能不表示被该对象视为有效的状态。只有当用户打算立即填充所有字段时,当前方法才应该用于反序列化。它不会创建未初始化的字符串,因为创建不可变类型的空实例没有任何用途。

我有自己的序列化引擎,但我不打算使用FormatterServices;我很喜欢知道一个构造函数(任何一个构造函数)是否已经执行。

6
嘿,事实证明我没有遵循自己的建议;protobuf-net(可选)允许使用FormatterServices已经有很长时间了。 - Marc Gravell
1
但是我不理解的是,如果没有指定构造函数,编译器会创建一个公共的无参构造函数,那么为什么这对于XML反序列化引擎来说不够好呢? - toddmo
1
@Shimmy 不行,那个不被支持。虽然有 IXmlSerializable 接口,但是 a: 它在构造函数之后才会被调用,而且 b: 它非常难以实现(特别是反序列化) - 我强烈建议不要尝试去实现它,但是 : 它也无法使用构造函数。 - Marc Gravell
谢谢。除了 XML 之外,还有其他允许通过构造函数进行反序列化的方法吗? - Shimmy Weitzhandler
无参数构造函数的内容应该是什么,以满足XmlSerializer在反序列化过程中的要求?由于有人在这里写了一些答案,但没有人提到构造函数的内容,因此构造函数的内容似乎并不重要。假设我在那里放了一些复杂的东西,以便它被执行,但最终还是会被XML文件所表示的对象树覆盖掉?如果我的理解是正确的,那么无参数构造函数只是为了在内存中腾出一些空间。.NET执行那段代码是愚蠢的吗? - H2ONaCl
显示剩余4条评论

8
答案是:毫无道理的原因。 与其名称相反,XmlSerializer类不仅用于序列化,还用于反序列化。它对您的类执行某些检查,以确保它可以工作,其中一些检查仅与反序列化相关,但它仍然执行所有这些检查,因为它不知道您以后打算做什么。
您的类未能通过的检查是仅与反序列化相关的检查之一。以下是发生的情况:
在反序列化期间,XmlSerializer类将需要创建您的类型的实例。
为了创建类型的实例,需要调用该类型的构造函数。
如果您没有声明构造函数,则编译器已经提供了一个默认的无参数构造函数,但如果您声明了构造函数,则它是唯一可用的构造函数。
因此,如果您声明的构造函数接受参数,则实例化您的类的唯一方法是调用接受参数的构造函数。
然而,XmlSerializer不能调用除无参构造函数之外的任何构造函数,因为它不知道要传递给接受参数的构造函数什么参数。因此,它检查您的类是否有一个无参构造函数,由于没有这样的构造函数,所以失败了。
因此,如果XmlSerializer类被编写成只执行与序列化相关的检查,那么您的类将通过,因为关于序列化没有必要拥有一个无参数构造函数。
正如其他人已经指出的那样,解决您问题的快速方法是简单地添加一个无参数构造函数。不幸的是,这也是一种肮脏的解决方案,因为它意味着您不能从构造函数参数初始化任何只读成员。
除此之外,XmlSerializer类甚至可以以允许没有无参数构造函数的类反序列化的方式编写。所需的只是利用"工厂方法设计模式" (Wikipedia)。从外表看来,微软认为这种设计模式对于DotNet程序员来说过于高级,显然不应该让他们被这些东西迷惑。因此,根据Microsoft的说法,DotNet程序员最好坚持使用无参数构造函数。

1
你说,“毫无道理地”,然后又说,“XmlSerializer 除了能够调用无参数构造函数之外,不能调用任何构造函数,因为它不知道要传递哪些参数给需要参数的构造函数。” 如果它不知道要传递哪些参数给构造函数,那么它怎么知道要传递哪些参数给工厂?或者使用哪个工厂?我无法想象这个工具使用起来会更加简单 - 你想反序列化一个类,那就让反序列化器创建一个默认实例,然后填充每个标记字段。很容易。 - Chuck
1
@Chuck,你可能想在StackOverflow上再提一个问题:“工厂方法如何帮助我处理构造函数参数”,然后告诉我,我会为你解答。 - Mike Nakis

2

似乎没人真正阅读原帖...它是关于序列化而不是反序列化的,因此根本不需要或调用任何构造函数。问题只是来自微软的糟糕编码实践。


0
首先,这是在文档中写的内容。我认为这是你的一个类字段,而不是主要的字段 - 你希望反序列化器如何在没有无参构造函数的情况下将其构建回来?
我认为有一种解决方法是将构造函数设为私有。

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