Страница 312 из 371
В заключение построим тест, проверяющий работу с объектами классов а и в;
public void TestClientSupplier ()
{
ClassB objB = new ClassB("AA",22, "BB",33);
objB.MethodB1 ();
objВ.MethodB2();
objВ.MethodB3 ();
}
Результаты работы этого теста показаны на рис. 18.1.
Рис. 18.1. Клиенты и поставщики
Сам себе клиент
Зададимся вопросом, может ли класс быть сам себе клиентом, другими словами, может ли поле класса быть объектом описываемого класса? Другой, не менее интересный вопрос — могут ли два класса быть одновременно клиентами и поставщиками друг для друга? Ответы на оба вопросы положительны, и подобные ситуации типичны и не являются какой-либо экзотикой.
Первая ситуация характерна для динамических структур данных. Элемент односвязного списка имеет поле, представляющее элемент односвязного списка; элемент двусвязного списка имеет два таких поля; узел двоичного дерева имеет два поля, представляющих узлы двоичного дерева. Эта ситуация характерна не только для рекурсивно определяемых структур данных. Вот еще один типичный пример.
В классе Person могут быть заданы два поля — Father и Mother, задающие родителей персоны, и массив children. Понятно, что все эти объекты могут быть того же класса Person.
Не менее часто встречается ситуация, когда классы имеют поля, взаимно ссылающиеся друг на друга. Типичным примером могут служить классы Man и woman, первый из которых имеет поле wife класса Woman, а второй — поле husband класса Man.
Заметьте, классы устроены довольно просто — их тексты понятны, отношения между классами очевидны. А вот динамический мир объектов этих классов может быть довольно сложным, отношения между объектами могут быть запутанными; для объектов характерны не только любовные треугольники, но и куда более сложные фигуры.
Наследование
Мощь ООП основана на наследовании. Когда построен полезный класс, то он может многократно использоваться. Повторное использование — это одна из главных целей ООП. Но и для хороших классов неизбежно наступает момент, когда необходимо расширить возможности класса, придать ему новую функциональность, изменить интерфейс. Всякая попытка изменять сам работающий класс чревата большими неприятностями — могут перестать работать прекрасно работавшие программы, многим клиентам класса вовсе не нужен новый интерфейс и новые возможности. Здесь-то и приходит на выручку наследование. Существующий класс не меняется, но создается его потомок, продолжающий дело отца, только уже на новом уровне.
Класс-потомок наследует все возможности родительского класса — все поля и все методы, открытую и закрытую часть класса. Правда, не ко всем полям и методам класса возможен прямой доступ потомка. Поля и методы родительского класса, снабженные атрибутом private, хотя и наследуются, но по-прежнему остаются закрытыми, и методы, создаваемые потомком, не могут к ним обращаться напрямую, а только через методы, наследованные от родителя. Единственное, что не наследует потомок — это конструкторы родительского класса. Конструкторы потомок должен создавать сам. В этом есть некоторая разумная идея, и я позже поясню ее суть.
Рассмотрим класс, названный Found, играющий роль родительского класса. У него есть обычные поля, конструкторы и методы, один из которых снабжен новым модификатором virtual, ранее не появлявшимся в классах, другой — модификатором override:
public class Found
{
//поля
protected string name;
protected int credit;
public Found()
{
}
public Found(string name, int sum)
{
this.name = name; credit = sum;
}
public virtual void VirtMethodO {
Console.WriteLine ("Отец: " + this.ToString());
}
public override string ToString ()
{
return(String.Format("поля: name = {0}, credit = {1}", name, credit));
}
public void NonVirtMethod()
{
Console.WriteLine ("Мать: " + this.ToString());
}
public void Analysis()
{
Console.WriteLine ("Простой анализ");
}
public void Work()
}
VirtMethod();
NonVirtMethod();
Analysis ();
}
}
Заметьте, класс Found, как и все классы, по умолчанию является наследником класса Object, его потомки наследуют методы этого класса уже не напрямую, а через методы родителя, который мог переопределить методы класса Object, в частности, класс Found переопределил метод Tostring, задав собственную реализацию возвращаемой методом строки, которая связывается с объектами класса. Как часто делается, в этой строке отображаются значения полей объекта данного класса. На переопределение родительского метода Tostring указывает модификатор метода override.
Класс Found закрыл свои поля для клиентов, но открыл для потомков, снабдив их модификатором доступа protected.
Создадим теперь класс Derived — потомка класса Found, в простейшем случае объявление класса может выглядеть так:
public class Derived: Found
}
}
Тело класса Derived пусто, но это вовсе не значит, что объекты этого класса не имеют полей и методов: они "являются" объектами класса Found, наследуя все его поля и методы (кроме конструктора) и поэтому могут делать все, что могут делать объекты родительского класса.
Вот пример работы с объектами родительского и производного класса:
public void TestFoundDerived()
{
Found bs = new Found ("father", 777);
Console.WriteLine("Объект bs вызывает методы базового класса");
bs.VirtMethod();
bs.NonVirtMethod ();
bs.Analysis(); bs.Work();
Derived der = new Derived ();
Console.WriteLine("Объект der вызывает методы класса потомка");
der.VirtMethod();
der.NonVirtMethod();
der.Analysis();
der.Work();
}
Результаты работы этой процедуры показаны на рис. 18.2.
Рис. 18.2. Объект потомка наследует поля и методы родителя
В чем отличие работы объектов bs и der? Поскольку класс-потомок Derived ничего самостоятельно не определял, то он наследовал все поля и методы у своего родителя, за исключением конструкторов. У этого класса имеется собственный конструктор без аргументов, задаваемый по умолчанию. При создании объекта der вызывался его собственный конструктор по умолчанию, инициализирующий поля класса значениями по умолчанию. Об особенностях работы конструкторов потомков скажу чуть позже, сейчас же упомяну лишь, что конструктор по умолчанию потомка вызывает конструктор без аргументов своего родителя, поэтому для успеха работы родитель должен иметь такой конструктор. Заметьте, поскольку родитель не знает, какие у него могут быть потомки, то желательно конструктор без аргументов включать в число конструкторов класса, как это сделано для класса Found.
Добавление полей потомком
Ничего не делающий самостоятельно потомок не эффективен, от него мало проку. Что же может делать потомок? Прежде всего, он может добавить новые свойства — поля класса. Заметьте, потомок не может ни отменить, ни изменить модификаторы или типы полей, наследованных от родителя — он может только добавить собственные поля.