Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ооп теория

.pdf
Скачиваний:
21
Добавлен:
14.02.2015
Размер:
3.58 Mб
Скачать

возвращаемый методом объект нельзя присвоить текущему объекту 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