Пользовательская сериализация

Пользовательской сериализацией называется процесс управления сериализацией и десериализацией типа. Управление сериализацией позволяет обеспечить совместимость сериализации, в результате чего становится возможной сериализация и десериализация между различными версиями типа без нарушения основных функциональных возможностей типа. Например, в первой версии типа может быть только два поля. В следующей версии типа добавлено еще несколько полей. Во второй версии приложения должна быть предусмотрена возможность сериализации и десериализации обоих типов. В следующих разделах объясняется, как управлять сериализацией.

Предупреждение

Двоичная сериализация может быть опасной. Дополнительные сведения см. в разделе BinaryFormatter Security Guide.

Важно!

В версиях до .NET Framework 4.0 сериализация пользовательских данных в сборке с частичным доверием выполнялась методом GetObjectData. Начиная с версии 4.0 этот метод помечен атрибутом SecurityCriticalAttribute, который запрещает выполнение в сборках с частичным доверием. Чтобы решить эту проблему, реализуйте интерфейс ISafeSerializationData.

Использование пользовательских методов во время сериализации и после нее

Для выполнения настраиваемых методов во время и после сериализации рекомендуем применять следующие атрибуты к методам, которые используются для исправления данных во время и после сериализации:

Эти атрибуты обеспечивают использование типа на любом или на всех четырех этапах процесса сериализации и десериализации. Атрибуты определяют методы типа, которые должны вызываться на каждом этапе. Доступ методов к потоку сериализации отсутствует, однако можно изменить объект перед сериализацией и после нее или перед десериализацией и после нее. Атрибуты можно применять на всех уровнях иерархии наследования типов, и каждый метод вызывается в иерархии от базового до самого дальнего в цепочке наследования. Этот механизм позволяет избежать усложненности и каких-либо проблем реализации интерфейса ISerializable, передав процесс сериализации и десериализации самой дальней в цепочке наследования реализации. Кроме того, благодаря такому механизму модули форматирования могут игнорировать заполнение полей и извлечение данных из потока сериализации. Дополнительные сведения и примеры управления сериализацией и десериализацией см. по ссылкам выше.

Кроме того, при добавлении нового поля в существующий сериализуемый тип применяйте к полю атрибут OptionalFieldAttribute. BinaryFormatter и SoapFormatter игнорируют отсутствие поля, если обрабатывается поток без нового поля.

Реализация интерфейса ISerializable

Еще одним способом управления сериализацией является реализация для объекта интерфейса ISerializable. Однако следует помнить, что метод, описанный в предыдущем разделе, замещает этот метод управления сериализацией.

Кроме того, не следует использовать сериализацию по умолчанию для класса, который отмечен атрибутом Serializable и имеет декларативную или принудительную безопасность на уровне класса или конструкторов. В таких случаях в классах всегда следует реализовывать интерфейс ISerializable.

Реализация ISerializable включает реализацию метода GetObjectData и специального конструктора, который используется при десериализации объекта. В следующем образце кода показано, как реализовать ISerializable в классе MyObject на основе информации предыдущего раздела.

[Serializable]
public class MyObject : ISerializable
{
    public int n1;
    public int n2;
    public String str;

    public MyObject()
    {
    }

    protected MyObject(SerializationInfo info, StreamingContext context)
    {
      n1 = info.GetInt32("i");
      n2 = info.GetInt32("j");
      str = info.GetString("k");
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("i", n1);
        info.AddValue("j", n2);
        info.AddValue("k", str);
    }
}
<Serializable()>  _
Public Class MyObject
    Implements ISerializable
    Public n1 As Integer
    Public n2 As Integer
    Public str As String

    Public Sub New()
    End Sub

    Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
        n1 = info.GetInt32("i")
        n2 = info.GetInt32("j")
        str = info.GetString("k")
    End Sub 'New

    <SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter := True)> _
    Public Overridable Sub GetObjectData(ByVal info As SerializationInfo, ByVal context As StreamingContext)
        info.AddValue("i", n1)
        info.AddValue("j", n2)
        info.AddValue("k", str)
    End Sub
End Class

Если во время сериализации вызывается GetObjectData, следует указать информацию SerializationInfo, которая предоставляется вместе с вызовом метода. Добавьте переменные, которые будут сериализованы как пары имен и значений. В качестве имени можно ввести любой текст. Переменные-члены, добавляемые в SerializationInfo могут быть любыми при условии, что сериализуется достаточное количество данных для восстановления объекта при десериализации. Производные классы должны вызывать метод GetObjectData для базового объекта, если в последнем реализован интерфейс ISerializable.

Обратите внимание, что сериализация позволяет другому коду просматривать или изменять данные экземпляра объекта, не доступные в ином случае. Поэтому код, выполняющий сериализацию, требует разрешения SecurityPermission с установленным флагом SerializationFormatter. При политике безопасности по умолчанию такое разрешение не предоставляется коду, загруженному из Интернета или интрасети, и дается только коду на локальном компьютере. Метод GetObjectData должен быть явно защищен посредством запроса либо SecurityPermission с установленным флагом SerializationFormatter, либо иных разрешений, которые предназначены специально для защиты закрытых данных.

Если в закрытом поле хранятся конфиденциальные сведения, для их защиты следует запрашивать соответствующие разрешения для GetObjectData. Помните, что код, которому предоставлены разрешения SecurityPermission с установленным флагом SerializationFormatter, может просматривать и изменять данные, которые хранятся в закрытых полях. Злонамеренный вызывающий объект с предоставленными разрешениями SecurityPermission может просматривать такие данные, как местоположения скрытых каталогов или предоставленные разрешения, и использовать такую информацию для повышения уязвимости системы безопасности компьютера. Полный список устанавливаемых флагов разрешений безопасности представлен в перечислении SecurityPermissionFlag Enumeration.

Следует особо отметить, что при добавлении ISerializable в класс следует реализовать и GetObjectData, и специальный конструктор. Компилятор выдает предупреждение, если GetObjectData отсутствует. Но поскольку невозможно принудительно реализовать конструктор, при его отсутствии предупреждение не выводится, и при попытке десериализовать класс без конструктора создается исключение.

В текущей разработке предусмотрена поддержка метода SetObjectData, чтобы избежать возможных проблем с безопасностью и управлением версиями. Например, метод SetObjectData должен быть открытым, если он определяется как часть интерфейса. По этой причине пользователям следует написать код, чтобы метод SetObjectData не вызывался несколько раз. Иначе вредоносное приложение, вызывающее метод SetObjectData для объекта в процессе выполнения операции, может стать причиной возникновения проблем.

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

Чтобы восстановить состояние объекта, необходимо всего лишь запросить значения переменных из SerializationInfo по именам, использованным во время сериализации. Если в базовом классе реализовано ISerializable, следует вызвать базовый конструктор, чтобы базовый объект смог восстановить свои переменные.

При создании нового производного класса на основе класса с реализацией ISerializable в производном классе должны быть реализованы как конструктор, так и метод GetObjectData, если имеются переменные, подлежащие сериализации. В следующем примере показано, как это можно сделать с помощью рассмотренного ранее класса MyObject.

[Serializable]
public class ObjectTwo : MyObject
{
    public int num;

    public ObjectTwo()
      : base()
    {
    }

    protected ObjectTwo(SerializationInfo si, StreamingContext context)
      : base(si, context)
    {
        num = si.GetInt32("num");
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
    public override void GetObjectData(SerializationInfo si, StreamingContext context)
    {
        base.GetObjectData(si,context);
        si.AddValue("num", num);
    }
}
<Serializable()>  _
Public Class ObjectTwo
    Inherits MyObject
    Public num As Integer

    Public Sub New()

    End Sub

    Protected Sub New(ByVal si As SerializationInfo, _
    ByVal context As StreamingContext)
        MyBase.New(si, context)
        num = si.GetInt32("num")
    End Sub

    <SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter := True)> _
    Public Overrides Sub GetObjectData(ByVal si As SerializationInfo, ByVal context As StreamingContext)
        MyBase.GetObjectData(si, context)
        si.AddValue("num", num)
    End Sub
End Class

Не забудьте вызвать базовый класс в конструкторе десериализации. Если этого не сделать, конструктор для базового класса никогда не вызывается, и после десериализации объект не является полностью построенным.

Объекты воссоздаются изнутри, вызов методов во время десериализации может привести к нежелательным побочным эффектам, поскольку вызываемые объекты могут относиться к ссылкам на объекты, которые не были десериализованы на момент вызова. Если в десериализуемом классе реализовано IDeserializationCallback, во время десериализации всего графа объекта автоматически вызывается метод OnDeserialization. На этом этапе все дочерние объекты, на которые имеются ссылки, полностью восстановлены. Типичным примером класса, который сложно десериализовать без использования прослушивателя событий, служит хэш-таблица. Вызов пар "ключ-значение" во время десериализации не представляет сложности, однако добавление таких объектов обратно в хэш-таблицу может быть затруднительным, поскольку нет никаких гарантий, что производные на основе хэш-таблицы классы десериализованы. Поэтому на данном этапе не рекомендуется вызывать методы для хэш-таблицы.

См. также