Как BinaryFormatter.Deserialize создает новые объекты? - PullRequest
22 голосов
/ 17 августа 2010

Когда 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.

Ответы [ 3 ]

19 голосов
/ 30 ноября 2010

Есть две вещи, которые вызывает конструктор (или, по крайней мере, должен делать).

Один состоит в том, чтобы выделить определенное количество памяти для объекта и выполнить всю домашнюю работу, необходимую для того, чтобы он стал объектом для остального мира .NET (обратите внимание на определенное количество рукопожатий в этом объяснении).

Другой способ - перевести объект в допустимое начальное состояние, возможно, на основе параметров - это то, что будет делать фактический код в конструкторе.

Десериализация делает то же самое, что и первый шаг, вызывая FormatterServices.GetUninitializedObject, а затем делает то же самое, что и второй шаг, устанавливая значения для полей, эквивалентные тем, которые были записаны во время сериализации (для этого может потребоваться десериализация других объектов с указанными значениями).

Теперь, состояние, в которое десериализация помещает объект, может не соответствовать тому, что возможно любому конструктору. В лучшем случае это будет расточительно (все значения, установленные конструктором, будут перезаписаны), а в худшем случае это может быть опасно (конструктор имеет некоторый побочный эффект). Это также может быть просто невозможно (только конструктор принимает параметры - у сериализации нет способа узнать, какие аргументы использовать).

Вы могли бы рассматривать его как специальный вид конструктора, используемый только при десериализации (пуристы ОО будут - и должны - вздрогнуть от идеи конструктора, который не конструирует, я имею в виду это только как аналогию, если вы знаете, C ++ думает о том, как переопределение new работает с памятью, и у вас есть еще лучшая аналогия, хотя это всего лишь аналогия).

Теперь, это может быть проблемой в некоторых случаях - возможно, у нас есть readonly поля, которые могут быть установлены только конструктором, или, может быть, у нас есть побочные эффекты, которые хотят, чтобы произошел.

Решение обоих - переопределить поведение сериализации с помощью ISerializable. Он будет сериализован на основе вызова ISerializable.GetObjectData, а затем вызовет определенный конструктор с полями SerializationInfo и StreamingContext для десериализации (указанный конструктор может быть даже закрытым - это означает, что большинство других кодов даже не будут видеть это). Следовательно, если мы можем десериализовать readonly поля и иметь любые побочные эффекты, которые мы хотим (мы также можем сделать все, чтобы контролировать только то, что сериализовано и как).

Если мы просто позаботимся о том, чтобы при десериализации происходил какой-то побочный эффект, который может произойти в процессе строительства, мы можем реализовать IDeserializationCallback, и у нас будет IDeserializationCallback.OnDeserialization, вызванный после завершения десериализации.

Что касается других вещей, которые делают то же самое, есть другие формы сериализации в .NET, но это все, что я знаю. Можно вызвать FormatterServices.GetUninitializedObject самостоятельно, но исключая случай, когда у вас есть надежная гарантия того, что последующий код переведет созданный объект в действительное состояние (т. Е. Именно в такую ​​ситуацию, в которой вы находитесь при десериализации объекта из данных, полученных сериализацией такой же объект) выполнение этого чревато и является хорошим способом создания действительно трудно диагностируемой ошибки.

3 голосов
/ 18 августа 2010

Дело в том, что BinaryFormatter на самом деле не создает ваш конкретный объект. Это помещает граф объекта в память. Граф объектов - это в основном представление вашего объекта в памяти; это было создано, когда объект сериализован. Затем, вызов десериализации в основном просто помещает этот граф обратно в память как объект с открытым указателем, и затем он приводится к тому, что он есть в коде. Если он приведен неверно, генерируется исключение.

Что касается вашего конкретного примера, вы действительно строите только одну машину; Вы просто делаете точную копию этой машины. Когда вы сериализуете его в поток, вы сохраняете точную двоичную копию. Когда вы десериализуете его, вам не нужно ничего строить. Он просто помещает график в память при некотором значении указателя как объект и позволяет вам делать с ним все, что вы захотите.

Ваше сравнение car1! = Car2 верно из-за различий в расположении указателя, поскольку Car является ссылочным типом.

Почему? Честно говоря, просто пойти и получить двоичное представление, вместо того, чтобы идти и извлекать каждое свойство и все такое.

Я не уверен, что что-то еще в .NET использует эту же процедуру; наиболее вероятными кандидатами будут все, что использует двоичный объект в некотором формате во время сериализации.

1 голос
/ 30 ноября 2010

Не знаю, почему конструктор не вызывается, но я использую IDeserializationCallback в качестве обходного пути.

также взгляните на

OnSerializingAttribute

OnSerializedAttribute

OnDeserializingAttribute

OnDeserializedAttribute

...