Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Пособие КНЕУ.doc
Скачиваний:
24
Добавлен:
07.03.2016
Размер:
3.9 Mб
Скачать

9.5.4. Клонування об'єктів (інтерфейс iСloneable)

Клонування - це створення копії об'єкту. Копія об'єкту називається клоном. Як вам відомо, при привласненні одного об'єкту посилального типу іншому копіюється посилання, а не сам об'єкт (рис. 9.1, а). Якщо необхідно скопіювати в іншу область пам'яті поля об'єкту, можна скористатися методом MemberwiseСlone, який будь-який об'єкт успадковує від класу object. При цьому об'єкти, на які указують поля об'єкту, що у свою чергу є посиланнями, не копіюються (рис. 9.1, б). Це називається поверхневим клонуванням.

Рис. 9.1. Клонування об'єктів

Для створення повністю незалежних об'єктів необхідне глибоке клонування, коли в пам'яті створюється дублікат всього дерева об'єктів, тобто об'єктів, на які посилаються поля об'єкту, поля полів і так далі (рис. 9.1, в). Алгоритм глибокого клонування дуже складний, оскільки вимагає рекурсивного обходу всіх посилань об'єкту і відстежування циклічних залежностей.

Об'єкт, що має власні алгоритми клонування, повинен оголошуватися як спадкоємець інтерфейсу ICloneable і перевизначати його єдиний метод Clone. У лістингу 9.4 приведений приклад створення поверхневої копії об'єкту класу Monster за допомогою методу MemberwiseClone, а також реалізований інтерфейс ICloneable. У демонстраційних цілях в ім'я клона об'єкту додано слово “Клон”.

Метод MemberwiseClone можна викликати тільки з методів класу. Він не може бути викликаний безпосередньо, оскільки оголошений в класі object як захищений (protected).

Лістинг 9.5. Клонування об'єктів

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace examp58

{

class Monster : ICloneable

{

string name;

int health;

int ammo;

public Monster(int health, int ammo, string name)

{

this.health = health;

this.ammo = ammo;

this.name = name;

}

public Monster ShallowClone() // поверхнева копія

{ return (Monster)this.MemberwiseClone(); }

public object Clone() // призначена копія для користувача

{

return new Monster(this.health, this.ammo, "Клон " + this.name);

}

virtual public void Passport()

{

Console.WriteLine("Monster {0} \t health = {1} ammo = {2}",

name, health, ammo);

}

}

class Program

{

static void Main(string[] args)

{

Monster Вася = new Monster(70, 80, "Вася" );

Monster X = Вася;

X.Passport();

Monster Y = Вася.ShallowClone();

Y.Passport();

Monster Z = (Monster)Вася.Clone();

Z.Passport();

}

}

}

Об'єкт X посилається на ту ж область пам'яті, що і об'єкт Вася. Отже, якщо ми внесемо зміни до одного з цих об'єктів, це відіб'ється на іншому. Об'єкти Y і Z, створені шляхом клонування, володіють власними копіями значень полів і незалежні від початкового об'єкту.

9.5.5. Перебір об'єктів (інтерфейс iEnumerable) і ітератори

Оператор foreach є зручним засобом перебору елементів об'єкту. Масиви і всі стандартні колекції бібліотеки .NET дозволяють виконувати такий перебір завдяки тому, що в них реалізовані інтерфейси IEnumerable і IEnumerator. Для застосування оператора foreach до призначеного для користувача типу даних потрібно реалізувати в нім ці інтерфейси. Давайте подивимося, як це робиться.

Інтерфейс IEnumerable (що перераховує) визначає всього один метод - GetEnuraerator, що повертає об'єкт типу IEnumerator (нумератор), який можна використовувати для проглядання елементів об'єкту.

Інтерфейс IEnumerator задає три елементи:

  • властивість Current, що повертає поточний елемент об'єкту;

  • метод Movenext, що просуває нумератор на наступний елемент об'єкту;

  • метод Reset, що встановлює нумератор в початок перегляду.

Цикл foreach використовує ці методи для перебору елементів, з яких складається об'єкт.

Таким чином, якщо потрібно, щоб для перебору елементів класу міг застосовуватися цикл foreach, необхідно реалізувати чотири методи: GetEnumerator, Current, MoveNext і Reset. Наприклад, якщо внутрішні елементи класу організовані в масив, потрібно буде описати закрите поле класу, що зберігає поточний індекс в масиві, в методі Movenext задати зміну цього індексу на 1 з перевіркою виходу за межу масиву, в методі Current - повернення елементу масиву по поточному індексу і так далі.

Це не цікава робота, а виконувати її доводиться часто, тому і були введені засоби, що полегшують виконання перебору в об'єкті, - ітератори. Ітератором є блок коду, задаючий послідовність перебору елементів об'єкту. На кожному проході циклу foreach виконується один крок ітератора, що закінчується видачею чергового значення. Видача значення виконується за допомогою ключового слова yield.

Розглянемо створення ітератора на прикладі лістингу 9.6. Нехай потрібно створити об'єкт, що містить бойову групу екземплярів типу Monster. Для простоти обмежимо максимальну кількість бійців в групі десятьма.

Лістинг 9.6. Клас з ітератором

using System;

using System.Collections;

namespace examp59

{

class Monster

{

string name;

int health;

int ammo;

public Monster( int health, int ammo, string name )

{

this. health = health;

this.ammo = ammo;

this.name = name;

}

public Monster()

{

name = "ЛЮДА";

ammo = 100;

health = 100;

}

public Monster(string name)

{

this.name = name;

ammo = 200;

health = 200;

}

public void Passport()

{

Console.WriteLine(" {0} {1} {2} ", name, ammo, health);

}

}

class Daemon : Monster

{

public int brain;

public Daemon(): base()

{

brain = 1;

}

}

class Stado : IEnumerable // 1

{

private Monster[] mas;

private int n;

public Stado()

{

mas = new Monster[10]; n = 0;

}

public IEnumerator GetEnumerator()

{

for (int i = 0; i < n; ++i)

yield return mas[i]; // 2

}

public IEnumerable Backwards() // у зворотному порядку

{

for (int i = n - 1; i >= 0; --i ) yield return mas[i];

}

public IEnumerable MonstersOnly() // тільки монстри

{

for ( int i = 0; i < n; ++i )

if ( mas [i].GetType().Name == "Monster" ) yield return mas[i];

}

public void Add( Monster m )

{

if ( n >= 10 ) return;

mas[n] = m;

++n;

}

}

class Program

{

static void Main(string[] args)

{

Stado s = new Stado();

s.Add( new Monster());

s.Add( new Monster("Bacя"));

s.Add( new Monster("Петя"));

foreach ( Monster i in s ) i.Passport();

foreach ( Monster i in s.Backwards()) i.Passport();

foreach ( Monster i in s.MonstersOnly()) i.Passport();

}

}

}

Все, що потрібно зробити для підтримки перебору, - вказати, що клас реалізує інтерфейс IEnumerable (оператор 1), і описати ітератор (оператор 2). Доступ до нього може бути здійснений через методи MoveNext і Current інтерфейсу IEnumerator.

За кодом, приведеним в лістингу 9.6, стоїть велика внутрішня робота компілятора. На кожному кроці циклу foreach для ітератора створюється “оболонка” - службовий об'єкт, який запам'ятовує поточний стан ітератора і виконує все необхідне для доступу до елементів об'єкту, що проглядаються. Іншими словами, код, що становить ітератор, не виконується так, як він виглядає - у вигляді безперервної послідовності, а розбитий на окремі ітерації, між якими стан ітератора зберігається.

У лістингу 9.7 приведений приклад ітератора, що перебирає чотири задані рядки.

Лістинг 9.7. Простий ітератор

usingSystem;

using System.Collections;

namespace examp60

{

class Num : IEnumerable

{

public IEnumerator GetEnumerator()

{

yield return "one";

yield return "two";

yield return "three";

yield return "oops";

}

}

class Program

{

static void Main(string[] args)

{

foreach ( string s in new Num() ) Console.WriteLine( s );

}

}

}

Результат роботи програми:

one

two

three

oops

Наступний приклад демонструє перебір значень в заданому діапазоні (від 1 до 5):

using System;

using System.Collections;

using System.Linq;

using System.Text;

namespace examp61

{

class Program

{

public static IEnumerable Count( int from, int to )

{

from = 1;

while ( from <= to ) yield return from++;

}

static void Main(string[] args)

{

foreach (int i in Count(1, 5)) Console.WriteLine(i);

}

}

}

Перевага використання ітераторів полягає в тому, що для одного і того ж класу можна задати різний порядок перебору елементів. У лістингу 9.8 описано дві додаткові стратегії перебору елементів класу Stado, введеного в лістингу 9.6, - перебір в зворотному порядку і вибірка тільки тих об'єктів, які є екземплярами класу Monster (для цього використаний метод отримання типу об'єкту GetType, успадкований від базового класу object).

Лістинг 9.8. Реалізація декількох стратегій перебору

using System;

using System.Collections;

namespace examp62

{

class Monster

{

string name;

int health;

int ammo;

public Monster(int health, int ammo, string name)

{

this.health = health;

this.ammo = ammo;

this.name = name;

}

public Monster()

{

name = "ЛЮДА";

ammo = 100;

health = 100;

}

public Monster(string name)

{

this.name = name;

ammo = 200;

health = 200;

}

public void Passport()

{

Console.WriteLine(" {0} {1} {2} \n", name, ammo, health);

}

}

class Daemon : Monster

{

public int brain;

public Daemon()

: base()

{

brain = 1;

}

}

class Stado : IEnumerable // 1

{

private Monster[] mas;

private int n;

public Stado()

{

mas = new Monster[10]; n = 0;

}

public IEnumerator GetEnumerator()

{

for (int i = 0; i < n; ++i)

yield return mas[i]; // 2

}

public IEnumerable Backwards() // у зворотному порядку

{

for (int i = n - 1; i >= 0; --i) yield return mas[i];

}

public IEnumerable MonstersOnly() // тільки монстри

{

for (int i = 0; i < n; ++i)

if (mas[i].GetType().Name == "Monster") yield return mas[i];

}

public void Add(Monster m)

{

if (n >= 10) return;

mas[n] = m;

++n;

}

}

class Program

{

static void Main(string[] args)

{

Stado s = new Stado();

s.Add( new Monster());

s.Add( new Monster("Bacя"));

s.Add( new Monster("Петя"));

s.Add(new Daemon());

foreach ( Monster i in s ) i.Passport();

foreach ( Monster i in s.Backwards()) i.Passport();

foreach ( Monster i in s.MonstersOnly()) i.Passport();

}

}

}

Блок ітератора синтаксично є звичайним блоком і може зустрічатися в тілі методу, операції або частині get властивості, якщо відповідне повертане значення має тип IEnumerable або IEnumerator.

У тілі блоку ітератора можуть зустрічатися дві конструкції:

  • yield return формує значення, що видається на черговій ітерації;

  • yield break сигналізує про завершення ітерації.

Ключове слово yield має спеціальне значення для компілятора тільки в цих конструкціях.

Код блоку ітератора виконується не так, як звичайні блоки. Компілятор формує службовий об'єкт-нумератор, при виклику методу MoveNext якого виконується код блоку ітератора, що видає чергове значення за допомогою ключового слова yield. Наступний виклик методу MoveNext об'єкту-нумератора відновлює виконання блоку ітератора з моменту, на якому він був припинений в попередній раз.