□ Метод BindToMemberThenlnvokeTheMember демонстрирует привязку и последующий вызов.
□ Метод BindToMemberCreateDelegateToMemberThenlnvokeTheMember демонстрирует привязку с последующим созданием делегата, ссылающегося на объект или член. Вызов через делегата выполняется очень быстро, и этот вариант позволяет еще больше повысить производительность программного кода для случаев многократного вызова одинаковых членов разными объектами.
□ Метод UseDynamicToBindAndlnvokeTheMember демонстрирует использование в языке C# примитивного типа dynamic (см. главу 5) с целью упрощения синтаксиса доступа к членам. К тому же этот вариант может помочь добиться действительно хорошей производительности программного кода для случаев вызова одинаковых членов разными объектами, потому что связывание происходит один раз для каждого типа и затем кэшируется таким образом, чтобы последующий многократный вызов членов происходил быстро. Вы также можете использовать этот вариант с целью вызова членов для объектов различных типов.
using System;
using System.Reflection;
using Microsoft.CSharp.RuntimeBinder;
using System.Linq;
// Класс для демонстрации отражения.
// У него есть поле, конструктор, метод, свойство и событие internal sealed class SomeType { private Int32 m_someField; public SomeType(ref Int32 x) { x *= 2; }
public override String ToStringQ { return msomeField.ToString(); } public Int32 SomeProp {
get { return m_someField; } set {
if (value < 1)
throw new ArgumentOutOfRangeException("value"); m_someField = value;
}
public event EventHandler SomeEvent;
private void NoCompilerWarnings() { SomeEvent.ToStringQ;}
}
public static class Program { public static void Main() {
Type t = typeof(SomeType);
BindToMemberThenlnvokeTheMember(t);
Console.Write Line();
BindToMemberCreateDelegateToMemberThenlnvokeTheMember(t); Console.Write Line();
UseDynamicToBindAndlnvokeTheMember(t);
Console.Write Line();
private static void BindToMemberThenInvokeTheMember(Type t) {
Console .Write Line ("" BindToMemberThenln vokeTheMember");
// Создание экземпляра
Type ctorArgument = Type.GetType("System.Int32&");
II или typeof(Int32).MakeByRefTypeQ;
Constructorlnfo ctor = t.GetTypeInfo().DeclaredConstructors.First( c => c.GetParameters()[0].ParameterType == ctorArgument);
0bject[] args = new Object[] { 12 }; // Аргументы конструктора
Console.WriteLine("x before constructor called: " + args[0]);
Object obj = ctor.Invoke(args);
Console.WriteLine("Type: " + obj.GetType());
Console.WriteLine("x after constructor returns: " + args[0]);
// Чтение и запись в поле
Fieldlnfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField"); fi.SetValue(obj, 33);
Console.WriteLine("someField: " + fi.GetValue(obj));
// Вызов метода
Methodlnfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); String s = (String)mi.Invoke(obj, null);
Console.WriteLine("ToString: " + s);
// Чтение и запись свойства
Propertylnfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomePropn);
try {
pi.SetValue(obj, 0, null);
}
catch (TargetlnvocationException e) {
if (e.InnerException.GetTypeQ != typeof(ArgumentOutOfRangeException)) throw; Console.WriteLine("Property set catch.");
}
pi.SetValue(obj, 2, null);
Console.WriteLine("SomeProp: " + pi.GetValue(obJj null));
// Добавление и удаление делегата для события
Eventlnfo ei = obj.GetTypeQ .GetTypelnfoQ .GetDeclaredEvent("SomeEvent"); EventHandler eh = new EventHandler(EventCallback); // Cm. ei.EventHandlerType ei.AddEventHandler(obj, eh); ei.RemoveEventHandler(obj, eh);
// Добавление метода обратного вызова для события
private static void EventCallback(Object sender, EventArgs e) { }
private static void BindToMemberCreateDelegateToMemberThenInvokeTheMember(Type t) {
Console.Write Line("BindToMemberCreateDelegateToMemberThenlnvokeTheMember");
// Создание экземпляра (нельзя создать делегата для конструктора)
Object[] args = new ObJect[] { 12 }; // Аргументы конструктора
Console.WriteLine("x before constructor called: " + args[0]);
Object obj = Activator.Createlnstance(t, args);
Console.WriteLine("Type: " + obj .GetTypeQ . ToString());
Console.WriteLine("x after constructor returns: " + args[0]);
// ВНИМАНИЕ: нельзя создать делегата для поля.
// Вызов метода
Methodlnfo mi = obj .GetType() .GetTypelnfoQ .GetDeclaredMethod("ToString"); var toString = mi.CreateDelegate<Func<String>>(obj);
String s = toString();
Console.WriteLine("ToString: " + s);
// Чтение и запись свойства
Propertylnfo pi = obj.GetTypeQ .GetTypelnfoQ .GetDeclaredProperty("SomeProp"); var setSomeProp = pi.SetMethod.CreateDelegate<Action<Int32>>(obj); try {
setSomeProp(0);
}
catch (ArgumentOutOfRangeException) {
Console.WriteLine("Property set catch.");
}
setSomeProp(2);
var getSomeProp = pi.GetMethod.CreateDelegate<Func<Int32>>(obj);
Console.WriteLine("SomeProp: " + getSomeProp());
// Добавление и удаление делегата для события
Eventlnfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); var addSomeEvent = ei.AddMethod.CreateDelegate<Action<EventHandler>>(obJ); addSomeEvent(EventCallback);
var removeSomeEvent =
ei.RemoveMethod.CreateDelegate<Action<EventHandler>>(obj); removeSomeEvent(EventCallback);
private static void UseDynamicToBindAndInvokeTheMember(Type t) { Console.Write Line("UseDynamicToBindAndlnvokeTheMember");
// Создание экземпляра (dynamic нельзя использовать для вызова конструктора) Object[] args = new Object[] { 12 }; // Аргументы конструктора
Console.WriteLine("x before constructor called: " + args[0]); dynamic obj = Activator.Createlnstance(t; args);
Console.WriteLine(MType: " + obj.GetTypeQ . ToStringQ);
Console.WriteLine("x after constructor returns: " + args[0]);
// Чтение и запись поля
try {
obj.m_someField = 5;
Int32 v = (Int32)obj.m_someField;
Console.WriteLine("someField: " + v);
}
catch (RuntimeBinderException e) {
// Получает управление; потому что поле является приватным Console.WriteLine("Failed to access field: " + e.Message);
}
// Вызов метода
String s = (String)obj.ToStringQ;
Console.WriteLine("ToString: " + s);
// Чтение и запись свойства try {
obj.SomeProp = 0;
}
catch (ArgumentOutOfRangeException) {
Console.WriteLine("Property set catch.");
}
obj.SomeProp = 2;
Int32 val = (Int32)obj.SomeProp;
Console.WriteLine("SomeProp: " + val);
// Добавление и удаление делегата для события obj.SomeEvent += new EventHandler(EventCallback); obj.SomeEvent = new EventHandler(EventCallback);
>
>
internal static class ReflectionExtensions {
// Метод расширения; упрощающий синтаксис создания делегата
public static TDelegate CreateDelegate<TDelegate>(this Methodlnfo mi;
Object target = null) {
return (TDelegate)(Object)mi.CreateDelegate(typeof(TDelegate), target);
>
>
Если построить и запустить этот код, будет выведен следующий результат:
BindToMemberThenlnvokeTheMember х before constructor called: 12 Type: SomeType
x after constructor returns: 24
someField: 33 ToString: 33 Property set catch.
SomeProp: 2
BindToMemberCreateDelegateToMemberThenlnvokeTheMember x before constructor called: 12 Type: SomeType
x after constructor returns: 24 ToString: 0 Property set catch.
SomeProp: 2
UseDynamicToBindAndlnvokeTheMember x before constructor called: 12 Type: SomeType
x after constructor returns: 24
Failed to access field: 'SomeType.msomeFleld' is inaccessible due to its protection level ToString: 0 Property set catch.
SomeProp: 2
Обратите внимание: в единственном параметре конструктора SomeType по ссылке передается Int32. В представленном коде показано, как вызвать этот конструктор и как после завершения конструктора проверить модифицированное значение Int32. Далее в начале метода BindToMemberThenlnvokeTheMember присутствует вызов метода GetType типа Туре, которому передается строка "System. Int32&". Амперсанд (&) в строке обозначает параметр, передаваемый по ссылке. Это предусмотрено нотацией Бэкуса-Наура для записи имен типов (подробнее о ней см. документацию на FCL). В коде также показано, как добиться того же результата с использованием метода MakeByRefType класса Туре.
Использование дескрипторов привязки для снижения потребления памяти процессом
Во многих приложениях требуется привязка к нескольким типам (то есть объектам Туре) или их членам (объектам, производным от Memberlnfo), а эти объекты сохраняются в определенной коллекции. Позже приложение ищет нужный объект в коллекции и вызывает его. Это разумное решение, но есть одна загвоздка: объекты Туре и объекты, производные от Memberlnfo, занимают много места в памяти. Поэтому если в приложении много таких объектов и к ним надо обращаться часто, объем потребляемой памяти резко возрастает, что отрицательно сказывается на производительности.
Внутренние механизмы CLR поддерживают более компактную форму хранения этой информации. CLR создает такие объекты в приложениях лишь для того, чтобы упростить работу программиста. Самой среде CLR для работы эти большие объекты
не нужны. В приложениях, в которых сохраняется и кэшируется много объектов Туре и объектов-потомков Memberlnfo, можно сократить потребление памяти, если использовать не объекты, а описатели времени выполнения. В FCL определены три типа таких описателей (все в пространстве имен System): RuntimeTypeHandle, RuntimeFieldHandle и RuntimeMethodHandle. Все они — значимые типы с единственным полем IntPtr; за счет чего расходуют очень мало ресурсов (то есть памяти). Поле IntPtr представляет собой дескриптор, ссылающийся на тип, поле или метод в куче загрузчика домена приложений. Так что теперь нам достаточно научиться просто и эффективно преобразовывать «тяжелые» объекты Туре и Memberlnfo в «легкие» дескрипторы времени выполнения, и наоборот. Это не сложно, если задействовать перечисленные далее методы и свойства.
□ Чтобы преобразовать объект Туре в RuntimeTypeHandle, вызовите статический метод GetTypeHandle объекта Туре, передав ему ссылку на объект Туре.
□ Чтобы преобразовать RuntimeTypeHandle в объект Туре, вызовите статический метод GetTypeFromHandle объекта Туре, передав ему RuntimeTypeHandle.
□ Чтобы преобразовать объект Fieldlnfo в RuntimeFieldHandle, запросите эк- земплярное неизменяемое свойство FieldHandle объекта Fieldlnfo.
□ Чтобы преобразовать RuntimeTypeHandle в объект Fieldlnfo, вызовите статический метод GetTypeFromHandle объекта Fieldlnfo.
□ Чтобы преобразовать объект Methodlnfo в RuntimeMethodHandle, запросите экземплярное неизменяемое свойство MethodHandle объекта Methodlnfo.
□ Чтобы преобразовать RuntimeTypeHandle в объект Methodlnfo, вызовите статический метод GetMethodFromHandle объекта Methodlnfo.
В приведенной далее программе создается много объектов Methodlnfo, которые преобразуются в экземпляры RuntimeMethodHandle, а затем выводится информация о разнице в объеме потребляемой памяти:
using System;
using System.Reflection;
using System.Collections.Generic;
public sealed class Program {
private const BindingFlags c_bf = BindingFlags.FlattenHierarchy |
BindingFlags.Instance |
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
public static void Main() {
// Выводим размер кучи до отражения Show("Before doing anything");
// Создаем кэш объектов Methodlnfo для всех методов из MSCorlib.dll
List<MethodBase> methodlnfos = new List<MethodBase>();
foreach (Type t in typeof(Object).Assembly.GetExportedTypes()) {
// Игнорируем обобщенные типы
if (t.IsGenericTypeDefinition) continue;
MethodBase[] mb = t.GetMethods(c_bf); methodlnfos.AddRange(mb);
}
// Выводим количество методов и размер кучи после привязки всех методов Console.Writel_ine("# of methods={0:N0}", methodlnfos.Count);
Show("After building cache of Methodlnfo objects");
// Создаем кэш дескрипторов RuntimeMethodHandles // для всех объектов Methodlnfo List<RuntimeMethodHandle> methodHandles =
methodInfos.ConvertAll<RuntimeMethodHandle>(mb => mb.MethodHandle);
Show("Holding Methodlnfo and RuntimeMethodHandle cache");
GC.KeepAlive(methodlnfos); // Запрещаем уборку мусора в кэше
methodlnfos = null; // Разрешаем уборку мусора в кэше Show("After freeing Methodlnfo objects");
methodlnfos = methodHandles.ConvertAll<MethodBase>( rmh=> MethodBase.GetMethodFromHandle(rmh));
Show("Size of heap after re-creating Methodlnfo objects");
GC.KeepAlive(methodHandles); // Запрещаем уборку мусора в кэше
GC.KeepAlive(methodlnfos); // Запрещаем уборку мусора в кэше
methodHandles = null; // Разрешаем уборку мусора в кэше
methodlnfos = null; // Разрешаем уборку мусора в кэше
Show("After freeing Methodlnfos and RuntimeMethodHandles");
}
При построении и выполнении этой программы выводится следующий результат:
Heap size= 85,000 - Before doing anything # of methods=48,467
Heap size= 7,065,632 - After building cache of Methodlnfo objects
Heap size= 7,453,496 - Holding Methodlnfo and RuntimeMethodHandle cache
Heap size= 6,732,704 - After freeing Methodlnfo objects
Heap size= 7,372,704 - Size of heap after re-creating Methodlnfo objects
Heap size= 192,232 - After freeing Methodlnfos and RuntimeMethodHandles
Глава 24. Сериализация
Сериализацией (serialization) называется процесс преобразования объекта или графа связанных объектов в поток байтов. Соответственно, обратное преобразование называется десериализацией (deserialization). Несколько примеров применения этого чрезвычайно полезного механизма:
□ Состояние приложения (граф объекта) можно легко сохранить в файле на диске или в базе данных и восстановить при следующем запуске приложения. ASP.NET сохраняет и восстанавливает состояние сеанса путем сериализации и десериализации.
□ Набор объектов можно скопировать в буфер и вставить в то же или в другое приложение. Этот подход используется в приложениях Windows Forms и Windows Presentation Foundation (WPF).
□ Можно клонировать набор объектов и сохранить как «резервную копию», пока пользователь работает с «основным» набором объектов.
□ Набор объектов можно легко передать по сети в процесс, запущенный на другой машине. Механизм удаленного взаимодействия платформы .NET Framework сериализует и десериализует объекты, продвигаемые по значению. Этаже технология используется при передаче объектов через границы домена (см. главу 22).
Кроме того, после сериализации объектов в поток байтов в памяти появляется возможность дополнительной обработки данных — например, шифрования и сжатия.
Неудивительно, что многие программисты проводят бесчисленные часы за разработкой кода, выполняющего эти операции. Однако при этом соответствующий код сложно и муторно писать, к тому же он подвержен ошибкам. Разработчикам приходится решать проблемы взаимодействия протоколов, несовпадения типов данных клиента и сервера (например, разный порядок следования байтов), обработки ошибок, ссылок одних объектов на другие, параметров in и out, массивов структур — и этот список можно продолжать бесконечно.
Впрочем, в .NET Framework существует встроенный механизм сериализации и десериализации. Это означает, что все упомянутые сложные проблемы уже решены средствами .NET Framework. Разработчик может использовать объекты до сериализации и после десериализации, а всю заботу о том, что происходит между этими двумя процедурами, на себя берет .NET Framework.
В этой главе рассказывается, как сервис сериализации и десериализации реализован в .NET Framework. Эти процедуры определены практически для всех типов данных, а следовательно, вам не придется предпринимать дополнительных усилий, чтобы сделать свои типы сериализуемыми. Впрочем, существуют и типы, для ко
торых подобная предварительная подготовка необходима. К счастью, механизм сериализации .NET Framework поддерживает расширение, и мы детально рассмотрим данный процесс, позволяющий выполнять различные операции при сериализации и десериализации объектов. К примеру, я покажу, как сериализовав одну версию объекта в файл на диске, десериализовать его потом в другую версию.
ПРИМЕЧАНИЕ
В этой главе в основном рассматривается технология сериализации в среде CLR, которая хорошо распознает типы данных CLR и умеет сериализовать поля объектов, помеченные модификаторами public, protected, internal и даже private, превращая их в сжатый двоичный поток и тем самым повышая производительность. Для сериализации типов данных CLR в поток ХМL требуется класс System.Runtime.Serialization. NetDataContractSerializer. Платформа .NET Framework предлагает и другие технологии сериализации, разработанные для взаимодействия между CLR-совместимыми и CLR-несовместимыми типами данных. В них используются классы System .Xml. Serialization.XmlSerializer и System.Runtime.Serialization.DataContractSerializer.
Практический пример сериализации/ десериализации
Рассмотрим такой код:
using System;
using System.Collections.Generic; using System.10;
using System.Runtime.Serialization.Formatters.Binary;
internal static class QuickStart { public static void Main() {
// Создание графа объектов для последующей сериализации в поток var obJectGraph = new List<String> {
"Jeff", "Kristin", "Aidan", "Grant" };
Stream stream = SerializeToMemory(obJectGraph);
// Обнуляем все для данного примера stream.Position = 0; obJectGraph = null;
// Десериализация объектов и проверка их работоспособности obJectGraph = (List<String>) DeserializeFromMemory(stream); foreach (var s in obJectGraph) Console.WriteLine(s);
}
private static MemoryStream SerializeToMemory(Ob]ect obJectGraph) {
// Конструирование потока, который будет содержать
продолжение
// сериализованные объекты MemoryStream stream = new MemoryStream();
// Задание форматирования при сериализации BinaryFormatter formatter = new BinaryFormatterQ;
II Заставляем модуль форматирования сериализовать объекты в поток formatter.Serialize(stream, objectGraph);
II Возвращение потока сериализованных объектов вызывающему методу return stream;
}
private static Object DeserializeFromMemory(Stream stream) {
// Задание форматирования при сериализации BinaryFormatter formatter = new BinaryFormatter();
// Заставляем модуль форматирования десериализовать объекты из потока return formatter.Deserialize(stream);
}
}
Видите, как просто! Метод SerializeToMemory создает объект System. 10.MemoryStream. Этот объект определяет, куда следует поместить сериализованный блок байтов. Затем метод конструирует объект BinaryFormatter (расположенный в пространстве имен System. Runtime. Serialization. Formatters. Binary). Модулем форматирования (formatter) называется тип (он реализует интерфейс System. Runtime.Serialization.IFormatter), умеющий сериализовать и десериализовать граф объектов. В библиотеку FCL включены два модуля форматирования: BinaryFormatter (используется в показанном фрагменте кода) и SoapFormatter (находящийся в пространстве имен System. Runtime. Serialization. Formatters. Soap и реализованный в сборке System. Runtime. Serialization. Formatters. Soap.dll).
ПРИМЕЧАНИЕ
Начиная с версии 3.5, в .NET Framework класс SoapFormatter считается устаревшим и не рекомендуется к использованию. Однако его имеет смысл применять при отладке кода сериализации, так какой создает доступный для чтения текст в формате XML. Если в выходном коде вы хотите воспользоваться механизмами XML-сериализации и XML-десериализации, обратитесь к классам XmlSerializer и DataContractSerializer.
Для сериализации графа объектов достаточно вызвать метод Serialize модуля форматирования и передать ему, во-первых, ссылку на объект потока ввода-вывода, во-вторых, ссылку на сериализуемый граф. Поток ввода-вывода указывает, куда следует поместить сериализуемые байты. Его роль может играть объект любого типа, производного от абстрактного базового класса System. 10.Stream. Это означает, что граф может быть сериализован в тип MemoryStream, FileStream, NetworkStream и т. п.
Во втором параметре метода Serialize является ссылка на объект любого типа: Int32, String, DateTime, Exception, List<String>, Dictionary<Int32,DateTime>
и т. п. Объект, на который ссылается параметр ob jectGraph, может, в свою очередь, содержать ссылки на другие объекты. К примеру, параметр ob jectGraph быть ссылкой на коллекцию объектов, которые, в свою очередь, могут ссылаться на другие объекты. Метод Serialize сериализует в поток все объекты графа.
Модуль форматирования «знает», как сериализовать весь граф, ссылаясь на описывающие тип каждого объекта метаданные. Для отслеживания состояния экземплярных полей в типе каждого объекта в процессе сериализации метод Serialize использует отражение. Если какие-то из полей ссылаются на другие объекты, метод сериализует и их.
Модули форматирования используют хорошо проработанные алгоритмы. Например, они знают, что каждый объект в графе должен сериализоваться всего один раз. Благодаря этому при перекрестной ссылке двух объектов модуль форматирования не входит в бесконечный цикл.
В моем методе SerializeToMemory при возвращении управления методом Serialize объект MemoryStream просто возвращается вызывающему коду. Приложение использует содержимое этого неструктурированного массива байтов тем способом, который считает необходимым. Например, оно может сохранить массив в файле, скопировать в буфер обмена, переслать по сети и т. п.
Метод DeserializeFromStream превращает поток ввода-вывода обратно в граф объекта. Эта операция еще проще сериализации. В моем коде был сконструирован объект BinaryFormatter, после чего оставалось вызвать его метод Deserialize. Этот метод берет в качестве параметра поток ввода-вывода и возвращает ссылку на корневой объект десериализованного графа.
При этом внутри метод Deserialize исследует содержимое потока, создает экземпляры всех обнаруженных в потоке объектов и инициализирует все их, присваивая им значения, которые граф объекта имел до сериализации. Обычно затем следует приведение ссылки на возвращенный методом Deserialize объект к типу, который ожидает увидеть приложение.
ПРИМЕЧАНИЕ
Перед вами интересный и полезный метод, использующий сериализацию для создания глубокой копии (клона) объекта:
private static Object DeepClone(Object original) {
// Создание временного потока в памяти
using (MemoryStream stream = new MemoryStream()) {
// Создания модуля форматирования для сериализации BinaryFormatter formatter = new BinaryFormatter();
// Эта строка описывается в разделе "Контексты потока ввода-вывода" formatter.Context = new StreamingContext(StreamingContextStates.Clone); [31] formatter.Serialize(stream, original);
// Возвращение к началу потока в памяти перед десериализацей stream.Position = в;
// Десериализация графа в новый набор объектов и возвращение // корня графа (детальной копии) вызывающему методу return formatter.Deserialize(stream);
}
Здесь я хотел бы добавить несколько замечаний. Во-первых, следует следить за тем, чтобы сериализация и десериализация производилась одним и тем же модулем форматирования. К примеру, недопустим код, в котором сериализация графа объекта производится модулем SoapFormatter, в то время как десериализацию осуществляет уже BinaryFormatter. Если метод Deserialize не в состоянии расшифровать содержимое потока, генерируется исключение System.Runtime.Serialization. SerializationException.
Во-вторых, хотелось бы упомянуть о возможности и полезности сериализации набора графов объектов в единый поток. Например, пусть у нас есть два определения классов:
[Serializable] internal sealed class Customer {/*...*/}
[Serializable] internal sealed class Order {/*...*/}
Тогда в основном классе нашего приложения можно определить следующие статические поля:
private static List<Customer> s_customers = new List<Customer>(); private static List<Order> s_pendingOrders = new List<Order>(); private static List<Order> s_processedOrders = new List<Order>();
Теперь при помощи показанного далее метода можно сериализовать состояние нашего приложения в единый поток:
private static void SaveApplicationState(Stream stream) {
// Конструирование модуля форматирования для сериализации BinaryFormatter formatter = new BinaryFormatterQ; [32]
BinaryFormatter formatter = new BinaryFormatter();
// Десериализация состояния приложения (выполняется в том же // порядке, что и сериализация)
s_customers = (List<Customer>) formatter.Deserialize(stream);
s_pendingOrders = (List<Order>) formatter.Deserialize(stream);
s_processedOrders = (List<Order>) formatter.Deserialize(stream);
}
Третий тонкий момент, на который хотелось бы указать, связан со сборками. При сериализации объекта в поток записываются полное имя типа и определяющая его сборка. По умолчанию BinaryFormatter выдает полный идентификатор сборки, в который входит ее полное имя (без расширения), номер версии, язык, региональные параметры и открытый ключ. Десериализуя объект, модуль форматирования берет идентификатор сборки и обеспечивает ее загрузку в выполняющийся домен при помощи метода Load класса System. Ref lection. Assembly (о нем шла речь в главе 23).
После загрузки сборки модуль форматирования ищет в нем тип, совпадающий с типом десериализованного объекта. Если сборка не содержит такого типа, генерируется исключение, и дальнейшая десериализация объектов не выполняется. При обнаружении же нужного типа создается его экземпляр, и поля этого экземпляра инициализируются значениями, содержащимися в потоке. В случаях, когда поля типа не полностью совпадают с именами полей, считанными из потока, генерируется исключение Serialization Exception и дальнейшая десериализация объектов приостанавливается. Механизмы, позволяющие переопределить такое поведение, мы обсудим чуть позже.
ВНИМАНИЕ
Некоторые расширяемые приложения используют для загрузки сборки метод Assembly. Load From, а затем конструируют объекты из найденных в загруженной сборке типов. Такие объекты могут без проблем сериализоваться в поток. Однако при обратной процедуре модуль форматирования пытается загрузить сборку, вызывая метод Load класса Assembly вместо метода LoadFrom. В большинстве случаев CLR не может найти файл сборки и генерирует исключение SerializationException. Такое поведение зачастую становится сюрпризом для разработчиков. Ведь после корректной сериализации они ожидают такой же корректной десериализации.
Если ваше приложение сериализует объекты, типы которых определены в сборке, загружаемой методом Assembly. Load From, рекомендую вам реализовать метод с сигнатурой, совпадающей с сигнатурой делегата System.ResolveEventHandler, а перед вызовом метода Deserialize зарегистрировать этот метод с событием AssemblyResolve класса System.AppDomain. (После того как метод Deserialize вернет управления, отмените регистрацию метода.) Теперь, если модуль форматирования не сможет загрузить сборку, CLR вызовет ваш метод ResolveEventHandler. В него будет передан идентификатор не загруженной сборки. Метод извлечет оттуда имя файла и использует его для построения маршрута доступа к нужной сборке. После этого будет вызван метод Assembly.LoadFrom, чтобы загрузить сборку и вернуть итоговую ссылку на нее из метода ResolveEventHandler.
Итак, мы рассмотрели основы сериализации и десериализации графов объектов. Пришло время узнать, каким образом определять собственные сериализуемые типы, а также рассмотреть механизмы, позволяющие получить дополнительный контроль над сериализацией и десериализацией.
Сериализуемые типы
В процессе проектирования типа разработчик должен принять сознательное решение, разрешать или нет сериализацию экземпляров типа. По умолчанию сериализация не допускается. К примеру, следующий код не работает ожидаемым образом:
internal struct Point { public Int32 x, y; }
private static void OptInSerialization() {
Point pt = new Point { x = 1, у = 2 }; using (var stream = new MemoryStream()) {
new BinaryFormatterQ.Serialize(stream, pt); // исключение
// SerializationException
}
}
Если запустить такой код, метод Serialize выдаст исключение System. Runtime. Serialization. SerializationException. Дело в том, что разработчик типа Point не указал в явном виде, что объекты данного типа можно сериализовать. Решить проблему можно при помощи настраиваемого атрибута System.SerializableAttribute, примененного к типу следующим образом (обратите внимание, что данный атрибут принадлежит пространству имен System, а не System.Runtime.Serialization):
[Serializable]
internal struct Point { public Int32 x, y; }
Если теперь снова скомпоновать и запустить приложение, все начнет нормально работать, а объекты типа Point будут сериализоваться в поток. Сериализуя граф, модуль форматирования проверяет способность к сериализации каждого из объектов. Если хотя бы один из объектов не поддерживает сериализацию, метод Serialize генерирует исключение SerializationException.
ПРИМЕЧАНИЕ
При сериализации графа объектов некоторые типы могут оказаться сериализуемыми, а некоторые — нет. По причинам, связанным с производительностью, модуль форматирования перед сериализацией не проверяет возможность этой операции для всех объектов. А значит, может возникнуть ситуация, когда некоторые объекты окажутся сериализованными в потокдо появления исключения SerializationException. В результате в потоке ввода-вывода оказываются поврежденные данные. Желательно, чтобы код приложения умел корректно восстанавливаться после такой ситуации. Этого можно добиться, например, сериализуя объекты сначала в MemoryStream. Если процедура для всех объектов пройдетуспешно, байты из MemoryStream можно скопировать в любой другой поток ввода-вывода (то есть в файл или в сеть).
Настраиваемый атрибут SerializableAttribute может применяться только к ссылочным типам (классам), значимым типам (структурам), перечислимым типам (перечислениям) и делегатам (имейте в виду, что перечислимые типы и делегаты всегда сериализуемы, поэтому к ним не нужно явно применять атрибут SerializableAttribute). Данный атрибут не наследуется производными типами. Соответственно, в следующем примере объект Person может быть сериализован, а вот объект Employee — нет:
[Serializable]
internal class Person { ... } internal class Employee : Person { ... }
Проблема может быть решена применением атрибута SerializableAttribute к типу Employee:
[Serializable]
internal class Person { ... }
[Serializable]
internal class Employee : Person { ... }
В этом случае проблема решалась легко. А вот обратная ситуация — когда требуется определить тип, производный от базового и не имеющий атрибута SerializableAttribute — уже не столь тривиальна, однако сделано намеренно; если базовый тип не допускает сериализации своих экземпляров, его поля нельзя будет подвергнуть этой процедуре, ведь базовый объект фактически является частью производного. Именно поэтому классу System.Object назначен атрибут SerializableAttribute.
ПРИМЕЧАНИЕ
В общем случае большинство типов лучше делать сериализуемыми. Хотя бы потому, что это дает пользователям большую гибкость. Однако следует учитывать, что при сериализации читаются все поля объекта вне зависимости оттого, с каким модификатором они были объявлены — public, protected, internal или private. Поэтому если тип содержит конфиденциальные или защищенные данные (например, пароли), вряд ли стоит делать его сериализуемым.
Если вы работаете с типом, не предназначенным для сериализации, а его исходный код недоступен, не все потеряно. Чуть позже я расскажу, как организовать сериализацию такого типа.
Управление сериализацией и десериализацией
После назначения типу настраиваемого атрибута SerializableAttribute все экземпляры его полей (открытые, закрытые, защищенные и т. и.) становятся сериализуемыми[33]. Впрочем, в типе можно указать некоторые экземпляры как не подлежащие сериализации. В общем случае это делается по двум причинам:
□ Поле содержит информацию, становящуюся недействительной после десериализации. Например, сюда относятся объекты, содержащие дескрипторы объектов ядра Windows (таких, как файлы, процессы, потоки, мьютексы, события, семафоры и т. п.). Их десериализация в другой процесс или на другую машину бессмысленна, так как дескрипторы ядра Windows привязаны к конкретному процессу.
□ Поля содержат легко обновляемую информацию. В этом случае сделав их не подлежащими сериализации, вы уменьшите объем передаваемых данных, а значит, повысите производительность.
В показанном далее фрагменте кода с помощью настраиваемого атрибута System. NonSerializedAttribute помечаются поля, не подлежащие сериализации (имейте в виду, что этот атрибут определен в пространстве имен System, а не System. Runtime.Serialization):
[Serializable] internal class Circle { private Double mradius;
[NonSerialized] private Double m_area;
public Circle(Double radius) { m_radius = radius;
m_area = Math.PI * m_radius * m_radius;
}
}
Объекты класса Circle допускают сериализацию, но модуль форматирования сериализует только значения поля m_radius. Значения поля m_area не сериализуются, так как этому полю был назначен атрибут NonSerializedAttribute. Его назначают только полям типа, но действие атрибута распространяется на поля и при наследовании другим типом. Конечно, в пределах типа он может быть назначен целому набору полей.
Предположим, в нашем коде объект Circle конструируется следующим образом:
Circle с = new Circle(10);
Полю m_area было присвоено значение, равное примерно 314,159. Но при сериализации объекта в поток ввода-вывода записывается только значение поля m_nadius, равное 10. Именно этого мы и добивались, но при десериализации потока обратно в объект Circle возникает проблема. Поле m_radius получит значение 10, а вот поле ш_агеа станет равным 0, а не 314,159!
Следующий фрагмент кода показывает, как решить эту проблему:
[Serializable] internal class Circle { private Double mradius;
[NonSerialized] private Double marea;
public Circle(Double radius) { m_radius = radius;
m_area = Math.PI * m_radius * mradius;
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context) { m_area = Math.PI * m_radius * mradius;
}
}
Теперь объект Circle снабжен методом с настраиваемым атрибутом System. Runtime .Serialization .OnDeserializedAttribute[34]. При десериализации экземпляра типа модуль форматирования проверяет наличие в типе метода с данным атрибутом. При дальнейшем вызове метода все сериализуемые поля получат корректные значения и к тому же станут доступными для дополнительных операций, без которых невозможна полная десериализация объекта.
В модифицированной версии объекта Circle я заставил метод OnDeserialized вычислить площадь круга на основе значения поля m_radius, помещая результат в поле m_area. В результате это поле получает нужное нам значение 314,159.
Кроме настраиваемого атрибута OnDeserializedAttribute в пространстве имен System. Runtime. Serialization определены также настраиваемые атрибуты OnSerializingAttribute, OnSerializedAttribute и OnDeserializingAttribute, применив которые к методам нашего типа мы получаем дополнительный контроль над сериализацией и десериализацией. Пример класса, применяющего к методу все эти атрибуты:
[Serializable] public class MyType {
Int32 хл у; [NonSerialized] Int32 sum;
продолжение &
public MyType(Int32 х, Int32 у) { this.x = х; this.у = у; sum = х + у;
}
[OnDeserializing]
private void OnDeserializing(StreamingContext context) {
11 Пример. Присвоение полям значений по умолчанию в новой версии типа
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context) {
// Пример. Инициализация временного состояния полей sum = х + у;
}
[OnSerializing]
private void OnSerializing(StreamingContext context) {
// Пример. Модификация состояния перед сериализацией
}
[OnSerialized]
private void OnSerialized(StreamingContext context) {
// Пример. Восстановление любого состояния после сериализации
}
Метод, определенный с этими четырьмя атрибутами, должен принимать единственный параметр StreamingContext (подробно мы поговорим о нем позже) и возвращать значение типа void. Имя метода может быть произвольным. Объявлять его следует с модификатором private, чтобы исключить вызов из обычного кода; модули форматирования имеют достаточный уровень прав, чтобы вызывать private-методы.
ПРИМЕЧАНИЕ
При сериализации набора объектов модуль форматирования сначала вызывает все методы объектов, помеченные атрибутом OnSerializing. Затем сериализуются поля всех объектов, последней наступает очередь методов объектов с атрибутом OnSerialized. Аналогично при десериализации набора объектов модуль форматирования вызывает сначала методы объектов, помеченные атрибутом OnDeserializing, затем десериализует поля объектов, в завершение вызывая методы объектов с атрибутом OnDeserialized.
Следует учитывать, что при десериализации, обнаружив тип с помеченным атрибутом OnDeserialized методом, модуль форматирования добавляет ссылку на этот объект во внутренний список. Этот список просматривается в обратном порядке после десериализации всех объектов, и модуль форматирования вызывает метод с атрибутом OnDeserialized для каждого объекта. При этом происходит корректное присвоение значения всем сериализуемым полям и предоставляется доступ ко всем необходимым для полной десериализации объекта операциям. Обратный порядок вызова методов обеспечивает сначала десериализацию внутренних объектов, а уж затем десериализуются включающие их в себя внешние объекты.
К примеру, представьте коллекцию (Hashtable или Dictionary), для управления набором элементов использующую хэш-таблицу. Тип объектов этой коллекции реализует метод с атрибутом OnDeserialized. Хотя коллекция десериализуется первой (перед ее элементами), указанный метод вызывается в конце (после аналогичных методов для всех элементов). Это позволяет элементам завершить десериализацию, корректно инициализировав все поля, и правильно вычислить хэш-код. Затем коллекция создает внутренний сегмент памяти и с помощью хэш-кода элементов помещает их в этот сегмент. Подробный пример с классом Dictionary вы найдете в этой главе далее.
Если сериализовать экземпляр типа, добавить к нему новое поле и попытаться десериализовать не содержащий этого поля объект, модуль форматирования выдаст исключение SerializationException с сообщением о том, что данные в десериализуемом потоке содержат неверное количество членов. Подобная проблема часто возникает при изменении версий, так как при переходе от старой версии к типу добавляются новые поля. К счастью, можно воспользоваться атрибутом System. Runtime.Serialization.OptionalFieldAttribute.
Атрибут OptionalFieldAttribute назначается каждому новому полю, добавляемому к типу. Встретив поле с таким атрибутом, модуль форматирования не генерирует исключение SerializationException, даже если данные в потоке не содержат такого поля.
Сериализация экземпляров типа
В этом разделе подробно рассматривается тема сериализации полей объекта. Эта тема поможет вам понять нетривиальные приемы сериализации и десериализации, которым посвящен остаток данной главы.
Для облегчения работы модуля форматирования в FCL включен тип FormatterServices из пространства имен System.Runtime.Serialization. Он обладает только статическими методами и не допускает создания экземпляров. Вот каким образом модуль форматирования автоматически сериализует объект, типу которого назначен атрибут SerializableAttribute:
1. Модуль форматирования вызывает метод GetSerializableMembers класса FormatterServices:
public static MemberInfo[] GetSerializableMembers(
Type type, StreamingContext context);
Для получения открытых и закрытых экземплярных полей (исключая поля с атрибутом NonSerializedAttribute) этот метод использует отражения. Он возвращает массив объектов Memberlnfo — по одному объекту на каждое сериализуемое экземплярное поле.
2. Полученный массив объектов System. Reflection .Memberlnfo передается статическому методу GetOb jectData класса FormatterServices:
public static Object[] GetObjectData(Object obj, MemberInfo[] members);
Этот метод возвращает массив Object, в котором каждый элемент определяет значение поля сериализованного объекта. Массивы Object и Memberlnfo параллельны. То есть нулевой элемент массива Object представляет собой значение члена, фигурирующего в массиве Membenlnfo под нулевым индексом.