Проект к данной лекции Вы можете скачать здесь.
Слово " интерфейс " - многозначное, и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом interface. В таком понимании интерфейс - это частный случай класса. Интерфейс представляет собой полностью абстрактный класс, все методы которого абстрактны. От абстрактного класса интерфейс отличается некоторыми деталями в синтаксисе и поведении. Синтаксическое отличие состоит в том, что методы интерфейса объявляются без указания
Интерфейсы позволяют частично справиться с таким существенным недостатком языка, как отсутствие множественного наследования классов. Хотя реализация множественного наследования сталкивается с рядом проблем, его отсутствие существенно снижает выразительную мощь языка. В языке C# полного множественного наследования классов нет. Чтобы частично сгладить этот пробел, допускается множественное наследование интерфейсов. Обеспечить возможность классу иметь несколько родителей - один полноценный класс, а остальные в виде интерфейсов, - в этом и состоит основное назначение интерфейсов.
Отметим еще одно важное назначение интерфейсов,отличающее их от абстрактных классов. Абстрактный класс представляет собой начальный этап проектирования класса, который в будущем получит конкретную
Давайте опишем некоторый интерфейс, реализация методов которого позволит классу проводить некоторые преобразования над объектами класса и возвращать строки, представляющие результат этих преобразований. Пример немного искусственный, поскольку наша главная задача сейчас состоит в том, чтобы рассмотреть, как класс реализует методы наследуемого интерфейса.
interface IStrings
{
/// <summary>
/// Преобразование
/// </summary>
/// <returns>результат преобразования</returns>
string Convert();
/// <summary>
/// Шифрование
/// </summary>
/// <param name="code">код шифра</param>
/// <returns>результат шифрования</returns>
string Cipher(string[] code);
}
У этого интерфейса два метода, которые и должны будут реализовать все классы, наследники интерфейса. Как следует из спецификации, заданной в тегах summary, метод Convert должен, следуя алгоритму, выбранному потомком, преобразовать объект, возвращая строку, а метод Chiper, возвращающий строку, рассматривается как шифрование, в алгоритме которого используется массив строк code, переданный методу.
Класс, наследующий интерфейс и реализующий его методы, может объявить соответствующие методы класса открытыми. Заметьте, у методов интерфейса не заданы модификаторы доступа. Построим пример класса, наследующего интерфейс IStrings:
/// <summary>
/// Наследует интерфейс IStrings,
/// реализуя его методы как общедоступные (public)
/// </summary>
class SimpleText : IStrings
{
//поля класса
string text;
static string[] codeTable =
{
"абвгдеёжзийклмнопрстуфхцчшщыьъэюя ,.!?;:",
"ъьыщшчцхфуэюя ,.!?;:тсрпонмлкйабвгдеёжзи"
};
//Конструкторы
public SimpleText()
{
text = "Простой текст!";
}
public SimpleText(string txt)
{
text = txt;
}
public string Text
{
get { return text; }
}
}
Построенный класс является обычным классом, содержащим текстовое поле text, конструкторы, метод - свойство, обеспечивающее доступ к закрытому полю. Но поскольку класс объявил себя наследником интерфейса IStrings, он обязан реализовать методы интерфейса, предложив некоторую их реализацию. Вот пример подобной реализации:
//Реализация интерфейсов
/// <summary>
/// Удаление пробелов в поле text,
/// преобразование к нижнему регистру
/// </summary>
/// <returns>преобразованная строка</returns>
public string Convert()
{
string res = "";
foreach (char sym in text)
if (sym != ' ') res += sym.ToString();
res = res.ToLower();
return res;
}
/// <summary>
/// шифрование поля text
/// с использованием таблицы кодировки символов
/// </summary>
/// <param name="code">таблица кодировки</param>
/// <returns>зашифрованный текст</returns>
public string Cipher(string[] code)
{
string res = "";
foreach (char sym in text)
{
int k = code[0].IndexOf(sym);
if (k >= 0) res += code[1][k];
else res += sym.ToString();
}
return res;
}
Класс реализует методы интерфейса, делая их открытыми для клиентов класса и наследников. Класс может построить
/// <summary>
/// Проверка поля text,является ли он палиндромом
/// после преобразования Convert
/// </summary>
/// <returns> true, если палиндром</returns>
public bool IsPalindrom()
{
string txt = Convert();
for(int i=0, j = txt.Length-1; i<j; i++, j--)
if(txt[i] != txt[j]) return false;
return true;
}
/// <summary>
/// Шифрование, заданное собственной таблицей кодировки
/// </summary>
/// <returns>шифрованный текст</returns>
public string Coding()
{
return Cipher(codeTable);
}
Рассмотрим класс Testing, методы которого позволят выполнить тестирование объектов создаваемых нами классов:
/// <summary>
/// тестирующий класс
/// </summary>
class Testing
{
//поля класса
const string PAL =
"А роза упала на лапу Азора";
static string[] CODE =
{
"абвгдежзиклмнопрстуфхцчшыъьэюя",
"abvgdejziklmnoprstyfhc461w'qux"
};
/// <summary>
/// Тестирование класса SimpleText
/// </summary>
public void TestText()
{
Console.WriteLine("Работа с объектом класса SimpleText! ");
SimpleText simpleText = new SimpleText(PAL);
Console.WriteLine("Исходный текст : " + PAL);
string text;
text = simpleText.Convert();
Console.WriteLine("Преобразованный текст : " + text);
if(simpleText.IsPalindrom())
Console.WriteLine("Это палиндром!");
text = simpleText.Coding();
Console.WriteLine("Шифрованный текст : " + text);
Console.WriteLine("Работа с объектом интерфейса IStrings! ");
IStrings istrings;
text = "Это простой текст!";
Console.WriteLine("Исходный текст : " + text);
simpleText = new SimpleText(text);
istrings = (IStrings)simpleText;
text = istrings.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = istrings.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
}
}
Заметьте: в классе Testing объявлен как объект класса SimpleText, так и объект istrings интерфейса IStrings. В методе TestText объект simpleText создается обычным способом при вызове конструктора класса. Затем этот объект вызывает как Convert, наследованный от интерфейса, так и IsPalindrom, Coding, использующие методы интерфейса. Все это совершено понятно и ожидаемо.
Более интересен тот факт, что можно работать с реализованными в классе методами интерфейса через объект istrings. Создать объект интерфейса путем вызова конструктора невозможно, но по отношению к классу SimpleText это объект родительского класса, и потому объекту интерфейса можно присвоить объект класса после соответствующего приведения типа. Такому объекту доступны методы, заданные интерфейсом, и только они. Результаты работы теста показаны на рис. 5.1.
(рис 5.1) Реализация методов интерфейса как открытых методов класса
Другая стратегия реализации состоит в том, чтобы все или некоторые методы интерфейса сделать закрытыми. Для реализации этой стратегии класс, наследующий интерфейс, объявляет методы без private, и уточняет имя метода именем интерфейса. Давайте создадим класс с именем PrivateInterfaces, являющийся аналогом только что рассмотренного класса SimpleText. В этом классе методы наследуемого интерфейса будут реализованы как закрытые для клиентов методы. Полного описания этого класса приводить не буду, ограничусь лишь некоторыми фрагментами. Вот как выглядят заголовки методов, реализующих методы интерфейса:
//Реализация интерфейсов как закрытых методов string IStrings.Convert() string IStrings.Cipher(string[] code)
Модификатор доступа стал по умолчанию private, а имя метода уточнено именем интерфейса. Интересно то, как теперь эти методы вызываются в методах класса. Их нельзя, как ранее, вызвать по имени или даже по полному имени. Нужно явно задать цель вызова. В качестве цели следует взять текущий объект this и привести его к типу интерфейса, так что получается вот такая сложная конструкция целевого объекта:
((IStrings)this)
Соответствующие строки кода в методах класса IsPalindrom и Coding выглядят теперь так:
string txt = ((IStrings)this).Convert(); return ((IStrings)this).Cipher(codeTable);
Класс PrivateInterface реализовал методы интерфейса IStrings, но сделал их закрытыми и недоступными для клиентов и наследников класса. Как же им получить доступ к закрытым методам? Есть два способа решения этой проблемы.
IStrings, полученный приведением (кастингом) объекта класса PrivateInterface. Этому объекту доступны Фактически IsPalindrom и Coding. Давайте выполним обертку
//Обертка методов интерфейса с переименованием
public string ClassConvert()
{
return ((IStrings)this).Convert();
}
public string ClassCipher(string[] code)
{
return ((IStrings)this).Cipher(code);
}
Какая цель достигается в этом случае? Такой прием позволяет переименовывать методы интерфейса. Метод интерфейса со своим именем закрывается, а потом открывается под тем именем, которое класс выбрал для него. Как видите, методы переименованы и получили другие имена, под которыми они и будут известны и доступны клиентам класса.
У клиентов класса есть возможность добраться до new нельзя. Тем не менее можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса - даже если они закрыты в классе, для интерфейсных объектов они являются открытыми.
Построим в классе Testing тест, аналогичный тесту TestText, для работы с объектом класса PrivateInterface.
/// <summary>
/// тестирование класса PrivateInterface
/// </summary>
public void TestTextPrivateInterface()
{
Console.WriteLine("Работа с объектом privateInterface! ");
PrivateInterface privateInterface = new PrivateInterface(PAL);
Console.WriteLine("Исходный текст : " + PAL);
string text;
text = privateInterface.ClassConvert();
Console.WriteLine("Преобразованный текст : " + text);
if (privateInterface.IsPalindrom())
Console.WriteLine("Это палиндром!");
text = privateInterface.Coding();
Console.WriteLine("Шифрованный текст : " + text);
Console.WriteLine("Работа с объектом интерфейса IStrings! ");
IStrings istrings;
text = "Это простой текст!";
Console.WriteLine("Исходный текст : " + text);
privateInterface = new PrivateInterface(text);
istrings = (IStrings)privateInterface;
text = istrings.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = istrings.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
}
В этом тесте объект класса privateInterface вызывает метод интерфейса под новым именем ClassConvert, а интерфейсный объект istrings вызывает методы интерфейса, несмотря на то, что они закрыты, под собственными именами. Результаты работы теста показаны на. рис. 5.2.
(рис 5.2) Реализация методов интерфейса как закрытых методов класса
Множественным наследованием называется ситуация, когда класс объявляет N классов ( N > 1 ) своими непосредственными родителями. В языке C# есть ограничения на множественное наследование. Ситуация здесь такая. Во-первых, у каждого класса родителем, хотя не всегда непосредственным, является класс object. Во-вторых, каждый класс может явно объявить один класс в качестве непосредственного родителя, а также объявить непосредственными родителями один или несколько интерфейсов. Таким образом, в C# допускается множественное наследование интерфейсов и одиночное (не считая наследования от класса object ) наследование классов.
Во многом ограничение множественного наследования классов связано с тем, что оно создает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы - коллизию имен и наследование от общего предка.
Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Если сигнатуры разные, то это не приводит к конфликтам. Класс реализует методы обоих интерфейсов, и у него просто появляются
Но что следует делать классу наследнику в тех случаях, когда
Стратегия склеивания применяется тогда, когда класс - наследник интерфейсов - полагает, что методы разных интерфейсов, имеющие одинаковое имя и сигнатуру, задают один и тот же метод, единая реализация которого и должна быть обеспечена наследником. В этом случае наследник строит единственную общедоступную ( public ) реализацию, соответствующую методам всех интерфейсов c единой сигнатурой.
Стратегия переименования исходит из того, что, несмотря на единую сигнатуру, методы разных интерфейсов должны быть реализованы по-разному. В этом случае необходимо переименовать конфликтующие методы. Конечно, переименование можно сделать в самих интерфейсах, но это неправильный путь: наследники не должны требовать изменений своих родителей - они сами должны меняться. Переименование методов интерфейсов иногда невозможно чисто технически, если интерфейсы являются встроенными или поставляются сторонними фирмами. К счастью, мы уже знаем, как производить переименование метода интерфейса в самом классе наследника. Для этого достаточно реализовать в классе методы разных интерфейсов как закрытые, а затем открыть их в классе с переименованием.
Итак, коллизия имен при множественном наследовании интерфейсов хотя и возможна, но разрешима. Разработчик класса может выбрать одну из двух возможных стратегий, наиболее подходящую для данного конкретного случая.
Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс C является наследником классов A и B, а те, в свой черед, являются наследниками класса P, то класс наследует свойства и методы своего предка P дважды: один раз получая их от класса A, другой - от B. Это явление называется еще дублирующим наследованием. Для классов ситуация осложняется тем, что классы A и B могли по-разному переопределить методы родителя и для потомков предстоит сложный выбор реализации. Ситуация дублирующего наследования показана на рис. 5.3.
(рис 5.3) Дублирующее наследованиеДля интерфейсов сама ситуация дублирующего наследования маловероятна, но возможна, поскольку интерфейс, как и любой класс, может быть наследником другого интерфейса. Поскольку у интерфейсов наследуются только сигнатуры, а не реализации, как в случае классов, проблема дублирующего наследования сводится к проблеме коллизии имен. По-видимому, естественным решением этой проблемы в данной ситуации является склеивание, когда методам, пришедшим разными путями от одного родителя, будет соответствовать единая реализация.
Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса - наследника этих интерфейсов, применяющего разные стратегии для конфликтующих методов. У нас уже определен интерфейс IStrings. Предположим, что существует интерфейс ITransform, подобный Istrings:
interface ITransform
{
/// <summary>
/// Преобразование
/// </summary>
/// <returns>результат преобразования</returns>
string Convert();
/// <summary>
/// Шифрование
/// </summary>
/// <param name="code">код </param>
/// <returns>результат шифрования</returns>
string Cipher(string[] code);
}
У этих интерфейсов имена и
/// <summary>
/// Наследник двух интерфейсов,
/// у методов которых Convert и Cipher
/// сигнатуры совпадают.
/// Методы Cipher склеиваются,
/// Convert - переименовываются
/// </summary>
class TwoInterfaces:IStrings,ITransform
{
//Опущена часть класса, общая с классом SimpleText
//Реализация интерфейсов
string IStrings.Convert()
{
string res = "";
foreach (char sym in text)
if (sym != ' ') res += sym.ToString();
res = res.ToLower();
return res;
}
string ITransform.Convert()
{
string res = "";
for (int i = text.Length - 1; i >= 0; i--)
res += text[i];
return res;
}
//Переименование закрытых методов
public string ConvertOne()
{
return ((IStrings)this).Convert();
}
public string ConvertTwo()
{
return ((ITransform)this).Convert();
}
//Склеивание метода Cipher двух интерфейсов
public string Cipher(string[] code)
{
string s = text;
string res = "";
foreach (char sym in s)
{
int k = code[0].IndexOf(sym);
if (k >= 0) res += code[1][k];
else res += sym.ToString();
}
return res;
}
}
Для методов Cipher двух интерфейсов выбрана стратегия склеивания. Для методов Convert выбрана стратегия переименования. Методы интерфейсов реализованы как
Приведу пример работы с объектами класса и интерфейсными объектами:
public void TestTextTwoInterfaces()
{
Console.WriteLine("Работа с объектом класса TwoInterfaces! ");
TwoInterfaces twoInterfaces = new TwoInterfaces(PAL);
Console.WriteLine("Исходный текст : " + PAL);
string text;
text = twoInterfaces.ConvertOne();
Console.WriteLine("Первое преобразование : " + text);
if (twoInterfaces.IsPalindrom())
Console.WriteLine("Это палиндром!");
text = twoInterfaces.ConvertTwo();
Console.WriteLine("Второе преобразование : " + text);
text = twoInterfaces.Coding();
Console.WriteLine("Шифрованный текст : " + text);
text = "Это простой текст!";
Console.WriteLine("Исходный текст : " + text);
twoInterfaces = new TwoInterfaces(text);
IStrings istrings;
ITransform itransform;
istrings = (IStrings)twoInterfaces;
itransform = (ITransform)twoInterfaces;
Console.WriteLine("Работа с объектом интерфейса IStrings!");
text = istrings.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = istrings.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
Console.WriteLine("Работа с объектом интерфейса ITransform!");
text = itransform.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = itransform.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
}
Результаты работы показаны на рис. 5.4.
(рис 5.4) Решение проблемы коллизии имен
Можно ли в интерфейсе объявлять не только методы, но и поля, обязательные для реализации потомками интерфейса? Ответ не однозначный: и да, и нет. Нет - потому что в явном виде в интерфейсе нельзя объявить поле. Да - потому что в интерфейсе можно объявить метод-свойство с процедурами get и set, обеспечивающими доступ к полю.
Вот пример такого интерфейса:
/// <summary>
/// Доступ к полям Name и Age
/// </summary>
interface IFields
{
string Name { get; set; }
int Age {get;}
}
Создадим класс, наследующий этот интерфейс:
/// <summary>
/// Класс, наследующий интерфейс IFields
/// Имеет поля Name и Age,
/// доступ к полю Name открыт клиентам класса,
/// доступ к полю Age закрыт и открыт с переименованием!
/// </summary>
class TwoFields:IFields
{
string name;
int age;
public TwoFields()
{
name = "Nemo"; age = 37;
}
public TwoFields(string name, int age)
{
this.name = name; this.age = age;
}
public string Name
{
get { return name; }
set { name = value; }
}
int IFields.Age
{
get { return age; }
}
public int WhatAge()
{
return age;
}
}
Обратите внимание: методы доступа к полям, наследуемые от интерфейса, можно реализовать как открытые и как закрытые, открывая их затем под другим именем. У класса всегда должна быть возможность переименовывать имена методов и полей, наследуемых от интерфейса.
Добавим в класс Testing новый метод, позволяющий протестировать работу с наследуемыми полями.
public void TestFields()
{
Console.WriteLine("Работа с объектом класса TwoFields!");
TwoFields twofields = new TwoFields();
Console.WriteLine("Имя: {0}, Возраст: {1}",
twofields.Name, twofields.WhatAge());
twofields.Name = "Captain Nemo";
Console.WriteLine("Работа с интерфейсным объектом IFields!");
IFields ifields = (IFields)twofields;
Console.WriteLine("Имя: {0}, Возраст: {1}",
ifields.Name,ifields.Age);
}
Результаты работы теста показаны на рис. 5.5.
(рис 5.5) Поля, наследуемые от интерфейса IFieldsРассмотрим несколько встроенных интерфейсов, являющихся частью
Часто, когда создается класс, желательно задать отношение порядка на его объектах. Такой класс следует объявить наследником интерфейса IComparable. Этот интерфейс имеет всего один метод CompareTo (object obj), возвращающий целочисленное значение, положительное, отрицательное или равное нулю, в зависимости от выполнения отношения "больше", "меньше" или "равно".
Как правило, в классе вначале определяют метод CompareTo, а после этого вводят перегруженные операции, чтобы выполнять сравнение объектов привычным образом с использованием знаков операций отношения.
Давайте введем отношение порядка на классе Person, рассмотренном в лекции 1, сделав этот класс наследником интерфейса 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, позволяющая проверить корректность выполнения приведения. Если приведение невозможно, то невозможно выполнить и сравнение объектов. В этом случае выбрасывается исключение, которое может обработать разумным образом только клиент класса, пытавшийся выполнить сравнение. В самом классе Person можно только пояснить ситуацию, передав информацию объекту исключения.
Заметьте также, что при проверке на значение null используется отношение Equals, а не обычное равенство, которое будет переопределено.
Давно пора привести отложенное пояснение операций is и as, полезных при работе с объектами, тип которых может быть не определен. Операция is используется в логических выражениях. Логическое выражение
obj is T
истинно, если объект obj принадлежит типу T, и ложно в противном случае.
Оператор присваивания
obj = P as T;
присваивает объекту obj объект P, T, если такое приведение возможно, иначе объекту присваивается значение null. Семантику as можно выразить следующим условным выражением:
(P is T) ? (T)P : (T)null
Определив метод CompareTo для класса Person, мы тем самым ввели отношение порядка для объектов этого класса. Конечно, сравнение персон может выполняться по разным критериям: возрасту, росту, зарплате. В данном случае отношение порядка на объектах класса Person задается как отношение порядка на фамилиях персон. Так как строки наследуют интерфейс IComparable, для фамилий персон вызывается метод CompareTo, его результат и возвращается в качестве результата метода CompareTo для персон.
Введем теперь в нашем классе 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));
}
Вот результаты работы этого теста.
(рис 5.6) Сравнение персонКонечно, заданный нами порядок не имеет никакого отношения к поэтическому дару, а лишь говорит об относительном расположении фамилий поэтов в словарях.
При ссылочном присваивании 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) Поверхностное клонированиеЕсли стандартное поверхностное клонирование нас не устраивает, то класс можно объявить наследником интерфейса и реализовать метод Clone - единственный метод этого интерфейса. В нем можно реализовать полное
Давайте расширим класс Person, сделав его наследником интерфейса . Реализация метода 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, класс должен быть наследником интерфейса или иметь в своем составе итераторы - методы, возвращающие результат типа .
У этого интерфейса всего один метод - GetEnumerator(). У метода нет никаких GetEnumerator синтаксически определен следующим образом:
IEnumerator GetEnumerator()
Это означает, что в результате вызова метода должен возвращаться интерфейсный объект, принадлежащий интерфейсу - еще одному интерфейсу, связанному с перечислимостью. Метод GetEnumerator требует создания объекта перечислителя - объекта, реализующего методы интерфейса . У интерфейса несколько методов, и в совокупности именно они и позволяют организовать процесс перечисления.
Как же получить интерфейсный объект типа ? В принципе эту задачу мы уже умеем решать, что неоднократно демонстрировалось в примерах этой лекции. Нужно объявить интерфейсный объект, затем создать объект класса, являющегося наследником интерфейса , и далее интерфейсный объект связать с объектом класса, используя приведение типа. Но такой подход все равно требует создания объекта, реализующего методы интерфейса , что не так просто.
К счастью, во многих ситуациях есть более простой путь. Пусть нам нужно организовать перечислимость объектов некоторого класса. У этого класса есть поля, являющиеся объектами, для которых перечислимость уже обеспечена, например, одним из полей класса является массив. Все классы массивы независимо от типа элементов реализуют перечислимость элементов массива, и для них определен метод 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. Поскольку класс объявлен наследником интерфейса , необходимо реализовать метод 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.
(рис 5.8) Перечислимость и метод GetEnumerator
Нам удалось достаточно просто организовать перечислимость в классе за счет того, что в классе был определен контейнер со встроенным методом GetEnumerator. А что, если такого контейнера нет, или таких контейнеров несколько и хотелось бы организовать перечисление по каждому из контейнеров? Для решения подобных задач в язык C#, начиная с версии 2.0, встроен мощный механизм итераторов, существенно облегчающих задачу перечислимости объектов класса и открывающих новые возможности, которые нельзя реализовать только за счет наследования интерфейса .
Итератором называется метод класса, возвращающий в качестве результата интерфейсный объект типа . В теле итератора должен присутствовать оператор языка , имеющий следующий синтаксис:
yield return <выражение> ;
При выполнении итератора автоматически создается контейнер, в который добавляется объект при каждом выполнении оператора . Добавляемый в контейнер элемент определяется выражением оператора . Порядок выполнения операторов определяет порядок перечислимости элементов контейнера. Оператор может быть задан в форме
yield break;
В такой форме он сигнализирует об окончании заполнения контейнера элементами.
Итераторов в классе может быть несколько. Каждый из них создает свой вид перечислимости объектов класса. Итератору не требуется, чтобы в классе существовал контейнер со встроенным методом перечислимости, хотя итератор может использовать контейнер, изменяя порядок перечислимости, заданный для контейнера по умолчанию.
Давайте добавим в класс Persons два итератора. Первый из этих итераторов будет использовать существующий в классе контейнер, изменяя заданный порядок перечислимости элементов на обратный.
/// <summary>
/// Перечисление элементов массива container
/// в обратном порядке
/// </summary>
/// <returns>интерфейсный объект</returns>
public IEnumerable ReverseIterator()
{
for (int i = container.Length - 1; i >= 0; i--)
yield return container[i];
}
Оператор , работающий в цикле, первым возвратит последний элемент массива, а последним - первый.
Создадим еще один итератор, никак не связанный с элементами массива поля container. Зададим перечисление, содержащее имена известных языков программирования:
public enum ProgrammingLanguages
{
Fortran, Algol, Cobol, Simula, Pascal,
Ada, C, CPlusPlus, Eiffel, Java, CSharp
}
Построим теперь итератор, в котором создадим локальную переменную для данного перечисления и используем ее, чтобы поместить в контейнер элементы перечисления.
/// <summary>
/// Итератор. Создает объект перечисления
/// и строит контейнер,
/// помещая в него элементы перечисления
/// </summary>
/// <returns>интерфейсный объект</returns>
public IEnumerable LanguagesIterator()
{
ProgrammingLanguages lang;
int i = 0;
while (true)
{
lang = (ProgrammingLanguages)i;
yield return lang;
if (lang.ToString() == "CSharp") yield break;
i++;
}
}
В цикле оператор возвращает элементы перечисления до тех пор, пока не встретится элемент со значением "CSharp". Давайте теперь протестируем то, что у нас получилось. Добавим несколько строк кода в уже рассмотренный метод TestEnumeration:
Console.WriteLine("Обратный порядок перечисления");
foreach(Person agent in agents.ReverseIterator())
Console.WriteLine(agent.ToString());
Console.WriteLine("Перечисление языков программирования");
foreach (ProgrammingLanguages lang in
agents.LanguagesIterator())
Console.WriteLine(lang.ToString());
Обратите внимание: когда клиент организует цикл foreach, он указывает не только имя объекта класса, но и имя итератора, организующего перечислимость. Результаты работы теста показаны на рис. 5.9.
(рис 5.9) Перечислимость и итераторы
Итераторы и контейнеры со встроенным методом GetEnumerator позволяют достаточно просто организовать перечислимость объектов класса, позволяя клиентам класса использовать цикл foreach для доступа к объектам на чтение. В основе предоставляемого механизма доступа лежат методы интерфейса . Используя итератор или встроенный метод GetEnumerator, можно не вникать в суть того, как, например, метод GetEnumerator создает объект интерфейса . Тем не менее в ряде случаев полезно при организации перечислимости объектов класса объявить класс наследником интерфейса и реализовать самостоятельно методы этого интерфейса.
Реализация в for each, который скрывает детали применения свойств и методов интерфейса.
Общая схема перечисления объектов в коллекции такова. Коллекция рассматривается как список с курсором. Вводится понятие позиции курсора, указывающей на текущий объект из списка. В начальный момент курсор установлен перед первым объектом списка и текущий объект не определен. Курсор можно передвигать к следующему элементу списка. Можно прочесть значение элемента, на который указывает курсор. Методы интерфейса позволяют реализовать такую схему работы с коллекцией объектов. Если методы интерфейса предполагается сделать доступными для клиентов, то они реализуются как
Метод bool MoveNext () передвигает курсор к следующему элементу списка, так что после его первого вызова курсор будет указывать на первый элемент списка. После его вызова, когда курсор указывает на MoveNext возвращает значение true, когда курсор указывает на элемент списка, и возвращает false, когда курсор вне списка элементов.
Метод void Reset() возвращает курсор в начальную позицию перед первым элементом списка.
Метод-свойство Object Current { get } возвращает текущий элемент списка. Выбрасывается исключение InvalidOperationException,если при вызове свойства Current курсор установлен перед или после списка элементов.
Приведу теперь пример класса, реализующего интерфейс . Давайте построим класс People, который будет наследником класса Persons и интерфейса . Будучи наследником класса Persons, новый класс будет обладать всеми возможностями родительского класса, в том числе перечислимостью объектов. Но как наследник интерфейса он реализует методы интерфейса и предоставит своим клиентам новые возможности.
/// <summary>
/// Наследник класса Persons
/// Реализует методы интерфейса IEnumerator
/// Предоставляет клиентам класса возможность
/// собственной организации перебора объектов
/// </summary>
class People:Persons,IEnumerator
{
int position = -1;
public People() : base() { }
public People(int size): base(size) { }
public People(Person[] people) : base(people) { }
/// <summary>
/// Передвигает курсор к следующему элементу в контейнере
/// Возвращает true, если курсор установлен на элементе списка,
/// false - в противном случае
/// </summary>
/// <returns></returns>
public bool MoveNext()
{
if (position < size) position++;
return (position < size);
}
/// <summary>
/// Восстанавливает текущее положение курсора
/// в контейнере объектов
/// </summary>
public void Reset()
{
position = -1;
}
/// <summary>
/// Возвращает текущий объект, если курсор установлен
/// на элементе списка
/// Выбрасывает исключение InvalidOperationException,
/// когда курсор вне списка
/// </summary>
public object Current
{
get
{
try
{
return container[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
set
{
try
{
container[position] = (Person)value;
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
У класса три собственных конструктора, которые сводятся к вызову конструкторов родителя, поле position, необходимое для организации доступа, и три . Методы устроены достаточно просто и в особых комментариях не нуждаются. Но стоит обратить внимание на реализацию метода-свойства Current. В самом интерфейсе для этого свойства указан только метод get. В реализации я добавил и реализацию метода set. Это сразу же расширяет возможности клиента. Если при работе с методом foreach объекты доступны только для чтения, то теперь свойство Current позволяет изменять элементы коллекции. Тест, построенный для работы с новым классом People, демонстрирует все эти возможности.
public void TestPeople()
{
People people = new People();
foreach (Person person in people)
Console.WriteLine(person.ToString());
Console.WriteLine("Работа клиента с коллекцией!");
try
{
Person person;
person = new Person("James Bond", 44);
people.MoveNext();
people.Current = person;
people.Reset();
while (true)
{
people.MoveNext();
person = (Person)people.Current;
Console.WriteLine(person.ToString());
}
}
catch (InvalidOperationException)
{
people.Reset();
}
}
Результаты работы этого теста показаны на рис. 5.10.
(рис 5.10) Реализация интерфейса IEnumeratorОбратите внимание: клиент сумел изменить объект, полученный в результате перечисления, добавив "Джеймса Бонда" в список агентов. На этой оптимистической ноте закончим тему организации перечислимости в классе.
Student, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Student.Student, наследующим интерфейс IEnumerable (например, для перечисления оценок студента). Постройте Windows-проект для работы с классом Student.Student, наследующим интерфейсы IComparable и IEnumerable . Постройте Windows-проект для работы с классом Student.Student и Students. В классе Students задайте несколько итераторов, позволяющих перечислять студентов по разным критериям - фамилиям, рейтингу. Постройте Windows-проект для работы с классами.Student и Teacher, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.Car, наследующим интерфейс IComparable. Постройте Windows- проект для работы с классом Car.Car, наследующим интерфейс IEnumerable (например, для перечисления характеристик машины). Постройте Windows-проект для работы с классом Car.Car, наследующим интерфейсы IComparable и IEnumerable . Постройте Windows-проект для работы с классом Car.Car и SaleOfCars. В классе SaleOfCars задайте несколько итераторов, позволяющих перечислять машины по разным критериям - маркам, стоимости. Постройте Windows-проект для работы с классами.Car и OwnerOfCar, связанных отношением клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.Employee, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Employee.Employee, наследующим интерфейс IEnumerable (например, для перечисления послужного списка). Постройте Windows-проект для работы с классом Employee.Employee, наследующим интерфейсы IComparable и IEnumerable . Постройте Windows-проект для работы с классом Employee.Employee и Firm. В классе Firm задайте несколько итераторов, позволяющих перечислять служащих по разным критериям - фамилиям, должности. Постройте Windows-проект для работы с классами.Employee и Firm, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.Проект к данной лекции Вы можете скачать здесь.
Слово " интерфейс " - многозначное, и в разных контекстах оно имеет различный смысл. В данной лекции речь идет о понятии интерфейса, стоящем за ключевым словом interface. В таком понимании интерфейс - это частный случай класса. Интерфейс представляет собой полностью абстрактный класс, все методы которого абстрактны. От абстрактного класса интерфейс отличается некоторыми деталями в синтаксисе и поведении. Синтаксическое отличие состоит в том, что методы интерфейса объявляются без указания
Интерфейсы позволяют частично справиться с таким существенным недостатком языка, как отсутствие множественного наследования классов. Хотя реализация множественного наследования сталкивается с рядом проблем, его отсутствие существенно снижает выразительную мощь языка. В языке C# полного множественного наследования классов нет. Чтобы частично сгладить этот пробел, допускается множественное наследование интерфейсов. Обеспечить возможность классу иметь несколько родителей - один полноценный класс, а остальные в виде интерфейсов, - в этом и состоит основное назначение интерфейсов.
Отметим еще одно важное назначение интерфейсов,отличающее их от абстрактных классов. Абстрактный класс представляет собой начальный этап проектирования класса, который в будущем получит конкретную
Давайте опишем некоторый интерфейс, реализация методов которого позволит классу проводить некоторые преобразования над объектами класса и возвращать строки, представляющие результат этих преобразований. Пример немного искусственный, поскольку наша главная задача сейчас состоит в том, чтобы рассмотреть, как класс реализует методы наследуемого интерфейса.
interface IStrings
{
/// <summary>
/// Преобразование
/// </summary>
/// <returns>результат преобразования</returns>
string Convert();
/// <summary>
/// Шифрование
/// </summary>
/// <param name="code">код шифра</param>
/// <returns>результат шифрования</returns>
string Cipher(string[] code);
}
У этого интерфейса два метода, которые и должны будут реализовать все классы, наследники интерфейса. Как следует из спецификации, заданной в тегах summary, метод Convert должен, следуя алгоритму, выбранному потомком, преобразовать объект, возвращая строку, а метод Chiper, возвращающий строку, рассматривается как шифрование, в алгоритме которого используется массив строк code, переданный методу.
Класс, наследующий интерфейс и реализующий его методы, может объявить соответствующие методы класса открытыми. Заметьте, у методов интерфейса не заданы модификаторы доступа. Построим пример класса, наследующего интерфейс IStrings:
/// <summary>
/// Наследует интерфейс IStrings,
/// реализуя его методы как общедоступные (public)
/// </summary>
class SimpleText : IStrings
{
//поля класса
string text;
static string[] codeTable =
{
"абвгдеёжзийклмнопрстуфхцчшщыьъэюя ,.!?;:",
"ъьыщшчцхфуэюя ,.!?;:тсрпонмлкйабвгдеёжзи"
};
//Конструкторы
public SimpleText()
{
text = "Простой текст!";
}
public SimpleText(string txt)
{
text = txt;
}
public string Text
{
get { return text; }
}
}
Построенный класс является обычным классом, содержащим текстовое поле text, конструкторы, метод - свойство, обеспечивающее доступ к закрытому полю. Но поскольку класс объявил себя наследником интерфейса IStrings, он обязан реализовать методы интерфейса, предложив некоторую их реализацию. Вот пример подобной реализации:
//Реализация интерфейсов
/// <summary>
/// Удаление пробелов в поле text,
/// преобразование к нижнему регистру
/// </summary>
/// <returns>преобразованная строка</returns>
public string Convert()
{
string res = "";
foreach (char sym in text)
if (sym != ' ') res += sym.ToString();
res = res.ToLower();
return res;
}
/// <summary>
/// шифрование поля text
/// с использованием таблицы кодировки символов
/// </summary>
/// <param name="code">таблица кодировки</param>
/// <returns>зашифрованный текст</returns>
public string Cipher(string[] code)
{
string res = "";
foreach (char sym in text)
{
int k = code[0].IndexOf(sym);
if (k >= 0) res += code[1][k];
else res += sym.ToString();
}
return res;
}
Класс реализует методы интерфейса, делая их открытыми для клиентов класса и наследников. Класс может построить
/// <summary>
/// Проверка поля text,является ли он палиндромом
/// после преобразования Convert
/// </summary>
/// <returns> true, если палиндром</returns>
public bool IsPalindrom()
{
string txt = Convert();
for(int i=0, j = txt.Length-1; i<j; i++, j--)
if(txt[i] != txt[j]) return false;
return true;
}
/// <summary>
/// Шифрование, заданное собственной таблицей кодировки
/// </summary>
/// <returns>шифрованный текст</returns>
public string Coding()
{
return Cipher(codeTable);
}
Рассмотрим класс Testing, методы которого позволят выполнить тестирование объектов создаваемых нами классов:
/// <summary>
/// тестирующий класс
/// </summary>
class Testing
{
//поля класса
const string PAL =
"А роза упала на лапу Азора";
static string[] CODE =
{
"абвгдежзиклмнопрстуфхцчшыъьэюя",
"abvgdejziklmnoprstyfhc461w'qux"
};
/// <summary>
/// Тестирование класса SimpleText
/// </summary>
public void TestText()
{
Console.WriteLine("Работа с объектом класса SimpleText! ");
SimpleText simpleText = new SimpleText(PAL);
Console.WriteLine("Исходный текст : " + PAL);
string text;
text = simpleText.Convert();
Console.WriteLine("Преобразованный текст : " + text);
if(simpleText.IsPalindrom())
Console.WriteLine("Это палиндром!");
text = simpleText.Coding();
Console.WriteLine("Шифрованный текст : " + text);
Console.WriteLine("Работа с объектом интерфейса IStrings! ");
IStrings istrings;
text = "Это простой текст!";
Console.WriteLine("Исходный текст : " + text);
simpleText = new SimpleText(text);
istrings = (IStrings)simpleText;
text = istrings.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = istrings.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
}
}
Заметьте: в классе Testing объявлен как объект класса SimpleText, так и объект istrings интерфейса IStrings. В методе TestText объект simpleText создается обычным способом при вызове конструктора класса. Затем этот объект вызывает как Convert, наследованный от интерфейса, так и IsPalindrom, Coding, использующие методы интерфейса. Все это совершено понятно и ожидаемо.
Более интересен тот факт, что можно работать с реализованными в классе методами интерфейса через объект istrings. Создать объект интерфейса путем вызова конструктора невозможно, но по отношению к классу SimpleText это объект родительского класса, и потому объекту интерфейса можно присвоить объект класса после соответствующего приведения типа. Такому объекту доступны методы, заданные интерфейсом, и только они. Результаты работы теста показаны на рис. 5.1.
(рис 5.1) Реализация методов интерфейса как открытых методов класса
Другая стратегия реализации состоит в том, чтобы все или некоторые методы интерфейса сделать закрытыми. Для реализации этой стратегии класс, наследующий интерфейс, объявляет методы без private, и уточняет имя метода именем интерфейса. Давайте создадим класс с именем PrivateInterfaces, являющийся аналогом только что рассмотренного класса SimpleText. В этом классе методы наследуемого интерфейса будут реализованы как закрытые для клиентов методы. Полного описания этого класса приводить не буду, ограничусь лишь некоторыми фрагментами. Вот как выглядят заголовки методов, реализующих методы интерфейса:
//Реализация интерфейсов как закрытых методов string IStrings.Convert() string IStrings.Cipher(string[] code)
Модификатор доступа стал по умолчанию private, а имя метода уточнено именем интерфейса. Интересно то, как теперь эти методы вызываются в методах класса. Их нельзя, как ранее, вызвать по имени или даже по полному имени. Нужно явно задать цель вызова. В качестве цели следует взять текущий объект this и привести его к типу интерфейса, так что получается вот такая сложная конструкция целевого объекта:
((IStrings)this)
Соответствующие строки кода в методах класса IsPalindrom и Coding выглядят теперь так:
string txt = ((IStrings)this).Convert(); return ((IStrings)this).Cipher(codeTable);
Класс PrivateInterface реализовал методы интерфейса IStrings, но сделал их закрытыми и недоступными для клиентов и наследников класса. Как же им получить доступ к закрытым методам? Есть два способа решения этой проблемы.
IStrings, полученный приведением (кастингом) объекта класса PrivateInterface. Этому объекту доступны Фактически IsPalindrom и Coding. Давайте выполним обертку
//Обертка методов интерфейса с переименованием
public string ClassConvert()
{
return ((IStrings)this).Convert();
}
public string ClassCipher(string[] code)
{
return ((IStrings)this).Cipher(code);
}
Какая цель достигается в этом случае? Такой прием позволяет переименовывать методы интерфейса. Метод интерфейса со своим именем закрывается, а потом открывается под тем именем, которое класс выбрал для него. Как видите, методы переименованы и получили другие имена, под которыми они и будут известны и доступны клиентам класса.
У клиентов класса есть возможность добраться до new нельзя. Тем не менее можно объявить объект интерфейсного класса и связать его с настоящим объектом путем приведения (кастинга) объекта наследника к классу интерфейса. Это преобразование задается явно. Имея объект, можно вызывать методы интерфейса - даже если они закрыты в классе, для интерфейсных объектов они являются открытыми.
Построим в классе Testing тест, аналогичный тесту TestText, для работы с объектом класса PrivateInterface.
/// <summary>
/// тестирование класса PrivateInterface
/// </summary>
public void TestTextPrivateInterface()
{
Console.WriteLine("Работа с объектом privateInterface! ");
PrivateInterface privateInterface = new PrivateInterface(PAL);
Console.WriteLine("Исходный текст : " + PAL);
string text;
text = privateInterface.ClassConvert();
Console.WriteLine("Преобразованный текст : " + text);
if (privateInterface.IsPalindrom())
Console.WriteLine("Это палиндром!");
text = privateInterface.Coding();
Console.WriteLine("Шифрованный текст : " + text);
Console.WriteLine("Работа с объектом интерфейса IStrings! ");
IStrings istrings;
text = "Это простой текст!";
Console.WriteLine("Исходный текст : " + text);
privateInterface = new PrivateInterface(text);
istrings = (IStrings)privateInterface;
text = istrings.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = istrings.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
}
В этом тесте объект класса privateInterface вызывает метод интерфейса под новым именем ClassConvert, а интерфейсный объект istrings вызывает методы интерфейса, несмотря на то, что они закрыты, под собственными именами. Результаты работы теста показаны на. рис. 5.2.
(рис 5.2) Реализация методов интерфейса как закрытых методов класса
Множественным наследованием называется ситуация, когда класс объявляет N классов ( N > 1 ) своими непосредственными родителями. В языке C# есть ограничения на множественное наследование. Ситуация здесь такая. Во-первых, у каждого класса родителем, хотя не всегда непосредственным, является класс object. Во-вторых, каждый класс может явно объявить один класс в качестве непосредственного родителя, а также объявить непосредственными родителями один или несколько интерфейсов. Таким образом, в C# допускается множественное наследование интерфейсов и одиночное (не считая наследования от класса object ) наследование классов.
Во многом ограничение множественного наследования классов связано с тем, что оно создает ряд проблем. Они остаются и при множественном наследовании интерфейсов, хотя становятся проще. Рассмотрим две основные проблемы - коллизию имен и наследование от общего предка.
Проблема коллизии имен возникает, когда два или более интерфейса имеют методы с одинаковыми именами и сигнатурой. Если сигнатуры разные, то это не приводит к конфликтам. Класс реализует методы обоих интерфейсов, и у него просто появляются
Но что следует делать классу наследнику в тех случаях, когда
Стратегия склеивания применяется тогда, когда класс - наследник интерфейсов - полагает, что методы разных интерфейсов, имеющие одинаковое имя и сигнатуру, задают один и тот же метод, единая реализация которого и должна быть обеспечена наследником. В этом случае наследник строит единственную общедоступную ( public ) реализацию, соответствующую методам всех интерфейсов c единой сигнатурой.
Стратегия переименования исходит из того, что, несмотря на единую сигнатуру, методы разных интерфейсов должны быть реализованы по-разному. В этом случае необходимо переименовать конфликтующие методы. Конечно, переименование можно сделать в самих интерфейсах, но это неправильный путь: наследники не должны требовать изменений своих родителей - они сами должны меняться. Переименование методов интерфейсов иногда невозможно чисто технически, если интерфейсы являются встроенными или поставляются сторонними фирмами. К счастью, мы уже знаем, как производить переименование метода интерфейса в самом классе наследника. Для этого достаточно реализовать в классе методы разных интерфейсов как закрытые, а затем открыть их в классе с переименованием.
Итак, коллизия имен при множественном наследовании интерфейсов хотя и возможна, но разрешима. Разработчик класса может выбрать одну из двух возможных стратегий, наиболее подходящую для данного конкретного случая.
Проблема наследования от общего предка характерна, в первую очередь, для множественного наследования классов. Если класс C является наследником классов A и B, а те, в свой черед, являются наследниками класса P, то класс наследует свойства и методы своего предка P дважды: один раз получая их от класса A, другой - от B. Это явление называется еще дублирующим наследованием. Для классов ситуация осложняется тем, что классы A и B могли по-разному переопределить методы родителя и для потомков предстоит сложный выбор реализации. Ситуация дублирующего наследования показана на рис. 5.3.
(рис 5.3) Дублирующее наследованиеДля интерфейсов сама ситуация дублирующего наследования маловероятна, но возможна, поскольку интерфейс, как и любой класс, может быть наследником другого интерфейса. Поскольку у интерфейсов наследуются только сигнатуры, а не реализации, как в случае классов, проблема дублирующего наследования сводится к проблеме коллизии имен. По-видимому, естественным решением этой проблемы в данной ситуации является склеивание, когда методам, пришедшим разными путями от одного родителя, будет соответствовать единая реализация.
Приведу пример двух интерфейсов, имеющих методы с одинаковой сигнатурой, и класса - наследника этих интерфейсов, применяющего разные стратегии для конфликтующих методов. У нас уже определен интерфейс IStrings. Предположим, что существует интерфейс ITransform, подобный Istrings:
interface ITransform
{
/// <summary>
/// Преобразование
/// </summary>
/// <returns>результат преобразования</returns>
string Convert();
/// <summary>
/// Шифрование
/// </summary>
/// <param name="code">код </param>
/// <returns>результат шифрования</returns>
string Cipher(string[] code);
}
У этих интерфейсов имена и
/// <summary>
/// Наследник двух интерфейсов,
/// у методов которых Convert и Cipher
/// сигнатуры совпадают.
/// Методы Cipher склеиваются,
/// Convert - переименовываются
/// </summary>
class TwoInterfaces:IStrings,ITransform
{
//Опущена часть класса, общая с классом SimpleText
//Реализация интерфейсов
string IStrings.Convert()
{
string res = "";
foreach (char sym in text)
if (sym != ' ') res += sym.ToString();
res = res.ToLower();
return res;
}
string ITransform.Convert()
{
string res = "";
for (int i = text.Length - 1; i >= 0; i--)
res += text[i];
return res;
}
//Переименование закрытых методов
public string ConvertOne()
{
return ((IStrings)this).Convert();
}
public string ConvertTwo()
{
return ((ITransform)this).Convert();
}
//Склеивание метода Cipher двух интерфейсов
public string Cipher(string[] code)
{
string s = text;
string res = "";
foreach (char sym in s)
{
int k = code[0].IndexOf(sym);
if (k >= 0) res += code[1][k];
else res += sym.ToString();
}
return res;
}
}
Для методов Cipher двух интерфейсов выбрана стратегия склеивания. Для методов Convert выбрана стратегия переименования. Методы интерфейсов реализованы как
Приведу пример работы с объектами класса и интерфейсными объектами:
public void TestTextTwoInterfaces()
{
Console.WriteLine("Работа с объектом класса TwoInterfaces! ");
TwoInterfaces twoInterfaces = new TwoInterfaces(PAL);
Console.WriteLine("Исходный текст : " + PAL);
string text;
text = twoInterfaces.ConvertOne();
Console.WriteLine("Первое преобразование : " + text);
if (twoInterfaces.IsPalindrom())
Console.WriteLine("Это палиндром!");
text = twoInterfaces.ConvertTwo();
Console.WriteLine("Второе преобразование : " + text);
text = twoInterfaces.Coding();
Console.WriteLine("Шифрованный текст : " + text);
text = "Это простой текст!";
Console.WriteLine("Исходный текст : " + text);
twoInterfaces = new TwoInterfaces(text);
IStrings istrings;
ITransform itransform;
istrings = (IStrings)twoInterfaces;
itransform = (ITransform)twoInterfaces;
Console.WriteLine("Работа с объектом интерфейса IStrings!");
text = istrings.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = istrings.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
Console.WriteLine("Работа с объектом интерфейса ITransform!");
text = itransform.Convert();
Console.WriteLine("Преобразованный текст : " + text);
text = itransform.Cipher(CODE);
Console.WriteLine("Шифрованный текст : " + text);
}
Результаты работы показаны на рис. 5.4.
(рис 5.4) Решение проблемы коллизии имен
Можно ли в интерфейсе объявлять не только методы, но и поля, обязательные для реализации потомками интерфейса? Ответ не однозначный: и да, и нет. Нет - потому что в явном виде в интерфейсе нельзя объявить поле. Да - потому что в интерфейсе можно объявить метод-свойство с процедурами get и set, обеспечивающими доступ к полю.
Вот пример такого интерфейса:
/// <summary>
/// Доступ к полям Name и Age
/// </summary>
interface IFields
{
string Name { get; set; }
int Age {get;}
}
Создадим класс, наследующий этот интерфейс:
/// <summary>
/// Класс, наследующий интерфейс IFields
/// Имеет поля Name и Age,
/// доступ к полю Name открыт клиентам класса,
/// доступ к полю Age закрыт и открыт с переименованием!
/// </summary>
class TwoFields:IFields
{
string name;
int age;
public TwoFields()
{
name = "Nemo"; age = 37;
}
public TwoFields(string name, int age)
{
this.name = name; this.age = age;
}
public string Name
{
get { return name; }
set { name = value; }
}
int IFields.Age
{
get { return age; }
}
public int WhatAge()
{
return age;
}
}
Обратите внимание: методы доступа к полям, наследуемые от интерфейса, можно реализовать как открытые и как закрытые, открывая их затем под другим именем. У класса всегда должна быть возможность переименовывать имена методов и полей, наследуемых от интерфейса.
Добавим в класс Testing новый метод, позволяющий протестировать работу с наследуемыми полями.
public void TestFields()
{
Console.WriteLine("Работа с объектом класса TwoFields!");
TwoFields twofields = new TwoFields();
Console.WriteLine("Имя: {0}, Возраст: {1}",
twofields.Name, twofields.WhatAge());
twofields.Name = "Captain Nemo";
Console.WriteLine("Работа с интерфейсным объектом IFields!");
IFields ifields = (IFields)twofields;
Console.WriteLine("Имя: {0}, Возраст: {1}",
ifields.Name,ifields.Age);
}
Результаты работы теста показаны на рис. 5.5.
(рис 5.5) Поля, наследуемые от интерфейса IFieldsРассмотрим несколько встроенных интерфейсов, являющихся частью
Часто, когда создается класс, желательно задать отношение порядка на его объектах. Такой класс следует объявить наследником интерфейса IComparable. Этот интерфейс имеет всего один метод CompareTo (object obj), возвращающий целочисленное значение, положительное, отрицательное или равное нулю, в зависимости от выполнения отношения "больше", "меньше" или "равно".
Как правило, в классе вначале определяют метод CompareTo, а после этого вводят перегруженные операции, чтобы выполнять сравнение объектов привычным образом с использованием знаков операций отношения.
Давайте введем отношение порядка на классе Person, рассмотренном в лекции 1, сделав этот класс наследником интерфейса 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, позволяющая проверить корректность выполнения приведения. Если приведение невозможно, то невозможно выполнить и сравнение объектов. В этом случае выбрасывается исключение, которое может обработать разумным образом только клиент класса, пытавшийся выполнить сравнение. В самом классе Person можно только пояснить ситуацию, передав информацию объекту исключения.
Заметьте также, что при проверке на значение null используется отношение Equals, а не обычное равенство, которое будет переопределено.
Давно пора привести отложенное пояснение операций is и as, полезных при работе с объектами, тип которых может быть не определен. Операция is используется в логических выражениях. Логическое выражение
obj is T
истинно, если объект obj принадлежит типу T, и ложно в противном случае.
Оператор присваивания
obj = P as T;
присваивает объекту obj объект P, T, если такое приведение возможно, иначе объекту присваивается значение null. Семантику as можно выразить следующим условным выражением:
(P is T) ? (T)P : (T)null
Определив метод CompareTo для класса Person, мы тем самым ввели отношение порядка для объектов этого класса. Конечно, сравнение персон может выполняться по разным критериям: возрасту, росту, зарплате. В данном случае отношение порядка на объектах класса Person задается как отношение порядка на фамилиях персон. Так как строки наследуют интерфейс IComparable, для фамилий персон вызывается метод CompareTo, его результат и возвращается в качестве результата метода CompareTo для персон.
Введем теперь в нашем классе 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));
}
Вот результаты работы этого теста.
(рис 5.6) Сравнение персонКонечно, заданный нами порядок не имеет никакого отношения к поэтическому дару, а лишь говорит об относительном расположении фамилий поэтов в словарях.
При ссылочном присваивании 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) Поверхностное клонированиеЕсли стандартное поверхностное клонирование нас не устраивает, то класс можно объявить наследником интерфейса и реализовать метод Clone - единственный метод этого интерфейса. В нем можно реализовать полное
Давайте расширим класс Person, сделав его наследником интерфейса . Реализация метода 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, класс должен быть наследником интерфейса или иметь в своем составе итераторы - методы, возвращающие результат типа .
У этого интерфейса всего один метод - GetEnumerator(). У метода нет никаких GetEnumerator синтаксически определен следующим образом:
IEnumerator GetEnumerator()
Это означает, что в результате вызова метода должен возвращаться интерфейсный объект, принадлежащий интерфейсу - еще одному интерфейсу, связанному с перечислимостью. Метод GetEnumerator требует создания объекта перечислителя - объекта, реализующего методы интерфейса . У интерфейса несколько методов, и в совокупности именно они и позволяют организовать процесс перечисления.
Как же получить интерфейсный объект типа ? В принципе эту задачу мы уже умеем решать, что неоднократно демонстрировалось в примерах этой лекции. Нужно объявить интерфейсный объект, затем создать объект класса, являющегося наследником интерфейса , и далее интерфейсный объект связать с объектом класса, используя приведение типа. Но такой подход все равно требует создания объекта, реализующего методы интерфейса , что не так просто.
К счастью, во многих ситуациях есть более простой путь. Пусть нам нужно организовать перечислимость объектов некоторого класса. У этого класса есть поля, являющиеся объектами, для которых перечислимость уже обеспечена, например, одним из полей класса является массив. Все классы массивы независимо от типа элементов реализуют перечислимость элементов массива, и для них определен метод 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. Поскольку класс объявлен наследником интерфейса , необходимо реализовать метод 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.
(рис 5.8) Перечислимость и метод GetEnumerator
Нам удалось достаточно просто организовать перечислимость в классе за счет того, что в классе был определен контейнер со встроенным методом GetEnumerator. А что, если такого контейнера нет, или таких контейнеров несколько и хотелось бы организовать перечисление по каждому из контейнеров? Для решения подобных задач в язык C#, начиная с версии 2.0, встроен мощный механизм итераторов, существенно облегчающих задачу перечислимости объектов класса и открывающих новые возможности, которые нельзя реализовать только за счет наследования интерфейса .
Итератором называется метод класса, возвращающий в качестве результата интерфейсный объект типа . В теле итератора должен присутствовать оператор языка , имеющий следующий синтаксис:
yield return <выражение> ;
При выполнении итератора автоматически создается контейнер, в который добавляется объект при каждом выполнении оператора . Добавляемый в контейнер элемент определяется выражением оператора . Порядок выполнения операторов определяет порядок перечислимости элементов контейнера. Оператор может быть задан в форме
yield break;
В такой форме он сигнализирует об окончании заполнения контейнера элементами.
Итераторов в классе может быть несколько. Каждый из них создает свой вид перечислимости объектов класса. Итератору не требуется, чтобы в классе существовал контейнер со встроенным методом перечислимости, хотя итератор может использовать контейнер, изменяя порядок перечислимости, заданный для контейнера по умолчанию.
Давайте добавим в класс Persons два итератора. Первый из этих итераторов будет использовать существующий в классе контейнер, изменяя заданный порядок перечислимости элементов на обратный.
/// <summary>
/// Перечисление элементов массива container
/// в обратном порядке
/// </summary>
/// <returns>интерфейсный объект</returns>
public IEnumerable ReverseIterator()
{
for (int i = container.Length - 1; i >= 0; i--)
yield return container[i];
}
Оператор , работающий в цикле, первым возвратит последний элемент массива, а последним - первый.
Создадим еще один итератор, никак не связанный с элементами массива поля container. Зададим перечисление, содержащее имена известных языков программирования:
public enum ProgrammingLanguages
{
Fortran, Algol, Cobol, Simula, Pascal,
Ada, C, CPlusPlus, Eiffel, Java, CSharp
}
Построим теперь итератор, в котором создадим локальную переменную для данного перечисления и используем ее, чтобы поместить в контейнер элементы перечисления.
/// <summary>
/// Итератор. Создает объект перечисления
/// и строит контейнер,
/// помещая в него элементы перечисления
/// </summary>
/// <returns>интерфейсный объект</returns>
public IEnumerable LanguagesIterator()
{
ProgrammingLanguages lang;
int i = 0;
while (true)
{
lang = (ProgrammingLanguages)i;
yield return lang;
if (lang.ToString() == "CSharp") yield break;
i++;
}
}
В цикле оператор возвращает элементы перечисления до тех пор, пока не встретится элемент со значением "CSharp". Давайте теперь протестируем то, что у нас получилось. Добавим несколько строк кода в уже рассмотренный метод TestEnumeration:
Console.WriteLine("Обратный порядок перечисления");
foreach(Person agent in agents.ReverseIterator())
Console.WriteLine(agent.ToString());
Console.WriteLine("Перечисление языков программирования");
foreach (ProgrammingLanguages lang in
agents.LanguagesIterator())
Console.WriteLine(lang.ToString());
Обратите внимание: когда клиент организует цикл foreach, он указывает не только имя объекта класса, но и имя итератора, организующего перечислимость. Результаты работы теста показаны на рис. 5.9.
(рис 5.9) Перечислимость и итераторы
Итераторы и контейнеры со встроенным методом GetEnumerator позволяют достаточно просто организовать перечислимость объектов класса, позволяя клиентам класса использовать цикл foreach для доступа к объектам на чтение. В основе предоставляемого механизма доступа лежат методы интерфейса . Используя итератор или встроенный метод GetEnumerator, можно не вникать в суть того, как, например, метод GetEnumerator создает объект интерфейса . Тем не менее в ряде случаев полезно при организации перечислимости объектов класса объявить класс наследником интерфейса и реализовать самостоятельно методы этого интерфейса.
Реализация в for each, который скрывает детали применения свойств и методов интерфейса.
Общая схема перечисления объектов в коллекции такова. Коллекция рассматривается как список с курсором. Вводится понятие позиции курсора, указывающей на текущий объект из списка. В начальный момент курсор установлен перед первым объектом списка и текущий объект не определен. Курсор можно передвигать к следующему элементу списка. Можно прочесть значение элемента, на который указывает курсор. Методы интерфейса позволяют реализовать такую схему работы с коллекцией объектов. Если методы интерфейса предполагается сделать доступными для клиентов, то они реализуются как
Метод bool MoveNext () передвигает курсор к следующему элементу списка, так что после его первого вызова курсор будет указывать на первый элемент списка. После его вызова, когда курсор указывает на MoveNext возвращает значение true, когда курсор указывает на элемент списка, и возвращает false, когда курсор вне списка элементов.
Метод void Reset() возвращает курсор в начальную позицию перед первым элементом списка.
Метод-свойство Object Current { get } возвращает текущий элемент списка. Выбрасывается исключение InvalidOperationException,если при вызове свойства Current курсор установлен перед или после списка элементов.
Приведу теперь пример класса, реализующего интерфейс . Давайте построим класс People, который будет наследником класса Persons и интерфейса . Будучи наследником класса Persons, новый класс будет обладать всеми возможностями родительского класса, в том числе перечислимостью объектов. Но как наследник интерфейса он реализует методы интерфейса и предоставит своим клиентам новые возможности.
/// <summary>
/// Наследник класса Persons
/// Реализует методы интерфейса IEnumerator
/// Предоставляет клиентам класса возможность
/// собственной организации перебора объектов
/// </summary>
class People:Persons,IEnumerator
{
int position = -1;
public People() : base() { }
public People(int size): base(size) { }
public People(Person[] people) : base(people) { }
/// <summary>
/// Передвигает курсор к следующему элементу в контейнере
/// Возвращает true, если курсор установлен на элементе списка,
/// false - в противном случае
/// </summary>
/// <returns></returns>
public bool MoveNext()
{
if (position < size) position++;
return (position < size);
}
/// <summary>
/// Восстанавливает текущее положение курсора
/// в контейнере объектов
/// </summary>
public void Reset()
{
position = -1;
}
/// <summary>
/// Возвращает текущий объект, если курсор установлен
/// на элементе списка
/// Выбрасывает исключение InvalidOperationException,
/// когда курсор вне списка
/// </summary>
public object Current
{
get
{
try
{
return container[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
set
{
try
{
container[position] = (Person)value;
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
У класса три собственных конструктора, которые сводятся к вызову конструкторов родителя, поле position, необходимое для организации доступа, и три . Методы устроены достаточно просто и в особых комментариях не нуждаются. Но стоит обратить внимание на реализацию метода-свойства Current. В самом интерфейсе для этого свойства указан только метод get. В реализации я добавил и реализацию метода set. Это сразу же расширяет возможности клиента. Если при работе с методом foreach объекты доступны только для чтения, то теперь свойство Current позволяет изменять элементы коллекции. Тест, построенный для работы с новым классом People, демонстрирует все эти возможности.
public void TestPeople()
{
People people = new People();
foreach (Person person in people)
Console.WriteLine(person.ToString());
Console.WriteLine("Работа клиента с коллекцией!");
try
{
Person person;
person = new Person("James Bond", 44);
people.MoveNext();
people.Current = person;
people.Reset();
while (true)
{
people.MoveNext();
person = (Person)people.Current;
Console.WriteLine(person.ToString());
}
}
catch (InvalidOperationException)
{
people.Reset();
}
}
Результаты работы этого теста показаны на рис. 5.10.
(рис 5.10) Реализация интерфейса IEnumeratorОбратите внимание: клиент сумел изменить объект, полученный в результате перечисления, добавив "Джеймса Бонда" в список агентов. На этой оптимистической ноте закончим тему организации перечислимости в классе.
Student, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Student.Student, наследующим интерфейс IEnumerable (например, для перечисления оценок студента). Постройте Windows-проект для работы с классом Student.Student, наследующим интерфейсы IComparable и IEnumerable . Постройте Windows-проект для работы с классом Student.Student и Students. В классе Students задайте несколько итераторов, позволяющих перечислять студентов по разным критериям - фамилиям, рейтингу. Постройте Windows-проект для работы с классами.Student и Teacher, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.Car, наследующим интерфейс IComparable. Постройте Windows- проект для работы с классом Car.Car, наследующим интерфейс IEnumerable (например, для перечисления характеристик машины). Постройте Windows-проект для работы с классом Car.Car, наследующим интерфейсы IComparable и IEnumerable . Постройте Windows-проект для работы с классом Car.Car и SaleOfCars. В классе SaleOfCars задайте несколько итераторов, позволяющих перечислять машины по разным критериям - маркам, стоимости. Постройте Windows-проект для работы с классами.Car и OwnerOfCar, связанных отношением клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.Employee, наследующим интерфейс IComparable. Постройте Windows-проект для работы с классом Employee.Employee, наследующим интерфейс IEnumerable (например, для перечисления послужного списка). Постройте Windows-проект для работы с классом Employee.Employee, наследующим интерфейсы IComparable и IEnumerable . Постройте Windows-проект для работы с классом Employee.Employee и Firm. В классе Firm задайте несколько итераторов, позволяющих перечислять служащих по разным критериям - фамилиям, должности. Постройте Windows-проект для работы с классами.Employee и Firm, связанными взаимными отношениями клиент - поставщик. Обеспечьте возможность сериализации данных, используя атрибут сериализации и интерфейс ISerializable.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.