【问题标题】:How does BinaryFormatter.Deserialize create new objects?BinaryFormatter.Deserialize 如何创建新对象?
【发布时间】:2011-03-30 20:23:16
【问题描述】:

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.

【问题讨论】:

  • 好问题。要解决这个问题,您需要在反序列化期间进行一些指针/引用修复,这可能很难甚至是不可能的。请注意,new Car 仅被调用一次。您可能想在 2 个过程中尝试此操作。
  • 注意:我链接的另一个问题是关于 DataContractSerializer,但是 BinaryFormatter 的解释是一样的
  • @Thomas:谢谢,回答了。 FormatterServices.GetUninitializedObject() 很奇怪。

标签: c# constructor serialization binary-serialization


【解决方案1】:

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

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

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

反序列化通过调用FormatterServices.GetUninitializedObject 与第一步大致相同,然后通过将字段的值设置为与序列化期间记录的值相同(这可能需要将其他对象反序列化为所述值)。

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

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

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

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

如果我们只关心确保反序列化会在构造过程中发生一些副作用,我们可以实现IDeserializationCallback,当反序列化完成时我们将调用IDeserializationCallback.OnDeserialization

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

【讨论】:

  • +1 - IDeserializationCallback 是个好主意。用它来初始化必要的私有字段等。解决了我的问题!
【解决方案2】:

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

也看看

OnSerializingAttribute

OnSerializedAttribute

OnDeserializingAttribute

OnDeserializedAttribute

【讨论】:

    【解决方案3】:

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2018-07-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-01
      • 1970-01-01
      • 2015-03-31
      • 2020-06-06
      • 2011-06-21
      相关资源
      最近更新 更多