Как BinaryFormatter.Deserialize создает новые объекты?

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


person Joe Daley    schedule 17.08.2010    source источник
comment
Хороший вопрос. Чтобы обойти это, вам нужно будет сделать некоторые исправления указателя / ссылки во время десериализации, что может быть сложно или даже невозможно. Обратите внимание на тот факт, что new Car был вызван только один раз. Вы можете попробовать это в двух процессах.   -  person leppie    schedule 17.08.2010
comment
возможный дубликат DataContractSerializer не вызывает мой конструктор ??   -  person Thomas Levesque    schedule 17.08.2010
comment
Примечание. Другой вопрос, с которым я связался, касается DataContractSerializer, но объяснение такое же для BinaryFormatter.   -  person Thomas Levesque    schedule 17.08.2010
comment
@Thomas: Спасибо, это ответ. FormatterServices.GetUninitializedObject () странный.   -  person Joe Daley    schedule 18.08.2010


Ответы (3)


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

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

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

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

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

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

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

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

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

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

person Jon Hanna    schedule 30.11.2010
comment
+1 - IDeserializationCallback - отличная идея. Используйте его для инициализации необходимых приватных полей и т. Д. Моя проблема решена! - person womp; 28.01.2011

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

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

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

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

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

person fire.eagle    schedule 17.08.2010

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

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

OnSerializingAttribute

OnSerializedAttribute

OnDeserializingAttribute

OnDeserializedAttribute

person decyclone    schedule 30.11.2010