Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2369 / 261 | Оценка: 4.47 / 4.24 | Длительность: 14:45:00
Лекция 6:

Интерфейсы. Множественное наследование

< Лекция 5 || Лекция 6: 12345 || Лекция 7 >

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

При ссылочном присваивании x = y, где x и y - объекты класса T, как уже много раз говорилось, происходит присваивание ссылок. Если до присваивания ссылка y была связана с объектом в динамической памяти, то после присваивания x будет ссылаться на этот же объект. С самим объектом ничего не происходит, никакая копия этого объекта не создается. Иногда требуется создать копию объекта, так, чтобы x и y ссылались на разные объекты.

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

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

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

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

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

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

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

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

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

public object Clone()
    {
    Person clone = (Person)this.MemberwiseClone();
    clone.fam = "clone_" + fam;
    return clone;
    }

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

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);

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

Перечислимость объектов и интерфейсы

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

Интерфейс перечислимости IEnumerable

У этого интерфейса всего один метод - GetEnumerator(). У метода нет никаких входных аргументов, так что, кажется, организовать перечислимость объектов класса достаточно просто - написать реализацию одного метода. Это действительно просто, но требует понимания процесса перечислимости. Первая сложность состоит в том, что метод GetEnumerator синтаксически определен следующим образом:

IEnumerator GetEnumerator()

Это означает, что в результате вызова метода должен возвращаться интерфейсный объект, принадлежащий интерфейсу IEnumerator - еще одному интерфейсу, связанному с перечислимостью. Метод GetEnumerator требует создания объекта перечислителя - объекта, реализующего методы интерфейса IEnumerator. У интерфейса IEnumerator несколько методов, и в совокупности именно они и позволяют организовать процесс перечисления.

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

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

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

/// <summary>
    /// Класс - наследник интерфейса IEnumerable,
    /// допускающий перечислимость объектов.
    /// Перечислимость сводится к перечислимости персон,
    /// заданных полем container
    /// </summary>
    class Persons:IEnumerable
    {
    protected int size;
    protected Person[] container;
    Random rnd = new Random(); 
    /// <summary>
    /// Конструктор по умолчанию.
    /// Создает массив из 10 персон
    /// </summary>
    public Persons()
    {
    size = 10;
    container = new Person[size];
    FillContainer(); 
   }
    /// <summary>
    /// Конструктор. Создает массив заданной размерности
    /// Создает его элементы, используя рандомизацию.
    /// </summary>
    /// <param name="size">размерность массива</param>
    public Persons(int size)
    {
    this.size = size;
    container = new Person[size];
    FillContainer();
    }
    /// <summary>
    /// Конструктор, которому передается массив персон
    /// </summary>
    /// <param name="container"> массив Person</param>
    public Persons(Person[] container)
    {
    this.container = container;
    size = container.Length;
    }
    /// <summary>
    /// Заполнение массива person 
    /// </summary>
    void FillContainer()
    {
    for(int i = 0; i <size; i++)
    {
        int num = rnd.Next(3*size);
        int age = rnd.Next(27, 46);
        container[i] = new Person("агент_" + num, age);
    }
    }    
    }

Созданный класс Persons устроен просто. У него есть поле, названное container и представляющее собой массив с элементами класса Person. Набор конструкторов класса позволяет передать классу массив персон либо поручить самому классу его формирование, используя метод FillContainer. Поскольку класс объявлен наследником интерфейса IEnumerable, необходимо реализовать метод GetEnumerator, что обеспечит перечислимость объектов класса и даст возможность использовать цикл for each при работе с объектами класса. В данном случае реализовать метод интерфейса несложно, и вот как выглядит его реализация:

/// <summary>
    /// Реализация метода интерфейса IEnumerable
    /// Сводится к вызову соответствующего метода 
    /// для поля container - массива,
    /// для которого этот метод реализован в библиотеке FCL 
    /// </summary>
    /// <returns>перечислитель - интерфейсный объект</returns>
    public IEnumerator GetEnumerator()
    {
    return container.GetEnumerator();
    }

На описание метода в теге summary потребовалось больше времени и строчек текста, чем на тело метода, состоящее из одной строчки. Но на описание не стоит жалеть усилий! Задача решена, и можно попробовать протестировать работу с объектами класса. Добавим в класс Testing соответствующий метод:

public void TestEnumeration()
  { 
   int size = 10;    
   Persons agents = new Persons(size);
   foreach (Person agent in agents)
       Console.WriteLine(agent.ToString());
  }

В тесте создается объект класса Persons, и в цикле foreach объекты перебираются, возвращая каждый раз очередной объект класса Person. Метод ToString, определенный в классе Person, позволяет выводить информацию об объектах. Результаты работы теста показаны на рис. 5.8.

Перечислимость и метод GetEnumerator

Рис. 5.8. Перечислимость и метод GetEnumerator
< Лекция 5 || Лекция 6: 12345 || Лекция 7 >
Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дарья Федотова
Дарья Федотова
Михаил Алексеев
Михаил Алексеев
Россия, Уфа, УГАТУ, 2002
Олег Корсак
Олег Корсак
Латвия, Рига