
ооп теория
.pdfвозвращаемый методом объект нельзя присвоить текущему объекту this.
Важно также отметить, что метод Deserialize восстанавливает весь граф объектов, возвращая в качестве результата корень графа.
В классе определен еще один метод, сообщающий о текущем состоянии объектов:
public void About()
{
Console.WriteLine("имя |
= |
{0}, |
возраст = |
{1},"+ |
||||
"статус |
= |
{2}, |
состояние |
={3}",name,age,status, wealth); |
||||
Console.WriteLine("имя |
= |
{0}, |
возраст = |
{1}," + |
||||
"статус |
= |
{2}, |
состояние |
={3}", |
this.couple.name, |
this.couple.age,this.couple.status, this.couple.wealth);
}
Для завершения сказки нам нужно в клиентском классе создать ее героев:
public void TestGoldFish()
{
Personage fisher = new Personage("рыбак", 70); Personage wife = new Personage("старуха", 70); fisher.marry(wife);
Console.WriteLine("До золотой рыбки"); fisher.About(); fisher = fisher.AskGoldFish(); Console.WriteLine("Первое желание"); fisher.About();
fisher = fisher.AskGoldFish(); Console.WriteLine("Второе желание"); fisher.About();
fisher = fisher.AskGoldFish(); Console.WriteLine("Третье желание"); fisher.About();
fisher = fisher.AskGoldFish(); |
|
Console.WriteLine("Еще хочу"); fisher.About(); |
|
fisher = fisher.AskGoldFish(); |
|
Console.WriteLine("Хочу, но уже поздно"); |
fisher.About(); |
} |
|
На рис. 19.6 показаны результаты исполнения сказки. |
|
371

Рис. 19.6. Сказка о рыбаке и рыбке
Что изменится, если перейти к сохранению данных в xml-формате? немногое. Нужно лишь заменить объявление форматера:
void SaveStateXML()
{
SoapFormatter sf = new SoapFormatter(); FileStream fs = new FileStream
("State.xml",FileMode.Create, FileAccess.Write); sf.Serialize(fs,this);
fs.Close();
}
void BackStateXML(ref Personage fisher)
{
SoapFormatter sf = new SoapFormatter(); FileStream fs = new FileStream
("State.xml",FileMode.Open, FileAccess.Read); fisher = (Personage)sf.Deserialize(fs); fs.Close();
}
Клиент, работающий с объектами класса, этих изменений и не почувствует. Результаты вычислений останутся теми же, что и в предыдущем случае. Правда, файл, сохраняющий данные, теперь выглядит совсем по-другому. Это обычный xml-документ, который мог быть создан в любом из приложений. Вот как выглядит этот документ, открытый в браузере Internet
Explorer.
372

Рис. 19.7. XML-документ, сохраняющий состояние объектов
Интерфейс ISerializable
При необходимости можно самому управлять процессом сериализации. В
этом случае наш класс должен быть наследником интерфейса ISerializable.
Класс, наследующий этот интерфейс, должен реализовать единственный метод этого интерфейса GetObjectData и добавить защищенный конструктор.
Схема сериализации и десериализации остается и в этом случае той же самой. Можно использовать как бинарный форматер, так и soap-форматер.
Но теперь метод Serialize использует не стандартную реализацию, а вызывает метод GetObjectData, управляющий записью данных. Метод Deserialize, в
свою очередь, вызывает защищенный конструктор, создающий объект и заполняющий его поля сохраненными значениями.
Конечно, возможность управлять сохранением и восстановлением данных дает большую гибкость и позволяет, в конечном счете, уменьшить размер
373
файла, хранящего данные, что может быть крайне важно, особенно если речь идет об обмене данными с удаленным приложением. Если речь идет о поверхностной сериализации, то атрибут NonSerialized, которым можно помечать поля, не требующие сериализации, как правило, достаточен для управления эффективным сохранением данных. Так что управлять имеет смысл только глубокой сериализацией, когда сохраняется и восстанавливается граф объектов. Но, как уже говорилось, это может быть довольно сложным занятием, что будет видно и для нашего простого примера с рыбаком и рыбкой.
Рассмотрим, как устроен метод GetObjectData, управляющий сохранением данных. У этого метода два аргумента:
GetObjectData(SerializedInfo info, StreamingContext context)
Поскольку самому вызывать этот метод не приходится - он вызывается автоматически методом Serialize, то можно не особенно задумываться о том,
как создавать аргументы метода. Более важно понимать, как их следует использовать. Чаще всего используется только аргумент info и его метод
AddValue (key, field). Данные сохраняются вместе с ключом, используемым позже при чтении данных. Аргумент key, который может быть произвольной строкой, задает ключ, а аргумент field - поле объекта. Например, для сохранения полей name и age можно задать следующие операторы:
info.AddValue("name",name); info.AddValue("age", age);
Поскольку имена полей уникальны, то их разумно использовать в качестве ключей.
Если поле son класса Father является объектом класса Child и этот класс сериализуем, то для сохранения объекта son следует вызвать метод:
son.GetObjectData(info, context)
Если не возникает циклов, причиной которых являются взаимные ссылки, то
особых сложностей с сериализацией и десериализацией не возникает.
374
Взаимные ссылки осложняют картину и требуют индивидуального подхода к решению. На последующем примере мы покажем, как можно справиться с этой проблемой в конкретном случае.
Перейдем теперь к рассмотрению специального конструктора класса. Он может быть объявлен с атрибутом доступа private, но лучше, как и во многих других случаях, применять атрибут protected, что позволит использовать этот конструктор потомками класса, осуществляющими собственную сериализацию. У конструктора те же аргументы, что и у метода
GetObjectData. Опять-таки, в основном используется аргумент info и его метод GetValue(key, type), который выполняет операцию, обратную к операции метода AddValue. По ключу key находится хранимое значение, а
аргумент type позволяет привести его к нужному типу. У метода GetValue
имеется множество типизированных версий, позволяющих не задавать тип.
Так что восстановление полей name и age можно выполнить следующими операторами:
name = info.GetString("name"); age = info.GetInt32("age");
Восстановление поля son, являющегося ссылочным типом, выполняется вызовом его специального конструктора:
son = new Child(info, context);
А теперь вернемся к нашему примеру со стариком, старухой и золотой рыбкой. Заменим стандартную сериализацию собственной. Для этого,
оставив атрибут сериализации у класса Personage, сделаем класс наследником интерфейса ISerializable:
[Serializable]
public class Personage :ISerializable {...}
Добавим в наш класс специальный метод, вызываемый при сериализации - метод сохранения данных:
//Специальный метод сериализации
375
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("name",name); info.AddValue("age", age); info.AddValue("status",status); info.AddValue("wealth", wealth); info.AddValue("couplename",couple.name); info.AddValue("coupleage", couple.age); info.AddValue("couplestatus",couple.status); info.AddValue("couplewealth", couple.wealth);
}
В трех первых строках сохраняются значимые поля объекта и тут все ясно.
Но вот запомнить поле, хранящее объект couple класса Personage, напрямую не удается. Попытка рекурсивного вызова
couple.GetObjectData(info,context);
привела бы к зацикливанию, если бы раньше из-за повторяющегося ключа не возникала исключительная ситуация в момент записи поля name объекта couple. Поэтому приходится явно сохранять поля этого объекта уже с другими ключами. Понятно, что с ростом сложности структуры графа объектов задача существенно осложняется.
Добавим в наш класс специальный конструктор, вызываемый при десериализации - конструктор восстановления состояния:
//Специальный конструктор сериализации protected Personage(SerializationInfo info,
StreamingContext context)
{
name = info.GetString("name"); age = info.GetInt32("age"); status = info.GetString("status");
wealth = info.GetString("wealth");
couple = new Personage(info.GetString("couplename"), info.GetInt32("coupleage"));
couple.status = info.GetString("couplestatus"); couple.wealth = info.GetString("couplewealth"); this.couple = couple; couple.couple = this;
}
Опять первые строки восстановления значимых полей объекта прозрачно ясны. А с полем couple приходится повозиться. Вначале создается новый объект обычным конструктором, аргументы которого читаются из
376

сохраняемой памяти. Затем восстанавливаются значения других полей этого объекта, а затем уже происходит взаимное связывание двух объектов.
Кроме введения конструктора класса и метода GetObjectData, никаких других изменений в проекте не понадобилось - ни в методах класса, ни на стороне клиента. Внешне проект работал совершенно идентично ситуации, когда не вводилось наследование интерфейса сериализации. Но с внутренних позиций изменения произошли: методы форматеров Serialize и Deserialize в процессе своей работы теперь вызывали созданный нами метод и конструктор класса.
Небольшие изменения произошли и в файлах, хранящих данные.
Мораль: должны быть веские основания для отказа от стандартно реализованной сериализации. Повторюсь, такими основаниями могут служить необходимость в уменьшении объема файла, хранящего данные, и в сокращении времени передачи данных.
Когда в нашем примере вводилось собственное управление сериализацией,
то не ставилась цель минимизации объема хранимых данных, в обоих случаях сохранялись одни и те же данные. Тем не менее представляет интерес взглянуть на таблицу, хранящую объемы создаваемых файлов.
Таблица 19.1. Размеры файлов при различных случаях сериализации
|
Формат |
|
Сериализация |
|
Размер файла |
|
|
|
|
|
|
||||
|
Бинарный поток |
|
Стандартная |
|
355 |
байтов |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Бинарный поток |
|
Управляемая |
|
355 |
байтов |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
XML-документ |
|
Стандартная |
|
1, 14 Кб. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
XML-документ |
|
Управляемая |
|
974 |
байта |
|
|
|
|
|
|
|
|
|
Преимуществами XML-документа являются его читабельность и хорошо развитые средства разбора, но зато бинарное представление выигрывает в объеме и скорости передачи тех же данных.
377

Тема 20. ФУНКЦИОНАЛЬНЫЙ ТИП В C#. ДЕЛЕГАТЫ СОДЕРЖАНИЕ ЛЕКЦИИ:
КАК ОПРЕДЕЛЯЕТСЯ ФУНКЦИОНАЛЬНЫЙ ТИП И КАК ПОЯВЛЯЮТСЯ ЕГО ЭКЗЕМПЛЯРЫ ФУНКЦИИ ВЫСШИХ ПОРЯДКОВ
o ВЫЧИСЛЕНИЕ ИНТЕГРАЛА
oПОСТРОЕНИЕ ПРОГРАММНЫХ СИСТЕМ МЕТОДОМ
"РАСКРУТКИ". ФУНКЦИИ ОБРАТНОГО ВЫЗОВА
oНАСЛЕДОВАНИЕ И ПОЛИМОРФИЗМ - АЛЬТЕРНАТИВА ОБРАТНОМУ ВЫЗОВУ
o ДЕЛЕГАТЫ КАК СВОЙСТВА
o ОПЕРАЦИИ НАД ДЕЛЕГАТАМИ. КЛАСС DELEGATE o ОПЕРАЦИИ "+" И "-"
o ПРИМЕР "КОМБИНИРОВАНИЕ ДЕЛЕГАТОВ" o ПРИМЕР "ПЛОХАЯ СЛУЖБА"
378

КАК ОПРЕДЕЛЯЕТСЯ ФУНКЦИОНАЛЬНЫЙ ТИП И КАК ПОЯВЛЯЮТСЯ ЕГО ЭКЗЕМПЛЯРЫ
Слово делегат (delegate) используется в C# для обозначения хорошо известного понятия. Делегат задает определение функционального типа
(класса) данных. Экземплярами класса являются функции. Описание делегата в языке C# представляет собой описание еще одного частного случая класса. Каждый делегат описывает множество функций с заданной сигнатурой. Каждая функция (метод), сигнатура которого совпадает с сигнатурой делегата, может рассматриваться как экземпляр класса, заданного делегатом. Синтаксис объявления делегата имеет следующий вид:
[<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);
Этим объявлением класса задается функциональный тип - множество функций с заданной сигнатурой, у которых аргументы определяются списком, заданным в объявлении делегата, и тип возвращаемого значения определяется типом результата делегата.
Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:
непосредственно в пространстве имен, наряду с объявлениями других классов, структур, интерфейсов;
внутри другого класса, наряду с объявлениями методов и свойств.
Такое объявление рассматривается как объявление вложенного класса.
Так же, как и интерфейсы C#, делегаты не задают реализации.
Фактически между некоторыми классами и делегатом заключается контракт
379
на реализацию делегата. Классы, согласные с контрактом, должны объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Если контракт выполняется, то можно создать экземпляры делегата, присвоив им в качестве значений функции,
удовлетворяющие контракту. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у экземпляра соответствующий параметр имеет тип, согласованный с object,
например, int.
Начнем примеры этой лекции с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс:
namespace Delegates
{
//объявление классов - делегатов delegate void Proc(ref int x); delegate void MesToPers(string s); class OwnDel
{
public delegate int Fun1(int x);
int Plus1( int x){return(x+100);}//Plus1 int Minus1(int x){return(x-100);}//Minus1 void Plus(ref int x){x+= 100;}
void Minus(ref int x){x-=100;} //поля класса
public Proc p1; public Fun1 f1; char sign; //конструктор
public OwnDel(char sign)
{
this.sign = sign; if (sign == '+')
{p1 = new Proc(Plus);f1 = new Fun1(Plus1);} else
{p1 = new Proc(Minus);f1 = new Fun1(Minus1);}
}
}//class OwnDel
Прокомментирую этот текст.
380