Оптимизация сериализации в .NET

ОГЛАВЛЕНИЕ

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

Введение

Это первая из двух статей про оптимизацию сериализации, особенно для использования в удаленном взаимодействии.

Эта первая статья содержит универсальный код, который используется для сохранения 'принадлежащих данных' (определяются позже) в компактную структурную единицу с максимальной скоростью. Вторая статья приводит пример того, как использовать этот код для сериализации наборов данных как автономных блоков. Возможная третья статья расскажет, как сериализовать Entity и EntityCollections из LLBLGenPro - ведущей O/R программы управления памятью – как пример того, как использовать весь процесс сериализации для получения наилучших результатов. Хотя это написано для одного конкретного приложения, вы можете найти эти методы полезными и для своего кода.

Если вы использовали удаленное взаимодействие .NET для больших объемов данных, то наверняка обнаружили, что в нем есть проблемы с расширяемостью. Для небольших объемов данных оно работает достаточно хорошо, но большие объемы отнимают много ресурсов процессора и памяти, генерируют огромные объемы данных для передачи и могут привести к сбою из-за нехватки памяти. Также большой проблемой является время, требуемое для реального выполнения сериализации – большие объемы данных может быть нереально использовать в приложениях, независимо от затрат ресурсов процессора/памяти, просто потому что выполнение сериализации/десериализации отнимает очень много времени. Использование сжатия данных через приемники сервера и клиента может помочь уменьшить итоговый размер передаваемых данных, но не позволит устранить излишние объемы данных на более ранних этапах процесса.

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

'Априорное знание' – это ключевой момент здесь. Его можно использовать для оптимизации сохранения определенных 'принадлежащих данных' (определяются позже), оставив всю остальную работу .NET. Это и есть предмет данной статьи.

Позвольте привести пример возможного масштаба оптимизации:

Имеется набор справочных данных из 34,423 строк в таблице базы данных, который был сохранен в виде коллекции объектов-сущностей. Ее сериализация (с помощью MemoryStream для максимальной скорости) занимает целые 92 секунды, и на выходе получается 13.5Мб сериализованных данных. Их десериализация отнимает примерно 58 секунд – не очень удобно для сценария удаленного взаимодействия!

Используя методы, описанные в данной статье, можно при сериализации тех же самых данных получить на выходе 2.1Мб, что отнимет всего 0.35 секунд на сериализацию и 0.82 секунды на десериализацию! Доля использования ресурсов процессора и памяти будет небольшой частью от тех, которые использует исходный сериализатор .NET.

Использование кода

Как указано во введении, загружаемый код достаточно универсален, поэтому в нем, по сути,  не содержится ничего специфического для удаленного взаимодействия. По существу, вы помещаете 'принадлежащие' данные в экземпляр класса SerializationWriter и затем используете метод ToArray, чтобы получить byte[],SerializationInfo,ISerializable.GetObjectData(), как обычно, таким образом: содержащий сериализованные данные. Затем его можно сохранить в параметре передаваемом методу

public virtual void GetObjectData(SerializationInfo info,
                                  StreamingContext context)
{
    SerializationWriter writer = new SerializationWriter();
    writer.Write(myInt32Field);
    writer.Write(myDateTimeField);
    writer.Write(myStringField);
    // и т.д.
    info.AddValue("data", writer.ToArray());
}

Десериализация, по существу, обратный процесс: в конструкторе десериализации вы извлекаете byte[] и создаете экземпляр SerializationReader, передавая byte[] его конструктору. Затем данные извлекаются в том самом же порядке, как они были записаны:

protected EntityBase2(SerializationInfo info, StreamingContext context)
{
    SerializationReader reader =
      new SerializationReader((byte[]) info.GetValue("data",
                               typeof(byte[])));
    myInt32Field = reader.ReadInt32();
    myDateTimeField = reader.ReadDateTime();
    myString = reader.ReadString();
    // и т.д.
}

Просто скопируйте файл FastSerializer.cs в ваш проект, измените пространство имен соответственно, и он будет готов к использованию. Прикрепленные к статье файлы также включают в себя файл FastSerializerTest.cs  с более чем 220 тестами, но в этом нет особой необходимости, и вам стоит его добавить только в том случае, если вы хотите изменить код с гарантией того, что ничего не будет испорчено.


Принадлежащие данные

Раньше упоминалась концепция 'принадлежащих данных', теперь определим ее: принадлежащие данные - это данные объекта, которые:

  • Любой тип значения/структура данных, такая как Int32 или Byte или Boolean и т.д. Так как типы значения являются структурами и восстанавливаются, когда они передаются по кругу, на данные любого типа внутри вашего объекта не может повлиять другой объект, и поэтому сериализовать/десериализовать их всегда безопасно.
  • Строки. Хотя они имеют ссылочный тип, они неизменяемы (их нельзя изменить после создания), поэтому имеют семантику типа значения, и сериализовать их всегда безопасно.
  • Другие ссылочные типы, созданные или переданные в ваш объект, которые никогда не используются внешними объектами. Они могут включать внутренние или приватные Hashtable, ArrayList, Array и т.д., потому что они не доступны для внешних объектов. Сюда могут входить объекты, которые были созданы как внешние и переданы для использования исключительно вашему объекту.
  • Другие ссылочные типы (снова созданные вашим объектом или переданные ему), которые могут использоваться другими объектами, но вы знаете, что это не вызовет проблемы во время десериализации. Проблема здесь в том, что ваш объект сам не знает о том, что другие объекты могут быть сериализованы в том же самом графе объекта. Поэтому если сериализуется коллективно используемый объект в byte[] путем использования  SerializationWriter, и тот же самый коллективно используемый объект был сериализован другим внешним объектом с помощью использования его SerializationWriter, то два экземпляра могут оказаться десериализованными – другой экземпляр для каждого, – потому что инфраструктура сериализации никогда не сможет обнаружить и обработать многократные ссылки.

Это может быть или не быть проблемой, в зависимости от коллективно используемого объекта. Если коллективно используемый объект был неизменяемым/не имел эксплуатационных данных, то наличие двух экземпляров, созданных во время десериализации, хотя и неэффективное, не вызовет проблемы. Но если присутствовали эксплуатационные данные, и они должны были коллективно использоваться двумя объектами, ссылающимися на них, то это может быть проблемой, потому что каждый теперь имеет свою собственную независимую копию. Наихудший сценарий – когда совместно используемый объект хранит обратные ссылки на объект, который ссылается на него, то есть риск появления цикла, который быстро вызовет ошибку сериализации с исключением OutOfMemoryException или StackOverflow. Это является проблемой, только если объекты с многократными ссылками сериализуются в один и тот же граф. Если один объект является частью графа, то объект, на который он ссылается, можно рассматривать как 'принадлежащие данные' – другие ссылки станут несущественными, – но определение и разрешение данной ситуации является вашей задачей.

Нижняя строка нужна, чтобы убедиться, что только тщательно идентифицированные ' принадлежащие данные' сохраняются внутри SerializationWriter, и позволить инфраструктуре сериализации .NET выполнить остальную работу.

Как это работает (вкратце)

SerializationWriter имеет определенное число перегруженных методов Write(xxx) для определенного числа типов. Он также имеет набор методов WriteOptimized(xxx), которые могут сохранить определенные типы более оптимальным образом, но могут иметь некоторые ограничения на сохраняемые значения (которые документированы для метода). Для данных, которые неизвестны во время компиляции, есть метод WriteObject(object), который сохранит тип данных, так же как и значение, так что SerializationReader знает, как снова восстановить данные. Тип данных сохраняется с помощью использования одиночного байта, который основан на внутреннем перечислении, называемом SerializedType.

SerializationReader имеет определенное число методов, соответствующих своим аналогам в SerializationWriter. Они не могут быть перегружены таким же образом, поэтому каждый является отдельным методом, с именем, описывающим его использование. Например, строка, записанная с помощью Write(string), будет извлечена с помощью ReadString(), WriteOptimized(Int32) будет извлечен с помощью ReadOptimizedInt32(), и WriteObject (object) будет извлечен с помощью ReadObject(), и так далее. Пока эквивалентный метод для извлечения данных вызывается для SerializationReader и, что важно, в таком же порядке, то вы получите назад точно такие те же данные, которые были записаны.


Как это работает (полный вариант)

Методы Write(xxx) сохраняют данные, используя нормальный размер для типа, поэтому Int32 всегда занимает 4 байта, а Double всегда занимает 8 байтов и т.д. Методы WriteOptimized(xxx) используют другой метод сохранения, который зависит не только от типа, но также и от его значения. Поэтому Int32, который меньше 128, может быть сохранен в единственный байт (с помощью 7-битного кодирования), но значения Int32WriteOptimized(). 268,435,456 или больше, или отрицательные числа нельзя сохранить с помощью этого метода (иначе они бы заняли 5 байтов, поэтому это нельзя было бы считать оптимизацией), но если вы хотите сохранить значение, например число элементов в списке, которое никогда не будет отрицательным и никогда не достигнет предела, то в этом случае можно использовать метод

DateTime – это другой тип, который имеет метод оптимизации. Его ограничения в том, что он не может оптимизировать значение DateTime с большей точностью, чем до миллисекунд, но в общем случае, когда дата сохраняется без времени, она займет не больше 3 байтов в потоке. (DateTime с hh:mm, не имеющий секунд, займет 5 байтов – все еще намного лучше, чем 8 байтов, занимаемые методом Write(DateTime).) Метод WriteObject() использует перечисление SerializedType, чтобы описать тип объекта, следующего в потоке. Перечисление определяется как byte, что дает нам 256 возможных значений. Каждый базовый тип принимает одно из этих значений, но 256 – это довольно много, поэтому ряд из них следует применить для 'кодирования' известных значений в их типе. Так что каждый числовой тип также имеет версию 'ноль' (некоторые имеют  также версию 'минус один'), что позволяет задавать тип и значение с помощью всего лишь одного байта. Это дает возможность сериализовать в компактном виде объекты, содержащие много данных, тип которых неизвестен во время компиляции.

Так как строки используются в значительной степени, нужно обеспечить, чтобы они всегда были оптимизированы (строки не имеют перегруженного метода WriteOptimized(string), потому что они всегда оптимизированы!). С этой целью мы выделили 128 из 256 значений для использования строками – фактически списками строк. Это позволяет записать любую строку using с помощью метки списка строк (состоящего из byte плюс оптимизированный Int32), чтобы убедиться, что значение данной строки записано один и только один раз – если строка встречается несколько раз, то только метка списка строк сохраняется несколько раз. Если мы сделаем доступными 128 списков строк, каждый из которых имеет мало строк, вместо одного списка строк, содержащего много строк, то метка строки будет занимать 1 байт для первых 16,256 уникальных строк, соответственно можно занять 2 байта для последующих 2,080,768 уникальных строк! Этого должно быть достаточно в любом случае! Особое внимание было уделено генерации нового списка, как только текущий список достигнет 127 в размере, чтобы воспользоваться преимуществами метки строки меньшего размера – как только все 128 возможных списков созданы, им назначаются строки методом циклического перебора. Строки помечаются, только если они длиннее двух символов - Null, Empty, 'Y', 'N', или один пробел имеют свои собственные SerializedTypeCode, а другие 1-символьные строки займут 2 байта (1 для типа, и 1 для самого символа).

Другое большое преимущество использования меток строки в том, что во время десериализации в памяти сохраняется только один экземпляр данной строки. Хотя сериализатор .NET делает то же самое для ссылки на ту же самую строку, он не делает этого в случае, когда ссылки разные, но значение одинаковое. Это часто происходит во время чтения таблиц баз данных, содержащих одинаковое строковое значение в столбце – потому что они поступают через DataReader в различные моменты, они имеют одинаковое значение, но разные ссылки, и сериализуются много раз. Это не имеет значения для SerializationWriter – он использует Hashtable для идентификации одинаковых строк независимо от их ссылок. Поэтому десериализованный граф обычно использует память более эффективно, чем граф перед сериализацией.

Массивом объектов также уделяется особое внимание, поскольку они широко используются при работе с базами данных, как часть DataTable или классы сущностей. Их содержимое записывается с помощью метода WriteObject(), чтобы сохранить тип и/или оптимизированное значение, но для них есть специальные методы оптимизации, такие как поиск последовательностей нулей или DBNull.Value. Где выявляется последовательность, там записывается SerializationType, идентифицирующий последовательность (1 байт), за которым следует длина последовательности (обычно 1 байт). Также есть перегруженный метод, названный WriteOptimized(object[], object[]), который принимает два объекта-массива одинаковой длины, такие, какие вы можете найти в измененном DataRow или в измененной сущности; первый object[]записывается, как описано выше, но значения во втором списке сравниваются с их эквивалентами в первом, и там, где значения идентичны, это обозначается специальным типом SerializationType, таким образом,сокращая размер пары одинаковых значений до одного байта независимо от их обычного размера при хранении.

Во время разработки SerializationWriter мы столкнулись с необходимостью сериализации объекта (фабрики класса), который должен использоваться многими сущностями в коллекции. Хотя эта фабрика класса не имеет собственных данных, сериализовать нужно только ее тип, но было бы полезно убедиться, что каждая сущность использует одну и ту же фабрику класса во время десериализации. Для этого мы снабжаем метками каждый объект: использование WriteTokenizedObject (object) помещает метку в поток, чтобы представлять этот объект, а сам объект будет сериализован позже, после того как метки строки будут сериализованы. Мы также добавили перегрузку этого метода.

Для обеспечения большей компактности, если ваш объект может быть восстановлен с помощью конструктора без параметров, используйте перегрузку WriteTokenizedObject(object, true): он сохранит имя типа как строку, и SerializationReader восстановит его, используя Activator.GetInstance.

Есть одно свойство, доступное для настройки: SerializationWriter.OptimizeForSize (которое по умолчанию равняется true) управляет тем, должен ли WriteObjectMethod() использовать оптимизацию сериализации там, где возможно. Поскольку он должен проверять, находится ли значение в пределах параметров, необходимых для оптимизации, то сериализация занимает немного больше времени. Установка свойства в false позволит обойти эти проверки и использовать быстрый и простой метод. В реальности эти проверки не будут заметны для маленьких наборов данных и займут только несколько дополнительных миллисекунд для больших наборов данных (десятки мегабайт), поэтому обычно данное свойство можно не менять. Помеченные данные записываются в поток только после завершения сериализации. Метод ToArray реализует это путем добавления всех таблиц строк и записи смещения данных этой таблицы в первые 4 байта потока. Таким образом, десериализатор может прочитать первые 4 байта, переустановить указатель потока на таблицу данных и прочитать их непосредственно. Положение потока затем изменяется обратно на байт 5, и десериализация происходит в таком порядке, в каком данные были сериализованы. Все оптимизации полностью документированы в коде, особенно требования к оптимизациям, которые вы должны видеть как пояснение действия.


Золотые правила оптимизации

Здесь приводится список важных советов по оптимизации сериализации:

  1. Знайте, какие у вас данные: Зная ваши данные, как они используются, вероятный диапазон значений, какой диапазон значений возможен и т.д., вы можете определить 'принадлежащие' данные, что поможет вам выбрать подходящие методы (или иначе, чтобы не использовать никакие методы, и сериализовать данные непосредственно в блок SerializationInfo). Всегда есть неоптимизированный метод, доступный для любого простого типа данных, но использование оптимизированной версии дает наилучшие результаты.
  2. Читайте данные в том же порядке, в каком вы их записывали: Потому что мы превращаем данные в потоковые, а не связываем их с именем, поэтому их необходимо повторно считывать в точно таком же порядке, в каком они записывались. Вы не увидите в этом большой проблемы – процесс быстро закончится неудачей, если есть проблема с упорядочиванием, и это выявится во время разработки.
  3. Не сериализуйте что-либо, если в этом нет необходимости: Вы не можете лучше оптимизировать данные, которые занимают ноль байтов! Как пример, смотрите «Методы оптимизации» ниже в этой статье.
  4. Обдумайте, можно ли дочерние объекты считать 'принадлежащими данными': Например, если вы сериализуете коллекцию, и ее содержимое считается частью коллекции, или отдельные объекты сериализованы по отдельности и только снабжены ссылками? Это соображение может сильно повлиять на размер сериализованных данных, поскольку если образец был равен истине, то единственный SerializationWriter эффективно используется многими объектами одновременно, и эффект от разметки строк может быть очень существенным. Как пример, смотрите часть 2 этой статьи.
  5. Помните, что процесс сериализации – это черный ящик: Сериализуемые данные не должны быть в том же формате, что и в памяти. До конца десериализации данные находятся в том же состоянии, что и перед сериализацией, поэтому неважно, что происходит с ними за это время. Для оптимизации нужно сериализовать достаточно данных, чтобы иметь возможность восстановить объекты на другом конце. Как пример, смотрите «Методы оптимизации» позже в этой статье.

Перечисление SerializedType

Таблица ниже показывает используемые в настоящий момент значения SerializedType. 128 зарезервированы для таблиц строк и 70 перечислены ниже, оставляя 58 доступными для другого использования.

NullType

Используется для всех нулевых значений

NullSequenceType

Используется внутренне для идентификации последовательностей нулевых значений в массивах объектов

DBNullType

Используется для всех экземпляров DBNull.Value

DBNullSequenceType

Используется внутренне для идентификации последовательностей DBNull.Value в массивах объектов (DataSet широко используют это значение)

OtherType

Используется для всех неопознанных типов

BooleanTrueType BooleanFalseType

Для логического типа и значений

ByteType SByteType CharType DecimalType DoubleType SingleType Int16Type Int32Type Int64Type UInt16Type UInt32Type UInt64Type

Стандартные типы числовых значений

ZeroByteType ZeroSByteType ZeroCharType ZeroDecimalType ZeroDoubleType ZeroSingleType ZeroInt16Type ZeroInt32Type ZeroInt64Type ZeroUInt16Type ZeroUInt32Type ZeroUInt64Type

Оптимизация хранения числового типа и нулевого значения

OneByteType OneSByteType OneDecimalType OneDoubleType OneSingleType OneInt16Type OneInt32Type OneInt64Type OneUInt16Type OneUInt32Type OneUInt64Type

Оптимизация хранения числового типа и значения один

MinusOneInt16Type MinusOneInt32Type MinusOneInt64Type

Оптимизация хранения числового типа и значения минус один

OptimizedInt32Type OptimizedInt64Type OptimizedInt64Type OptimizedUInt64Type

Сохраняет 32- и 64-битные типы, используя наименьшее возможное число байтов  (смотрите код на предмет наличия ограничений)

EmptyStringType SingleSpaceType SingleCharStringType YStringType NStringType

Оптимизация для эффективного хранения односимвольных строк (1 или 2 байта)

ObjectArrayType ByteArrayType CharArrayType

Оптимизации для обычных типов массивов

DateTimeType MinDateTimeType MaxDateTimeType

Структура DateTime с часто используемыми значениями

TimeSpanType ZeroTimeSpanType

Структура TimeSpan с часто используемыми значениями

GuidType EmptyGuidType

Структура GUID с часто используемыми значениями

BitVector32Type

Оптимизация для хранения BitVector32 в 1 - 4 байтах

DuplicateValueType

Используется внутренне при сохранении пары массивов объектов

BitArrayType

Оптимизация для хранения BitArrays

TypeType

Сохраняет Тип как строку (использует полное AssemblyQualifiedName для несистемных Типов)

SingleInstanceType

Используется внутренне для обозначения, что помеченный объект должен восстанавливаться с помощью Activator.GetInstance()

ArrayListType

Оптимизация для ArrayList


Методы оптимизации

Мы имеем идентифицированные 'принадлежащие данные', и убедились, что их можно сохранить, используя меньше байтов, чем составляет их реальный размер в памяти, используя метки и известные значения, но можно ли сделать что-то еще для улучшения оптимизации? Безусловно. Давайте посмотрим на пример золотого правила #3 – Не сериализуйте что-либо, если в этом нет необходимости:

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

Возьмите класс BitVector32: малоизвестный класс, который вам поможет здесь. Смотрите полную информацию в документации, но по существу это структура, занимающая 4 байта, которую можно использовать любым из двух способов (но не двумя одновременно!) – можно использовать ее 32 бита для хранения 32 логических флагов, или можно выделить разделы в несколько битов для размещения в них данных (оптимизация DateTime в SerializationWriter использует этот метод, поэтому посмотрите ее код). В режиме логического флага не имеет смысла определять, которые биты данных были действительно сохранены, и во время десериализации ваш код может проверять флаги, и считывать предполагаемые данные, или выполнять другое действие, где другое действие будет использовать значение по умолчанию, или создавать пустой объект, или ничего не делать (например, значение по умолчанию может быть уже создано в конструкторе).

Другие выгоды от использования BitVector32 в том, что логические значения данных сохраняются как один бит, и BitVector32 может храниться в оптимизированном виде (при условии, что используется не больше 21 бита – в противном случае используйте Write(BitVector32) для фиксированных 4 байтов), чтобы  BitVector32,BitArray, который использует один бит для одного элемента (округляется до ближайшего байта), но может сохранить много битов. использующий меньше 8 флагов, занимал только один байт! Аналогично, если вам нужно использовать много флагов, например, если у вас есть большой список объектов, и вам нужно сохранить логический флаг для каждого, то используйте

Как пример полезности битовых флагов, здесь приводится пример кода из быстрого сериализатора DataSetBitVector32.CreateMask(), который перегружен, чтобы связывать последующие маски с предыдущими. Они статические и доступны только для чтения, поэтому эффективно используют память. Данный набор флагов предназначен для DataColumn: он занимает 2 байта для одного сериализованного столбца, но учтите, что некоторые данные, такие как AllowNull и ReadOnly, уже сериализованы с помощью самого флага, и другие данные сейчас будут сериализованы только при определенных условиях. По сути, один битовый флаг (HasAutoIncrement) используется для сериализации трех элементов данных при определенных условиях (AutoIncrement, AutoIncrementSeed и AutoIncrementStep). serializer, о котором будет написано в части 2: Флаги создаются с помощью использования метода

static readonly int MappingTypeIsNotElement

= BitVector32.CreateMask();
static readonly int AllowNull = BitVector32.CreateMask(MappingTypeIsNotElement);
static readonly int HasAutoIncrement = BitVector32.CreateMask(AllowNull);
static readonly int HasCaption = BitVector32.CreateMask(HasAutoIncrement);
static readonly int HasColumnUri = BitVector32.CreateMask(HasCaption);
static readonly int ColumnHasPrefix = BitVector32.CreateMask(HasColumnUri);
static readonly int HasDefaultValue = BitVector32.CreateMask(ColumnHasPrefix);
static readonly int ColumnIsReadOnly =
                BitVector32.CreateMask(HasDefaultValue);
static readonly int HasMaxLength = BitVector32.CreateMask(ColumnIsReadOnly);
static readonly int DataTypeIsNotString = BitVector32.CreateMask(HasMaxLength);
static readonly int ColumnHasExpression =
                BitVector32.CreateMask(DataTypeIsNotString);
static readonly int ColumnHasExtendedProperties =
                BitVector32.CreateMask(ColumnHasExpression);

static BitVector32 GetColumnFlags(DataColumn dataColumn)
{
  BitVector32 flags = new BitVector32();
  flags[MappingTypeIsNotElement] =
        dataColumn.ColumnMapping != MappingType.Element;
  flags[AllowNull] = dataColumn.AllowDBNull;
  flags[HasAutoIncrement] = dataColumn.AutoIncrement;
  flags[HasCaption] = dataColumn.Caption != dataColumn.ColumnName;
  flags[HasColumnUri] = ColumnUriFieldInfo.GetValue(dataColumn) != null;
  flags[ColumnHasPrefix] = dataColumn.Prefix != string.Empty;
  flags[HasDefaultValue] = dataColumn.DefaultValue != DBNull.Value;
  flags[ColumnIsReadOnly] = dataColumn.ReadOnly;
  flags[HasMaxLength] = dataColumn.MaxLength != -1;
  flags[DataTypeIsNotString] = dataColumn.DataType != typeof(string);
  flags[ColumnHasExpression] = dataColumn.Expression != string.Empty;
  flags[ColumnHasExtendedProperties] =
        dataColumn.ExtendedProperties.Count != 0;
  return flags;
}

Здесь есть методы, которые используют флаги для сериализации/десериализации всех столбцов в DataTable. Вы можете увидеть флаги, используемые для объединения сериализации дополнительных данных с обязательными данными, такими как ColumnName, и данными по умолчанию, такими как DataType, причем эти данные всегда обязательны, но требуют сериализации, если они не выбраны нами по умолчанию (в этом случае typeof(string)).

void SerializeColumns(DataTable table)
{
  DataColumnCollection columns = table.Columns;
  writer.WriteOptimized(columns.Count);

  foreach(DataColumn column in columns)
  {
    BitVector32 flags = GetColumnFlags(column);
    writer.WriteOptimized(flags);

    writer.WriteString(column.ColumnName);
    if (flags[DataTypeIsNotString])
        writer.Write(column.DataType.FullName);
    if (flags[ColumnHasExpression])
        writer.Write(column.Expression);
    if (flags[MappingTypeIsNotElement])
        writer.WriteOptimized((int) MappingType.Element);

    if (flags[HasAutoIncrement]) {
      writer.Write(column.AutoIncrementSeed);
      writer.Write(column.AutoIncrementStep);
    }

    if (flags[HasCaption]) writer.Write(column.Caption);
    if (flags[HasColumnUri])
        writer.Write((string) ColumnUriFieldInfo.GetValue(column));
    if (flags[ColumnHasPrefix]) writer.Write(column.Prefix);
    if (flags[HasDefaultValue]) writer.WriteObject(column.DefaultValue);
    if (flags[HasMaxLength]) writer.WriteOptimized(column.MaxLength);
    if (flags[TableHasExtendedProperties])
        SerializeExtendedProperties(column.ExtendedProperties);
  }
}
void DeserializeColumns(DataTable table)
{
  int count = reader.ReadOptimizedInt32();
  DataColumn[] dataColumns = new DataColumn[count];
  for(int i = 0; i < count; i++)
  {
    DataColumn column = null;
    string columnName;
    Type dataType;
    string expression;
    MappingType mappingType;

    BitVector32 flags = reader.ReadOptimizedBitVector32();
    columnName = reader.ReadString();
    dataType = flags[DataTypeIsNotString] ?
               Type.GetType(reader.ReadString()) :
               typeof(string);
    expression = flags[ColumnHasExpression] ?
                 reader.ReadString() : string.Empty;
    mappingType = flags[MappingTypeIsNotElement] ?
                  (MappingType) reader.ReadOptimizedInt32() :
                  MappingType.Element;

    column = new DataColumn(columnName, dataType,
                            expression, mappingType);
    column.AllowDBNull = flags[AllowNull];
    if (flags[HasAutoIncrement]) {
        column.AutoIncrement = true;
        column.AutoIncrementSeed = reader.ReadInt64();
        column.AutoIncrementStep = reader.ReadInt64();
    }
    if (flags[HasCaption])
        column.Caption = reader.ReadString();
    if (flags[HasColumnUri])
        ColumnUriFieldInfo.SetValue(column, reader.ReadString());
    if (flags[ColumnHasPrefix])
        column.Prefix = reader.ReadString();
    if (flags[HasDefaultValue])
        column.DefaultValue = reader.ReadObject();
    column.ReadOnly = flags[ColumnIsReadOnly];
    if (flags[HasMaxLength])
        column.MaxLength = reader.ReadOptimizedInt32();
    if (flags[TableHasExtendedProperties])
        DeserializeExtendedProperties(column.ExtendedProperties);

    dataColumns[i] = column;
  }
  table.Columns.AddRange(dataColumns);
}

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

Автор: SimmoTech

Загрузить файлы версии v1.0 - 19.2 Kb (только .NET 1.1)

Загрузить файлы версии v2.0 - 39.3 Kb (.NET 1.1 / NET 2.0)

Загрузить файлы версии v2.1 - 50.9 Kb (.NET 1.1 / NET 2.0)