Главная страница
Навигация по странице:

  • Обертывание

  • Рис. 19.1.

  • Коллизия имен

  • Рис. 19.3.

  • Упорядоченность объектов и интерфейс IComparable

  • Клонирование и интерфейс ICloneable

  • Класс с атрибутом сериализации

  • Формат Сериализация Размер файла

  • Разработка интерфейса на C#. Лекция 19 Интерфейсы. Множественное наследование


    Скачать 106.77 Kb.
    НазваниеЛекция 19 Интерфейсы. Множественное наследование
    АнкорРазработка интерфейса на C
    Дата24.03.2022
    Размер106.77 Kb.
    Формат файлаdocx
    Имя файлаlection_19_interfaces (1).docx
    ТипЛекция
    #414203


    Лекция 19: Интерфейсы. Множественное наследование
    Аннотация: Интерфейсы как частный случай класса. Множественное наследование. Проблемы. Множественное наследование интерфейсов. Встроенные интерфейсы. Интерфейсы IComparable, ICloneable, ISerializable. Поверхностное и глубокое клонирование и сериализация. Сохранение и обмен данными.

    Ключевые слова: слово, интерфейс, interface, класс, потомок, развернутый тип, множественное наследование, пробел, назначение интерфейсов, FCL, равенство, свойства объектов класса, обертывание, кастинг, коллизия имен, наследование от общего предка, две стратегии реализации интерфейса, склеивание методов, переименование, встроенный интерфейс, отношение порядка на объектах класса, приведение типов, перегрузка операций, клонирование, клон, поверхностное клонирование, глубокое клонирование, ICloneable, сериализация объектов, десериализация, завершение сеанса работы, рекурсивный обход, атрибут [Serializable], deserialize
    Интерфейсы

    Слово " интерфейс " многозначное и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом interface. В таком понимании интерфейс - это частный случай класса. Интерфейс представляет собой полностью абстрактный класс, все методы которого абстрактны. От абстрактного класса интерфейс отличается некоторыми деталями в синтаксисе и поведении. Синтаксическое отличие состоит в том, что методы интерфейса объявляются без указания модификатора доступа. Отличие в поведении заключается в более жестких требованиях к потомкам. Класс, наследующий интерфейс, обязан полностью реализовать все методы интерфейса. В этом - отличие от класса, наследующего абстрактный класс, где потомок может реализовать лишь некоторые методы родительского абстрактного класса, оставаясь абстрактным классом. Но, конечно, не ради этих отличий были введены интерфейсы в язык C#. У них значительно более важная роль.

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

    Подробнее о развернутых и ссылочных типах см. лекцию 17.

    Интерфейсы позволяют частично справиться с таким существенным недостатком языка, как отсутствие множественного наследования классов. Хотя реализация множественного наследования встречается с рядом проблем, его отсутствие существенно снижает выразительную мощь языка. В языке C# полного множественного наследования классов нет. Чтобы частично сгладить этот пробел, допускается множественное наследование интерфейсов. Обеспечить возможность классу иметь несколько родителей - один полноценный класс, а остальные в виде интерфейсов, - в этом и состоит основное назначение интерфейсов.
    Отметим одно важное назначение интерфейсов. Интерфейс позволяет описывать некоторые желательные свойства, которыми могут обладать объекты разных классов. В библиотеке FCL имеется большое число подобных интерфейсов, с некоторыми из них мы познакомимся в этой лекции. Все классы, допускающие сравнение своих объектов, обычно наследуют интерфейс IComparable, реализация которого позволяет сравнивать объекты не только на равенство, но и на "больше", "меньше".

    Две стратегии реализации интерфейса

    Давайте опишем некоторый интерфейс, задающий дополнительные свойства объектов класса:
    public interface IProps

    {

    void Prop1(string s);

    void Prop2 (string name, int val);

    }

    У этого интерфейса два метода, которые и должны будут реализовать все классы - наследники интерфейса. Заметьте, у методов нет модификаторов доступа.

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

    public class Clain:IProps

    {

    public Clain() {}

    public void Prop1(string s)

    {

    Console.WriteLine(s);

    }

    public void Prop2(string name, int val)

    {

    Console.WriteLine("name = {0}, val ={1}", name, val);

    }

    }//Clain

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

    public class ClainP:IProps

    {

    public ClainP(){ }

    void IProps.Prop1(string s)

    {

    Console.WriteLine(s);

    }

    void IProps.Prop2(string name, int val)

    {

    Console.WriteLine("name = {0}, val ={1}", name, val);

    }

    }//class ClainP

    Класс ClainP реализовал методы интерфейса IProps, но сделал их закрытыми и недоступными для вызова клиентами и наследниками класса. Как же получить доступ к закрытым методам? Есть два способа решения этой проблемы:

    • Обертывание. Создается открытый метод, являющийся оберткой закрытого метода.

    • Кастинг. Создается объект интерфейсного класса IProps, полученный преобразованием (кастингом) объекта исходного класса ClainP. Этому объекту доступны закрытые методы интерфейса.

    В чем главное достоинство обертывания? Оно позволяет переименовывать методы интерфейса. Метод интерфейса со своим именем закрывается, а потом открывается под тем именем, которое класс выбрал для него. Вот пример обертывания закрытых методов в классе ClainP:

    public void MyProp1(string s)

    {

    ((IProps)this).Prop1(s);

    }

    public void MyProp2(string s, int x)

    {

    ((IProps)this).Prop2(s, x);

    }

    Как видите, методы переименованы и получили другие имена, под которыми они и будут известны клиентам класса. В обертке для вызова закрытого метода пришлось использовать кастинг, приведя объект this к интерфейсному классу IProps.

    Преобразование к классу интерфейса


    Создать объект класса интерфейса обычным путем с использованием конструктора и операции new нельзя. Тем не менее, можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса - даже если они закрыты в классе, для интерфейсных объектов они являются открытыми. Приведу соответствующий пример, в котором идет работа как с объектами классов Clain, ClainP, так и с объектами интерфейсного класса IProps:

    public void TestClainIProps()

    {

    Console.WriteLine("Объект класса Clain вызывает

    открытые методы!");

    Clain clain = new Clain();

    clain.Prop1(" свойство 1 объекта");

    clain.Prop2("Владимир", 44);

    Console.WriteLine("Объект класса IProps вызывает

    открытые методы!");

    IProps ip = (IProps)clain;

    ip.Prop1("интерфейс: свойство");

    ip.Prop2 ("интерфейс: свойство",77);

    Console.WriteLine("Объект класса ClainP вызывает

    открытые методы!");

    ClainP clainp = new ClainP();

    clainp.MyProp1(" свойство 1 объекта");

    clainp.MyProp2("Владимир", 44);

    Console.WriteLine("Объект класса IProps вызывает

    закрытые методы!");

    IProps ipp = (IProps)clainp;

    ipp.Prop1("интерфейс: свойство");

    ipp.Prop2 ("интерфейс: свойство",77);

    }

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




    Рис. 19.1. Наследование интерфейса. Две стратегии
    Проблемы множественного наследования

    При множественном наследовании классов возникает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы - коллизию имен и наследование от общего предка.

    Коллизия имен

    Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Сразу же заметим, что если имена методов совпадают, но сигнатуры разные, то это не приводит к конфликтам - при реализации у класса наследника просто появляются перегруженные методы. Но что следует делать классу-наследнику в тех случаях, когда сигнатуры методов совпадают? И здесь возможны две стратегии - склеивание методов и переименование.

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

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

    Итак, коллизия имен при множественном наследовании интерфейсов хотя и возможна, но легко разрешается. Разработчик класса может выбрать любую из двух возможных стратегий, наиболее подходящую для данного конкретного случая. Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса - наследника этих интерфейсов, применяющего разные стратегии реализации для конфликтующих методов.
    public interface IProps

    {

    void Prop1(string s);

    void Prop2 (string name, int val);

    void Prop3();

    }

    public interface IPropsOne

    {

    void Prop1(string s);

    void Prop2 (int val);

    void Prop3();

    }

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

    public class ClainTwo:IProps,IPropsOne

    {

    ///

    /// склеивание методов двух интерфейсов

    ///


    ///

    public void Prop1 (string s)

    {

    Console.WriteLine(s);

    }

    ///

    /// перегрузка методов двух интерфейсов

    ///


    ///

    ///

    public void Prop2(string s, int x)

    {

    Console.WriteLine(s + "; " + x);

    }

    public void Prop2 (int x)

    {

    Console.WriteLine(x);

    }

    ///

    /// переименование методов двух интерфейсов

    ///


    void IProps.Prop3()

    {

    Console.WriteLine("Свойство 3 интерфейса 1");

    }

    void IPropsOne.Prop3()

    {

    Console.WriteLine("Свойство 3 интерфейса 2");

    }

    public void Prop3FromInterface1()

    {

    ((IProps)this).Prop3();

    }

    public void Prop3FromInterface2()

    {

    ((IPropsOne)this).Prop3();

    }

    }

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

    Приведу пример работы с объектами класса и интерфейсными объектами:

    public void TestCliTwoInterfaces()

    {

    Console.WriteLine("Объект ClainTwo вызывает методы двух интерфейсов!");

    Cli.ClainTwo claintwo = new Cli.ClainTwo();

    claintwo.Prop1("Склейка свойства двух интерфейсов");

    claintwo.Prop2("перегрузка ::: ",99);

    claintwo.Prop2(9999);

    claintwo.Prop3FromInterface1();

    claintwo.Prop3FromInterface2();

    Console.WriteLine("Интерфейсный объект вызывает методы 1-го

    интерфейса!");

    Cli.IProps ip1 = (Cli.IProps)claintwo;

    ip1.Prop1("интерфейс IProps: свойство 1");

    ip1.Prop2("интерфейс 1 ", 88);

    ip1.Prop3();

    Console.WriteLine("Интерфейсный объект вызывает методы 2-го

    интерфейса!");

    Cli.IPropsOne ip2 = (Cli.IPropsOne)claintwo;

    ip2.Prop1("интерфейс IPropsOne: свойство1");

    ip2.Prop2(7777);

    ip2.Prop3();

    }

    Результаты работы тестирующей процедуры показаны на рис. 19.2.




    Рис. 19.2. Решение проблемы коллизии имен


    Наследование от общего предка


    Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс C является наследником классов A и B, а те, в свой черед, являются наследниками класса Parent, то класс наследует свойства и методы своего предка Parent дважды, один раз получая их от класса A, другой от - B. Это явление называется еще дублирующим наследованием. Для классов ситуация осложняется тем, что классы A и B могли по-разному переопределить методы родителя и для потомков предстоит сложный выбор реализации.

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

    Начнем наш пример с наследования интерфейсов:

    public interface IParent

    {

    void ParentMethod();

    }

    public interface ISon1:IParent

    {

    void Son1Method();

    }

    public interface ISon2:IParent

    {

    void Son2Method();

    }

    Два сыновних интерфейса наследуют метод своего родителя. А теперь рассмотрим класс, наследующий оба интерфейса:

    public class Pars:ISon1, ISon2

    {

    public void ParentMethod()

    {

    Console.WriteLine("Это метод родителя!");

    }

    public void Son1Method()

    {

    Console.WriteLine("Это метод старшего сына!");

    }

    public void Son2Method()

    {

    Console.WriteLine("Это метод младшего сына!");

    }

    }//class Pars

    Класс обязан реализовать метод ParentMethod, приходящий от обоих интерфейсов. Понимая, что речь идет о дублировании метода общего родителя – интерфейса IParent, лучшей стратегией реализации является склеивание методов в одной реализации, что и было сделано. Приведу тестирующую процедуру, где создается объект класса и три объекта интерфейсных классов, каждый из которых может вызывать только методы своего интерфейса:

    public void TestIParsons()

    {

    Console.WriteLine("Объект класса вызывает методы трех

    интерфейсов!");

    Cli.Pars ct = new Cli.Pars();

    ct.ParentMethod();

    ct.Son1Method();

    ct.Son2Method();

    Console.WriteLine("Интерфейсный объект 1 вызывает свои

    методы!");

    Cli.IParent ip = (IParent)ct;

    ip.ParentMethod();

    Console.WriteLine("Интерфейсный объект 2 вызывает свои

    методы!");

    Cli.ISon1 ip1 = (ISon1)ct;

    ip1.ParentMethod();

    ip1.Son1Method();

    Console.WriteLine("Интерфейсный объект 3 вызывает свои

    методы!");

    Cli.ISon2 ip2 = (ISon2)ct;

    ip2.ParentMethod();

    ip2.Son2Method();

    }

    Результаты работы тестирующей процедуры показаны на рис. 19.3.




    Рис. 19.3. Дублирующее наследование интерфейсов

    Встроенные интерфейсы

    Рассмотрим несколько встроенных интерфейсов, являющихся частью библиотеки FCL. Они используются многими классами-библиотеками так же, как и классами, создаваемыми пользователем.

    Упорядоченность объектов и интерфейс IComparable

    Часто, когда создается класс, желательно задать отношение порядка на его объектах. Такой класс следует объявить наследником интерфейса IComparable. Этот интерфейс имеет всего один метод CompareTo (object obj), возвращающий целочисленное значение, положительное, отрицательное или равное нулю, в зависимости от выполнения отношения "больше", "меньше" или "равно".

    Как правило, в классе вначале определяют метод CompareTo, а после этого вводят перегруженные операции, чтобы выполнять сравнение объектов привычным образом с использованием знаков операций отношения.

    Давайте введем отношение порядка на классе Person, рассмотренном в лекции 16, сделав этот класс наследником интерфейса IComparable. Реализуем в этом классе метод интерфейса CompareTo:

    public class Person:IComparable

    {

    public int CompareTo( object pers)

    {

    const string s = "Сравниваемый объект не принадлежит классу Person";

    Person p = pers as Person;

    if (!p.Equals(null))

    return (fam.CompareTo(p.fam));

    throw new ArgumentException (s);

    }

    // другие компоненты класса

    }

    Поскольку аргумент метода должен иметь универсальный тип object, то перед выполнением сравнения его нужно привести к типу Person. Это приведение использует операцию as, позволяющую проверить корректность выполнения приведения.

    При приведении типов часто используются операции is и as. Логическое выражение (obj is T) истинно, если объект obj имеет тип T. Оператор присваивания (obj = P as T;) присваивает объекту obj объект P, приведенный к типу T, если такое приведение возможно, иначе объекту присваивается значение null. Семантику as можно выразить следующим условным выражением: (P is T) ? (T)P : (T)null.

    Заметьте также, что при проверке на значение null используется отношение Equals, а не обычное равенство, которое будет переопределено.

    Отношение порядка на объектах класса Person задается как отношение порядка на фамилиях персон. Так как строки наследуют интерфейс IComparable, то для фамилий персон вызывается метод CompareTo, его результат и возвращается в качестве результата метода CompareTo для персон. Если аргумент метода не будет соответствовать нужному типу, то выбрасывается исключение со специальным уведомлением.

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

    Введем теперь в нашем классе Person перегрузку операций отношения:

    public static bool operator <(Person p1, Person p2)

    {

    return (p1.CompareTo(p2) < 0);

    }

    public static bool operator >(Person p1, Person p2)

    {

    return (p1.CompareTo(p2) > 0);

    }

    public static bool operator <=(Person p1, Person p2)

    {

    return (p1.CompareTo(p2) <= 0);

    }

    public static bool operator >=(Person p1, Person p2)

    {

    return (p1.CompareTo(p2) >=0);

    }

    public static bool operator ==(Person p1, Person p2)

    {

    return (p1.CompareTo(p2) == 0);

    }

    public static bool operator !=(Person p1, Person p2)

    {

    return (p1.CompareTo(p2) != 0);

    }

    Как обычно, приведу тестовый пример, проверяющий работу с введенными методами:

    public void TestCompare()

    {

    Person poet1 = new Person("Пушкин");

    Person poet2 = new Person("Лермонтов");

    Person poet3 = new Person("Пастернак");

    Person poet4 = new Person("Мандельштам");

    Person poet5 = new Person("Ахматова");

    Person poet6 = new Person("Цветаева");

    Console.WriteLine("{0} > {1} = {2}", poet1.Fam,

    poet2.Fam, (poet1 > poet2));

    Console.WriteLine("{0} >= {1} = {2}", poet3.Fam,

    poet4.Fam, (poet3 >= poet4));

    Console.WriteLine("{0} != {1} = {2}", poet5.Fam,

    poet6.Fam, (poet5 != poet6));

    }

    Вот результаты работы этого теста.




    Рис. 19.4. Сравнение персон

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

    Клонирование и интерфейс ICloneable

    Клонированием называется процесс создания копии объекта, а копия объекта называется его клоном. Различают два типа клонирования: поверхностное (shallow) и глубокое (deep). При поверхностном клонировании копируется сам объект. Все значимые поля клона получают значения, совпадающие со значениями полей объекта; все ссылочные поля клона являются ссылками на те же объекты, на которые ссылается и сам объект. При глубоком клонировании копируется вся совокупность объектов, связанных взаимными ссылками. Представьте себе мир объектов, описывающих людей. У этих объектов могут быть ссылки на детей и родителей, учителей и учеников, друзей и родственников. В текущий момент может существовать большое число таких объектов, связанных ссылками. Достаточно выбрать один из них в качестве корня, и при его клонировании воссоздастся копия существующей структуры объектов.

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

    Поверхностное клонирование можно выполнить достаточно просто. Наиболее простой путь – клонирование путем вызова метода MemberwiseClone, наследуемого от прародителя object. Единственное, что нужно помнить: этот метод защищен, он не может быть вызван у клиента класса. Поэтому клонирование нужно выполнять в исходном классе, используя прием обертывания метода.

    Давайте обеспечим эту возможность для класса Person, создав в нем соответствующий метод:

    public Person StandartClone()

    {

    Person p = (Person)this.MemberwiseClone();

    return(p);

    }

    Теперь клиенты класса могут легко создавать поверхностные клоны. Вот пример:

    public void TestStandartClone()

    {

    Person mother = new Person("Петрова Анна");

    Person daughter = new Person("Петрова Ольга");

    Person son = new Person("Петров Игорь");

    mother[0] = daughter;

    mother[1] = son;

    Person mother_clone = mother.StandartClone();

    Console.WriteLine("Дети матери: {0}",mother.Fam);

    Console.WriteLine (mother[0].Fam);

    Console.WriteLine (mother[1].Fam);

    Console.WriteLine("Дети клона: {0}",mother_clone.Fam);

    Console.WriteLine (mother_clone[0].Fam);

    Console.WriteLine (mother_clone[1].Fam);

    }

    При создании клона будет создана копия только одного объекта mother. Обратите внимание: при работе с полем children, задающим детей, используется индексатор класса Person, выполняющий индексацию по этому полю. Вот как выглядят результаты работы теста.




    Рис. 19.5. Поверхностное клонирование

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

    Давайте расширим наш класс, придав ему родительский интерфейс ICloneable. Реализация метода Clone будет отличаться от стандартной реализации тем, что к имени объекта – полю Fam - будет приписываться слово "clone". Вот как выглядит этот метод:

    public object Clone()

    {

    Person p = new Person(this.fam + "_clone");

    //копирование полей

    p.age = this.age; p.children = this.children;

    p.count_children = this.count_children;

    p.health = this.health; p.salary = this.salary;

    p.status = this.status;

    return (p);

    }

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

    Person mother_clone2 = (Person)mother.Clone();

    Console.WriteLine("Дети клона_2: {0}",mother_clone2.Fam);

    Console.WriteLine (mother_clone2[0].Fam);

    Console.WriteLine (mother_clone2[1].Fam);

    Все работает должным образом.

    Сериализация объектов


    При работе с программной системой зачастую возникает необходимость в сериализации объектов. Под сериализацией понимают процесс сохранения объектов в долговременной памяти (файлах) в период выполнения системы. Под десериализацией понимают обратный процесс - восстановление состояния объектов, хранимых в долговременной памяти. Механизмы сериализации C# и Framework .Net поддерживают два формата сохранения данных - в бинарном файле и XML-файле. В первом случае данные при сериализации преобразуются в бинарный поток символов, который при десериализации автоматически преобразуется в нужное состояние объектов. Другой возможный преобразователь (SOAP formatter) запоминает состояние объекта в формате xml.

    Сериализация позволяет запомнить рубежные состояния системы объектов с возможностью последующего возвращения к этим состояниям. Она необходима, когда завершение сеанса работы не означает завершение вычислений. В этом случае очередной сеанс работы начинается с восстановления состояния, сохраненного в конце предыдущего сеанса работы. Альтернативой сериализации является работа с обычной файловой системой, с базами данных и другими хранилищами данных. Поскольку механизмы сериализации, предоставляемые языком C#, эффективно поддерживаются .Net Framework, то при необходимости сохранения данных значительно проще и эффективнее пользоваться сериализацией, чем самому организовывать их хранение и восстановление.

    Еще одно важное применение сериализации - это обмен данными удаленных систем. При удаленном обмене данными предпочтительнее формат xml из-за открытого стандарта передачи данных в Интернете по soap-протоколу, из-за открытого стандарта на структуру xml-документов. Обмен становится достаточно простым даже для систем, построенных на разных платформах и в разных средах разработки.

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

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

    Класс с атрибутом сериализации

    Класс, объекты которого предполагается сериализовать стандартным образом, должен при объявлении сопровождаться атрибутом [Serializable]. Стандартная сериализация предполагает два способа сохранения объекта: в виде бинарного потока символов и в виде xml-документа. В бинарном потоке сохраняются все поля объекта, как открытые, так и закрытые. Процессом этим можно управлять, помечая некоторые поля класса атрибутом [NonSerialized] - эти поля сохраняться не будут:

    [Serializable]

    public class Test

    {

    public string name;

    [NonSerialized] int id;

    int age;

    //другие поля и методы класса

    }

    В класс Test встроен стандартный механизм сериализации его объектов. При сериализации поля name и age будут сохраняться, поле id - нет.

    Для запуска механизма необходимо создать объект, называемый форматером и выполняющий сериализацию и десериализацию данных с подходящим им форматированием. Библиотека FCL предоставляет два класса форматеров. Бинарный форматер, направляющий данные в бинарный поток, принадлежит классу BinaryFormatter. Этот класс находится в пространстве имен библиотеки FCL:

    System.Runtime.Serialization.Formatters.Binary

    Давайте разберемся, как устроен этот класс. Он является наследником двух интерфейсов: IFormatter и IRemotingFormatter. Интерфейс IFormatter имеет два открытых метода: Serialize и Deserialize, позволяющих сохранять и восстанавливать всю совокупность связанных объектов с заданным объектом в качестве корня. Интерфейс IRemotingFormatter имеет те же открытые методы: Serialize и Deserialize, позволяющие выполнять глубокую сериализацию, но в режиме удаленного вызова. Поскольку сигнатуры одноименных методов интерфейсов отличаются, то конфликта имен при наследовании не происходит - в классе BinaryFormatter методы Serialize и Deserialize перегружены. Для удаленного вызова задается дополнительный параметр, что и позволяет различать, локально или удаленно выполняются процессы обмена данными.

    В пространстве имен библиотеки FCL:

    System.Runtime.Serialization.Formatters.Soap

    находится класс SoapFormatter. Он является наследником тех же интерфейсов IFormatter и IRemotingFormatter и реализует их методы Serialize и Deserialize, позволяющие выполнять глубокую сериализацию и десериализацию при сохранении данных в формате xml. Помимо методов класса SoapFormatter, xml-сериализацию можно выполнять средствами другого класса -- XmlSerializer.

    Из новых средств, еще не рассматривавшихся в наших лекциях, для организации сериализации понадобятся файлы. Пространство имен IO библиотеки FCL предоставляет классы, поддерживающие ввод-вывод данных. В частности, в этом пространстве есть абстрактный класс Stream для работы с потоками данных. С одним из его потомков – классом FileStream - мы и будем работать в нашем примере.

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

    [Serializable]

    public class Personage

    {

    public Personage(string name, int age)

    {

    this.name = name; this.age = age;

    }

    //поля класса

    static int wishes;

    public string name, status, wealth;

    int age;

    public Personage couple;

    //методы класса

    }

    Герои сказки - объекты этого класса обладают свойствами, задающими имя, возраст, статус, имущество и супруга. Имя и возраст задаются в конструкторе класса, а остальные свойства задаются в следующем методе:

    public void marry (Personage couple)

    {

    this.couple = couple;

    couple.couple = this;

    this.status ="крестьянин";

    this.wealth ="рыбацкая сеть";

    this.couple.status = "крестьянка";

    this.couple.wealth = "корыто";

    SaveState();

    }

    Предусловие метода предполагает, что метод вызывается один раз главным героем (рыбаком). В методе устанавливаются взаимные ссылки между героями сказки, их начальное состояние. Завершается метод сохранением состояния объектов, выполняемого при вызове метода SaveState:

    void SaveState()

    {

    BinaryFormatter bf = new BinaryFormatter();

    FileStream fs = new FileStream

    ("State.bin",FileMode.Create, FileAccess.Write);

    bf.Serialize(fs,this);

    fs.Close();

    }

    Здесь и выполняется сериализация графа объектов. Как видите, все просто. Вначале создается форматер – объект bf класса BinaryFormatter. Затем определяется файл, в котором будет сохраняться состояние объектов, - объект fs класса FileStream. Заметьте, в конструкторе файла, кроме имени файла, указываются его характеристики: статус, режим доступа. На деталях введения файлов я останавливаться не буду. Теперь, когда основные объекты определены, остается вызвать метод Serialize объекта bf, которому в качестве аргументов передается объект fs и текущий объект, представляющий корневой объект графа объектов, которые подлежат сериализации. Глубокая сериализация, реализуемая в данном случае, не потребовала от нас никаких усилий.

    Нам понадобится еще метод, описывающий жизнь героев сказки:

    public Personage AskGoldFish()

    {

    Personage fisher = this;

    if (fisher.name == "рыбак")

    {

    wishes++;

    switch (wishes)

    {

    case 1: ChangeStateOne();break;

    case 2: ChangeStateTwo();break;

    case 3: ChangeStateThree();break;

    default: BackState(ref fisher);break;

    }

    }

    return(fisher);

    }//AskGoldFish

    Метод реализует анализ желаний героини сказки. Первые три желания исполняются, и состояние героев меняется:

    void ChangeStateOne()

    {

    this.status = "муж дворянки";

    this.couple.status = "дворянка";

    this.couple.wealth = "имение";

    }

    void ChangeStateTwo()

    {

    this.status = "муж боярыни";

    this.couple.status = "боярыня";

    this.couple.wealth = "много поместий";

    }

    void ChangeStateThree()

    {

    this.status = "муж государыни";

    this.couple.status = "государыня";

    this.couple.wealth = "страна";

    }

    Начиная с четвертого желания, все возвращается в начальное состояние – выполняется десериализация графа объектов:

    void BackState(ref Personage fisher)

    {

    BinaryFormatter bf = new BinaryFormatter();

    FileStream fs = new FileStream

    ("State.bin",FileMode.Open, FileAccess.Read);

    fisher = (Personage)bf.Deserialize(fs);

    fs.Close();

    }

    Обратите внимание, что у метода есть аргумент, передаваемый по ссылке. Этот аргумент получает значение - ссылается на объект, создаваемый методом Deserialize. Без аргумента метода не обойтись, поскольку возвращаемый методом объект нельзя присвоить текущему объекту 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 показаны результаты исполнения сказки.




    Рис. 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.




    Рис. 19.7. XML-документ, сохраняющий состояние объектов

    Интерфейс ISerializable


    При необходимости можно самому управлять процессом сериализации. В этом случае наш класс должен быть наследником интерфейса ISerializable. Класс, наследующий этот интерфейс, должен реализовать единственный метод этого интерфейса GetObjectData и добавить защищенный конструктор. Схема сериализации и десериализации остается и в этом случае той же самой. Можно использовать как бинарный форматер, так и soap-форматер. Но теперь метод Serialize использует не стандартную реализацию, а вызывает метод GetObjectData, управляющий записью данных. Метод Deserialize, в свою очередь, вызывает защищенный конструктор, создающий объект и заполняющий его поля сохраненными значениями.

    Конечно, возможность управлять сохранением и восстановлением данных дает большую гибкость и позволяет, в конечном счете, уменьшить размер файла, хранящего данные, что может быть крайне важно, особенно если речь идет об обмене данными с удаленным приложением. Если речь идет о поверхностной сериализации, то атрибут 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)

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

    Перейдем теперь к рассмотрению специального конструктора класса. Он может быть объявлен с атрибутом доступа 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

    {...}

    Добавим в наш класс специальный метод, вызываемый при сериализации - метод сохранения данных:

    //Специальный метод сериализации

    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 приходится повозиться. Вначале создается новый объект обычным конструктором, аргументы которого читаются из сохраняемой памяти. Затем восстанавливаются значения других полей этого объекта, а затем уже происходит взаимное связывание двух объектов.

    Кроме введения конструктора класса и метода GetObjectData, никаких других изменений в проекте не понадобилось - ни в методах класса, ни на стороне клиента. Внешне проект работал совершенно идентично ситуации, когда не вводилось наследование интерфейса сериализации. Но с внутренних позиций изменения произошли: методы форматеров Serialize и Deserialize в процессе своей работы теперь вызывали созданный нами метод и конструктор класса. Небольшие изменения произошли и в файлах, хранящих данные.

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

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

    Таблица 19.1. Размеры файлов при различных случаях сериализации

    Формат

    Сериализация

    Размер файла

    Бинарный поток

    Стандартная

    355 байтов

    Бинарный поток

    Управляемая

    355 байтов

    XML-документ

    Стандартная

    1, 14 Кб.

    XML-документ

    Управляемая

    974 байта

    Преимуществами XML-документа являются его читабельность и хорошо развитые средства разбора, но зато бинарное представление выигрывает в объеме и скорости передачи тех же данных.


    написать администратору сайта