Как не наступить на грабли, работая с сериализацией

в 6:53, , рубрики: .net, C#, open source, pvs-studio, serialization, static code analysis, Блог компании PVS-Studio, Программирование, статический анализ кода
Как не наступить на грабли, работая с сериализацией - 1

Несмотря на то, что использовать механизм сериализации при программировании на C# достаточно просто и удобно, есть моменты, которые стоит учитывать. О том, на какие грабли можно наступить, работая с сериализацией, о примерах кода, в котором эти грабли припрятаны, а также о том, как PVS-Studio поможет вам избежать шишек на лбу, и будет эта статья.

Для кого статья?

Данная статья будет особенно полезна разработчикам, только начинающим знакомиться с механизмом сериализации. Более опытные программисты также могут почерпнуть для себя что-то интересное, или просто убедиться, что даже профессионалы порой допускают ошибки.

Тем не менее, подразумевается, что читатель уже знаком с механизмом сериализации.

При чём тут PVS-Studio? В релизе 6.05 были добавлены 6 диагностических правил, обнаруживающих подозрительный код, связанный с использованием механизма сериализации. Эти диагностики в основном ищут проблемные места, связанные с атрибутом [Serializable] или реализацией интерфейса ISerializable.

Примечание.

Стоит понимать, что описанные в статье утверждения актуальны для некоторых сериализаторов, например — BinaryFormatter и SoapFormatter, а для других, например, собственноручно написанного сериализатора, поведение может отличаться. Например, отсутствие атрибута [Serializable] у класса может не помешать проводить его сериализацию и десериализацию собственным сериализатором.

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

Реализуя ISerializable, не забудьте про конструктор сериализации

Реализация типом интерфейса ISerializable позволяет управлять сериализацией, выбирая, какие члены нужно сериализовать, какие — нет, какие значение нужно записывать при сериализации членов и т.п.

Как не наступить на грабли, работая с сериализацией - 2

Интерфейс ISerializable содержит объявление одного метода — GetObjectData, который будет вызван при сериализации объекта. Но в паре с этим методом обязательно должен быть реализован конструктор, который будет вызываться при десериализации объекта. Так как интерфейс не может обязать вас реализовать в своем типе какой-то конструктор, эта задача ложится на плечи программиста, занимающегося реализацией сериализуемого типа. Конструктор сериализации имеет следующую сигнатуру:

Ctor(SerializationInfo, StreamingContext)

Без наличия данного конструктора сериализация объекта пройдёт успешно (при условии корректной реализации метода GetObjectData), но восстановить (десериализовать) его не удастся — будет сгенерировано исключение типа SerializationException.

Посмотрим на пример подобного кода из проекта Glimpse:

[Serializable]
internal class SerializableTestObject : ISerializable
{
  public string TestProperty { get; set; }

  public void GetObjectData(SerializationInfo info, 
                            StreamingContext context)
  {
    info.AddValue("TestProperty", this.TestProperty);
  }
}

Предупреждение PVS-Studio: V3094 Possible exception when deserializing. The SerializableTestObject(SerializationInfo, StreamingContext) constructor is missing. Glimpse.Test.AspNet SessionModelConverterShould.cs 111

Сериализация экземпляра данного класса пройдёт успешно, а вот при десериализации возникнет исключение, так как нет соответствующего конструктора. Скорее всего, это не ошибка (исходя из названия класса и файла), но в качестве иллюстрации описанной ситуации — то, что нужно.

Конструктор сериализации для данного класса мог бы выглядеть так:

protected SerializableTestObject(SerializationInfo info, 
                                 StreamingContext context)
{
  TestProperty = info.GetString(nameof(TestProperty));
}

Обращайте внимание на модификатор доступа конструктора сериализации

При разработке типа, реализующего интерфейс ISerializable, важно правильно определить модификатор доступа для конструктора сериализации. Здесь возможны несколько случаев:

  • конструктор сериализации объявлен с модификатором private в незапечатанном классе;
  • конструктор сериализации объявлен с модификатором доступа public или internal;
  • конструктор сериализации объявлен с модификатором protected в запечатанном классе.

Наибольший интерес представляет первый из приведённых выше вариантов, так как он таит в себе наибольшую опасность. Кратко остановимся и на втором пункте, а третий рассматривать не будем — объявить член с модификатором protected в структуре не даст компилятор (ошибка компиляции), если такой член объявляется в запечатанном классе — компилятор выдаст предупреждение.

Конструктор сериализации в незапечатанном классе имеет модификатор доступа 'private'

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

В таком случае у разработчика дочернего класса есть 2 выхода — отказаться от использования данного родительского класса или вручную десериализовать члены базового класса. Стоит отметить, что второй случай навряд ли можно считать решением проблемы:

  • не факт, что в базовом классе была предусмотрена тривиальная десериализация членов;
  • разработчик дочернего класса может забыть десереализовать какой-либо член базового класса;
  • при всём желании, десериализовать приватные члены базового класса не удастся.

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

При анализе проектов удалось найти несколько таких, в которых описанное выше правило не соблюдалось.

NHibernate

[Serializable]
public class ConnectionManager : ISerializable, 
                                 IDeserializationCallback
{
  ....
  private ConnectionManager(SerializationInfo info, 
                            StreamingContext context)
  {
    ....
  }
  ....
}

Предупреждение PVS-Studio: V3103 A private Ctor(SerializationInfo, StreamingContext) constructor in unsealed type will not be accessible when deserializing derived types. NHibernate ConnectionManager.cs 276

Roslyn

[Serializable]
private class TestDiagnostic : Diagnostic, ISerializable
{
  ....
  private ConnectionManager(SerializationInfo info, 
                            StreamingContext context)
  {
    ....
  }
  ....
}

Предупреждение PVS-Studio: V3103 A private Ctor(SerializationInfo, StreamingContext) constructor in unsealed type will not be accessible when deserializing derived types. NHibernate ConnectionManager.cs 276

В обоих примерах, приведённых выше, у конструктора сериализации следовало установить модификатор доступа protected, чтобы дочерние классы могли вызвать его при десериализации.

Не объявляйте конструктор сериализации с модификаторами 'public' или 'internal'

Это совет хорошего стиля программирования. Объявление конструктора сериализации с модификатором public или internal не приведёт к ошибке, но смысла в этом нет — данный конструктор не должен вызываться извне, а сериализатору без разницы, какой модификатор доступа имеет конструктор.

При проверке open source проектов встретились несколько таких, в которых это правило не соблюдалось.

MSBuild

[Serializable]
private sealed class FileState : ISerializable
{
  ....
  internal SystemState(SerializationInfo info, 
                       StreamingContext context)
  {
    ....
  }
  ....
}

Предупреждение PVS-Studio: V3103 The Ctor(SerializationInfo, StreamingContext) constructor should be used for deserialization. Making it internal is not recommended. Consider making it private. Microsoft.Build.Tasks SystemState.cs 218

[Serializable]
private sealed class FileState : ISerializable
{
  ....
  internal FileState(SerializationInfo info, StreamingContext context)
  {
    ....
  }
  ....
}

Предупреждение PVS-Studio: V3103 The Ctor(SerializationInfo, StreamingContext) constructor should be used for deserialization. Making it internal is not recommended. Consider making it private. Microsoft.Build.Tasks SystemState.cs 139

В обоих случаях для конструктора сериализации следовало установить модификатор доступа private, так оба класса, приведённых выше, являются запечатанными.

NHibernate

[Serializable]
public class StatefulPersistenceContext : IPersistenceContext,   
                                          ISerializable, 
                                          IDeserializationCallback
{
  ....
  internal StatefulPersistenceContext(SerializationInfo info, 
                                      StreamingContext context)
  {
    ....
  }
  ....
}

Предупреждение PVS-Studio: V3103 The Ctor(SerializationInfo, StreamingContext) constructor should be used for deserialization. Making it internal is not recommended. Consider making it protected. NHibernate StatefulPersistenceContext.cs 1478

[Serializable]
public class Configuration : ISerializable
{
  ....
  public Configuration(SerializationInfo info, 
                       StreamingContext context)
  {
   ....
  }
  ....
}

Предупреждение PVS-Studio: V3103 The Ctor(SerializationInfo, StreamingContext) constructor should be used for deserialization. Making it public is not recommended. Consider making it protected. NHibernate Configuration.cs 84

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

Реализуйте виртуальный метод GetObjectData в незапечатанных классах

Правило простое — если вы разрабатываете незапечатанный класс, реализующий интерфейс ISerializable, объявите метод GetObjectData с модификатором virtual. Это позволит дочерним классам корректно производить сериализацию объекта при использовании полиморфизма.

Как не наступить на грабли, работая с сериализацией - 3

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

Допустим, у нас есть следующие объявления родительского и дочернего классов.

[Serializable]
class Base : ISerializable
{
  ....
  public void GetObjectData(SerializationInfo info, 
                            StreamingContext context)
  {
    ....
  }
}

[Serializable]
sealed class Derived : Base
{
  ....
  public new void GetObjectData(SerializationInfo info, 
                                StreamingContext context)
  {
    ....
  }
}

Предположим, что имеется метод сериализации и десереализации объекта следующего вида:

void Foo(BinaryFormatter bf, MemoryStream ms)
{
  Base obj = new Derived();
  bf.Serialize(ms, obj);
  ms.Seek(0, SeekOrigin.Begin);
  Derived derObj = (Derived)bf.Deserialize(ms);
}

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

Для исправления ошибки в родительском классе к методу GetObjectData необходимо добавить модификатор virtual, в производном — override.

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

Рассмотрим пример реализации родительского и дочернего классов:

[Serializable]
class Base : ISerializable
{
  ....
  void ISerializable.GetObjectData(SerializationInfo info, 
                                   StreamingContext context)
  {
    ....
  }
}

[Serializable]
sealed class Derived : Base, ISerializable
{
  ....
  public void GetObjectData(SerializationInfo info, 
                            StreamingContext context)
  {
    ....
  }
}

В таком случае из дочернего класса будет невозможно обратиться к методу GetObjectData родительского класса. И если в базовом методе сериализуются приватные члены, обратиться к ним из дочернего класса также не удастся, а значит и не удастся провести корректную сериализацию. Для исправления ошибки помимо явной реализации в базовый класс необходимо добавить неявную реализацию виртуального метода GetObjectData. Тогда исправленный код может выглядеть так:

[Serializable]
class Base : ISerializable
{
  ....
  void ISerializable.GetObjectData(SerializationInfo info, 
                                    StreamingContext context)
  {
    GetObjectData(info, context);
  }

  public virtual void GetObjectData(SerializationInfo info, 
                                    StreamingContext context)
  {
    ....
  }
}

[Serializable]
sealed class Derived : Base
{
  ....
  public override void GetObjectData(SerializationInfo info, 
                                     StreamingContext context)
  {
    ....
    base.GetObjectData(info, context);
  }
}

Или же, если не подразумевается наследование данного класса, следует сделать его запечатанным, добавив к объявлению класса модификатор sealed.

Roslyn

[Serializable]
private class TestDiagnostic : Diagnostic, ISerializable
{
  private readonly string _kind;
  ....
  private readonly string _message;
  ....
  void ISerializable.GetObjectData(SerializationInfo info,  
                                   StreamingContext context)
  {
    info.AddValue("id", _descriptor.Id);
    info.AddValue("kind", _kind);
    info.AddValue("message", _message);
    info.AddValue("location", _location, typeof(Location));
    info.AddValue("severity", _severity, typeof(DiagnosticSeverity));
    info.AddValue("defaultSeverity", _descriptor.DefaultSeverity,
                   typeof(DiagnosticSeverity));
    info.AddValue("arguments", _arguments, typeof(object[]));
  }
  ....
}

Предупреждение PVS-Studio: V3104 'GetObjectData' implementation in unsealed type 'TestDiagnostic' is not virtual, incorrect serialization of derived type is possible. CSharpCompilerSemanticTest DiagnosticAnalyzerTests.cs 112

Класс TestDiagnostic является незапечатанным (хоть и приватным, так что унаследоваться от него в рамках того же класса возможно), но при этом он имеет только явную реализацию интерфейса ISerializable, в которой, ко всему прочему, сериализуются приватные члены. Это означает одно — разработчик дочернего класса никаким образом не сможет сериализовать необходимые члены: метод GetObjectData будет ему недоступен, а обратиться к членам напрямую не позволит модификатор доступа.

Правильнее было бы вынести весь код сериализации, приведённый выше, в виртуальный метод GetObjectData, на который сослаться из явной реализации интерфейса:

void ISerializable.GetObjectData(SerializationInfo info, 
                                 StreamingContext context)
{
  GetObjectData(info, context);
}

public virtual void GetObjectData(SerializationInfo info,
                                  StreamingContext context)
{
  info.AddValue("id", _descriptor.Id);
  info.AddValue("kind", _kind);
  info.AddValue("message", _message);
  info.AddValue("location", _location, typeof(Location));
  info.AddValue("severity", _severity, typeof(DiagnosticSeverity));
  info.AddValue("defaultSeverity", _descriptor.DefaultSeverity,
                typeof(DiagnosticSeverity));
  info.AddValue("arguments", _arguments, typeof(object[]));
}

Все сериализуемые члены должны иметь сериализуемый тип

Это условие является обязательным для корректной сериализации объекта вне зависимости от того, происходит ли автоматическая сериализация (когда тип декорирован атрибутом [Serializable] и при этом не реализует интерфейс ISerializable) или сериализация осуществляется вручную (реализован ISerializable).

В противном случае, если при сериализации встретится член, не декорированный атрибутом [Serializable], будет сгенерировано исключение типа SerializationException.

Если требуется сериализовать объект без учёта членов, имеющих несереализуемый тип, возможны несколько подходов:

  • сделайте несериализуемый тип сериализуемым;
  • если происходит автоматическая сериализация, декорируйте поля, которые не нужно сериализовать, атрибутом [NonSerialized];
  • если происходит ручная сериализация, просто игнорируйте те члены, которые вам не нужны.

Стоит обратить внимание на тот факт, что атрибут [NonSerialized] применим только к полям. Таким образом, вы не сможете запретить сериализацию свойства, но, если оно будет иметь несериализуемый тип — получите исключение. Например, при попытке сериализации класса SerializedClass, определение которого приведено ниже:

sealed class NonSerializedType { }

[Serializable]
sealed class SerializedClass
{
  private Int32 value;
  public NonSerializedType NSProp { get; set; }
}

Обойти эту ситуацию можно, реализовав свойство через поле, декорированное атрибутом [NonSerialized]:

[Serializable]
sealed class SerializedClass
{
  private Int32 value;

  [NonSerialized]
  private NonSerializedType nsField;

  public NonSerializedType NSProp
  {
    get { return nsField; }
    set { nsField = value; }
  }
}

Подобные ошибки, когда сериализуемый тип имеет члены несереализуемых типов, не декорированные атрибутом [NonSerialized], обнаруживает диагностическое правило V3097 статического анализатора кода PVS-Studio.

Напоминаю, что данное предупреждение не обязательно свидетельствует о наличии ошибки — всё зависит от используемого сериализатора.

Рассмотрим несколько примеров кода, в которых описанное условие было нарушено.

Subtext

public class BlogUrlHelper
{
  ....
}

[Serializable]
public class AkismetSpamService : ICommentSpamService
{
  ....
  readonly BlogUrlHelper _urlHelper;
  ....
}

Предупреждение PVS-Studio: V3097 Possible exception: the 'AkismetSpamService' type marked by [Serializable] contains non-serializable members not marked by [NonSerialized]. Subtext.Framework AkismetSpamService.cs 31

Тип BlogUrlHelper поля _urlHelper не является сериализуемым, поэтому при попытке сериализации экземпляра класса AkismetSpamService некоторыми сериализаторами, будет сгенерировано исключение типа SerializationException. Решать проблему нужно, отталкиваясь от ситуации. Если используются сериализаторы типа BinaryFormatter или SoapFormatter — необходимо либо декорировать поле атрибутом [NonSerialized], либо декорировать атрибутом [Serializable] тип BlogUrlHepler. Если используются другие сериализаторы, не требующие наличия атрибута [Serializable] у сериализуемых полей, можно не забивать голову.

NHibernate

public class Organisation
{
 ....
}

[Serializable]
public class ResponsibleLegalPerson  
{
  ....
  private Organisation organisation;
  ....
}

Предупреждение PVS-Studio: V3097 Possible exception: the 'ResponsibleLegalPerson' type marked by [Serializable] contains non-serializable members not marked by [NonSerialized]. NHibernate.Test ResponsibleLegalPerson.cs 9

Ситуация аналогична описанной выше — или пан, или пропал. Всё зависит от используемого сериализатора.

Не забывайте про атрибут [Serializable] при реализации интерфейса ISerializable

Данный совет относится скорее к тем, кто только начинает работать с сериализацией. Управляя сериализацией вручную, посредством реализации интерфейса ISerializable, легко забыть декорировать тип атрибутом [Serializable], что потенциально приводит к генерации исключения типа SerializationException. Такие сериализаторы, как BinaryFormatter, требуют наличия данного атрибута.

Как не наступить на грабли, работая с сериализацией - 4

SharpDevelop

Интересные примеры данной ошибки встретились в проекте SharpDevelop.

public class SearchPatternException : Exception, ISerializable
{
  ....
  protected SearchPatternException(SerializationInfo info, 
                                   StreamingContext context) 
    : base(info, context)
  {
  }
}

Предупреждение PVS-Studio: V3096 Possible exception when serializing 'SearchPatternException' type. [Serializable] attribute is missing. ICSharpCode.AvalonEdit ISearchStrategy.cs 80

public class DecompilerException : Exception, ISerializable
{
  ....
  protected DecompilerException(SerializationInfo info, 
                                StreamingContext context) 
    : base(info, context)
  {
  }
}

Предупреждение PVS-Studio: V3096 Possible exception when serializing 'DecompilerException' type. [Serializable] attribute is missing. ICSharpCode.Decompiler DecompilerException.cs 28

Для передачи объекта исключения между доменами приложений происходит его сериализация и десериализация. Соответственно, собственные типы исключений должны быть сериализуемыми. В приведённых выше примерах типы SearchPatternException и DecompilerException наследуются от Exception и реализуют конструкторы сериализации, но при этом не декорированы атрибутом [Serializable], а значит, что при попытке сериализации объектов данных типов (например, для передачи между доменами) будет сгенерировано исключение типа SerializationException. Таким образом, например, генерируя исключение в другом домене приложений, в текущем вы перехватите не сгенерированное исключение, а SerializationException.

Убедитесь, что в методе GetObjectData сериализуются все необходимые члены типа

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

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

Для диагностирования таких ситуаций в статическом анализаторе кода PVS-Studio предусмотрено диагностическое правило V3099. Предлагаю ознакомиться с некоторыми примерами кода, обнаруженных данным правилом.

SharpDevelop

[Serializable]
public abstract class XshdElement
{
  public int LineNumber { get; set; }
  
  public int ColumnNumber { get; set; }
  
  public abstract object AcceptVisitor(IXshdVisitor visitor);
}

[Serializable]
public class XshdColor : XshdElement, ISerializable
{
  ....
  public virtual void GetObjectData(SerializationInfo info,        
                                    StreamingContext context)
  {
    if (info == null)
      throw new ArgumentNullException("info");
    info.AddValue("Name", this.Name);
    info.AddValue("Foreground", this.Foreground);
    info.AddValue("Background", this.Background);
    info.AddValue("HasUnderline", this.Underline.HasValue);
    if (this.Underline.HasValue)
      info.AddValue("Underline", this.Underline.Value);
    info.AddValue("HasWeight", this.FontWeight.HasValue);
    if (this.FontWeight.HasValue)
      info.AddValue("Weight", this.FontWeight
                                  .Value
                                  .ToOpenTypeWeight());
    info.AddValue("HasStyle", this.FontStyle.HasValue);
    if (this.FontStyle.HasValue)
      info.AddValue("Style", this.FontStyle.Value.ToString());
    info.AddValue("ExampleText", this.ExampleText);
  }
}

Предупреждение PVS-Studio: V3099 Not all the members of 'XshdColor' type are serialized inside 'GetObjectData' method: LineNumber, ColumnNumber. ICSharpCode.AvalonEdit XshdColor.cs 101

В этом коде нет проблем, описанных ранее, таких как неправильные модификаторы доступа у конструктора сериализации, отсутствие атрибута [Serializable] или модификатора virtual у метода GetObjectData.
Увы, ошибка здесь всё равно есть. В методе GetObjectData не учитываются свойства базового класса, а значит, при сериализации часть данных будет потеряна. В итоге, при десериализации будет восстановлен объект с другим состоянием.

В данном случае решением проблемы будет ручное добавление необходимых значений, например, таким образом:

info.AddValue(nameof(LineNumber), LineNumber);
info.AddValue(nameof(ColumnNumber), ColumnNumber);

Если бы базовый класс также реализовывал интерфейс ISerializable, решение было бы более элегантным — вызовом в производном методе GetObjectData базового.

NHibernate

[Serializable]
public sealed class SessionImpl : AbstractSessionImpl, 
                                  IEventSource, 
                                  ISerializable, 
                                  IDeserializationCallback
{
  ....
  void ISerializable.GetObjectData(SerializationInfo info, 
                                   StreamingContext context)
  {
    log.Debug("writting session to serializer");

    if (!connectionManager.IsReadyForSerialization)
    {
      throw new InvalidOperationException("Cannot serialize a Session 
                                           while connected");
    }

    info.AddValue("factory", Factory, typeof(SessionFactoryImpl));
    info.AddValue("persistenceContext", persistenceContext, 
                   typeof(StatefulPersistenceContext));
    info.AddValue("actionQueue", actionQueue, typeof(ActionQueue));
    info.AddValue("timestamp", timestamp);
    info.AddValue("flushMode", flushMode);
    info.AddValue("cacheMode", cacheMode);

    info.AddValue("interceptor", interceptor, typeof(IInterceptor));

    info.AddValue("enabledFilters", enabledFilters, 
                   typeof(IDictionary<string, IFilter>));
    info.AddValue("enabledFilterNames", enabledFilterNames, 
                   typeof(List<string>));

    info.AddValue("connectionManager", connectionManager, 
                   typeof(ConnectionManager));
  }
  .... 
  private string fetchProfile;
  ....
}

Предупреждение PVS-Studio: V3099 Not all the members of 'SessionImpl' type are serialized inside 'GetObjectData' method: fetchProfile. NHibernate SessionImpl.cs 141

На этот раз забыли сериализовать поле текущего класса (fetchProfile). Как видно из определения, оно не декорировано атрибутом [NonSerialized] (в отличии от других полей, не сериализуемых в методе GetObjectData).

В данном проекте нашлось ещё два подобных места:

  • V3099 Not all the members of 'Configuration' type are serialized inside 'GetObjectData' method: currentDocumentName, preMappingBuildProcessed. NHibernate Configuration.cs 127
  • V3099 Not all the members of 'ConnectionManager' type are serialized inside 'GetObjectData' method: flushingFromDtcTransaction. NHibernate ConnectionManager.cs 290

С подобными ошибками связана интересная особенность — они либо приводят к генерации исключения, либо к трудноуловимым логическим ошибкам.

Исключение будет сгенерировано в том случае, если в конструкторе сериализации попытаются получить значение того поля, которое не было добавлено (обратятся по отсутствующему ключу). Если же про член забыли совсем (и в методе GetObjectData, и в конструкторе сериализации), будет происходить порча состояния объекта.

Обобщение

Кратко обобщив всю информацию, приведённую выше, можно сформулировать несколько советов и правил:

  • Декорируйте атрибутом [Serializable] типы, реализующие интерфейс ISerializable;
  • Убедитесь, что все сериализуемые члены декорированы атрибутом [Serializable] и корректно сериализуются;
  • Реализуя интерфейс ISerializable, не забудьте реализовать конструктор сериализации (Ctor(SerializationInfo, StreamingContext));
  • В запечатанных типах установите модификатор доступа private для конструктора сериализации, в незапечатанных — protected;
  • В незапечатанных типах, реализующий интерфейс ISerializable, сделайте метод GetObjectData виртуальным;
  • Проверьте, что в методе GetObjectData сериализуются все необходимые члены, включая члены базового типа, если он есть.

Как не наступить на грабли, работая с сериализацией - 5

Заключение

Надеюсь, вы узнали что-то новое из статьи и стали большим экспертом в вопросах сериализации. Придерживаясь приведённых выше советов и правил, вы сможете тратить меньше времени на отладку, облегчить жизнь себе и другим разработчикам, работающим с вашими классами. А анализатор PVS-Studio ещё больше облегчит жизнь, позволяя выявлять подобные ошибки сразу после их появления.

Дополнительная информация

  • V3094. Possible exception when deserializing type. The Ctor(SerializationInfo, StreamingContext) constructor is missing
  • V3096. Possible exception when serializing type. [Serializable] attribute is missing
  • V3097. Possible exception: type marked by [Serializable] contains non-serializable members not marked by [NonSerialized]
  • V3099. Not all the members of type are serialized inside 'GetObjectData' method
  • V3103. A private Ctor(SerializationInfo, StreamingContext) constructor in unsealed type will not be accessible when deserializing derived types
  • V3104. 'GetObjectData' implementation in unsealed type is not virtual, incorrect serialization of derived type is possible
  • MSDN. Serialization in the .NET Framework
  • MSDN. Custom Serialization

Как не наступить на грабли, работая с сериализацией - 6

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. How to not shoot yourself in the foot when working with serialization.

Прочитали статью и есть вопрос?

Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.

Автор: PVS-Studio

Источник

Поделиться новостью

* - обязательные к заполнению поля