26

BinaryFormatter将流反序列化为对象时,它似乎是在不调用构造函数的情况下创建新对象。

它是如何做到的?为什么?.NET 中还有其他东西可以做到这一点吗?

这是一个演示:

[Serializable]
public class Car
{
    public static int constructionCount = 0;

    public Car()
    {
        constructionCount++;
    }
}

public class Test
{
    public static void Main(string[] args)
    {
        // Construct a car
        Car car1 = new Car();

        // Serialize and then deserialize to create a second, identical car
        MemoryStream stream = new MemoryStream();
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, car1);
        stream.Seek(0, SeekOrigin.Begin);
        Car car2 = (Car)formatter.Deserialize(stream);

        // Wait, what happened?
        Console.WriteLine("Cars constructed: " + Car.constructionCount);
        if (car2 != null && car2 != car1)
        {
            Console.WriteLine("But there are actually two.");
        }
    }
}

输出:

Cars constructed: 1
But there are actually two.

4

3 回答 3

22

调用构造函数有两件事(或至少应该做)。

一种是为对象留出一定数量的内存,并完成所有必要的内务管理,使其成为 .NET 世界其他地方的对象(请注意此解释中的一定数量的手动操作)。

另一种是将对象置于有效的初始状态,可能基于参数 - 这是构造函数中的实际代码将执行的操作。

反序列化通过调用 与第一步FormatterServices.GetUninitializedObject做很多相同的事情,然后通过将字段的值设置为与序列化期间记录的值相同(这可能需要反序列化其他要说的对象)来做与第二步相同的事情值)。

现在,反序列化将对象放入的状态可能与任何构造函数都可能不对应。充其量是浪费(构造函数设置的所有值都将被覆盖),更糟的是它可能是危险的(构造函数有一些副作用)。这也可能是不可能的(只有构造函数是带参数的——序列化无法知道要使用哪些参数)。

您可以将其视为仅由反序列化使用的特殊类型的构造函数(OO 纯粹主义者会 - 并且应该 - 对不构造的构造函数的想法感到震惊,我的意思是这只是一个类比,如果你知道 C++ 会想到就记忆而言,覆盖的方式new是有效的,你有一个更好的类比,尽管仍然只是一个类比)。

现在,在某些情况下这可能是一个问题——也许我们有readonly只能由构造函数设置的字段,或者我们可能有我们想要发生的副作用。

两者的解决方案是使用ISerializable. 这将基于对的调用进行序列化,然后使用和字段ISerializable.GetObjectData调用特定的构造函数以进行反序列化(所述构造函数甚至可以是私有的——这意味着大多数其他代码甚至都看不到它)。因此,如果我们可以反序列化字段并产生我们想要的任何副作用(我们也可以做各种事情来控制序列化的内容和方式)。SerializationInfoStreamingContextreadonly

如果我们只关心确保反序列化会在构造上发生一些副作用,我们可以实施IDeserializationCallback并且在反序列化完成时我们会IDeserializationCallback.OnDeserialization调用。

至于与此做同样事情的其他事情,.NET 中还有其他形式的序列化,但这就是我所知道的。可以调用FormatterServices.GetUninitializedObject自己,但除非您有强有力的保证,即后续代码会将生成的对象置于有效状态(即,正是您在从序列化相同类型产生的数据中反序列化对象时所处的那种情况对象)这样做是令人担忧的,并且是产生非常难以诊断的错误的好方法。

于 2010-11-30T12:40:10.207 回答
3

问题是,BinaryFormatter 并没有真正制作您的特定对象。它将对象图放回内存中。对象图基本上是您的对象在内存中的表示;这是在对象被序列化时创建的。然后,反序列化调用基本上只是将该图作为对象返回到内存中一个打开的指针,然后它被代码强制转换为它的实际内容。如果投错了,就会抛出异常。

至于您的特定示例,您实际上只是在建造一辆车;你只是在复制那辆车。当您将其序列化到流中时,您将存储它的精确二进制副本。当你反序列化它时,你不必构造任何东西。它只是将图形作为对象粘贴在内存中的某个指针值处,并让您可以随心所欲地使用它。

由于指针位置不同,您对 car1 != car2 的比较是正确的,因为 Car 是引用类型。

为什么?坦率地说,只需拉取二进制表示很容易,而不必去拉取每个属性等等。

我不确定.NET 中的其他任何东西是否使用相同的过程;最有可能的候选对象是在序列化期间以某种格式使用对象二进制文件的任何其他东西。

于 2010-08-17T20:03:37.997 回答
1

不知道为什么没有调用构造函数,但我将IDeserializationCallback其用作解决方法。

也看看

OnSerializingAttribute

OnSerializedAttribute

OnDeserializingAttribute

OnDeserializedAttribute

于 2010-11-30T12:47:25.567 回答