Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Lektsii_po_programmirovaniyu_3_semestr.doc
Скачиваний:
1
Добавлен:
01.05.2025
Размер:
2.11 Mб
Скачать

Переопределение методов родительского класса. Раннее связывание.

Самый простой способ расширить функциональные возможности базовых классов заключается в порождении от них дочерних классов и добавлении в их состав новых методов. Рассмотрим конкретный пример.

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

class Rect

{

Rectangle area; // Положение и размеры прямоугольника

Color bc, fc; // Цвет контура и цвет закраски

public Rect(){ area=new Rectangle(100,100,200,200); } // Конструктор по умолчанию

// Еще один конструктор (с аргументами)

public Rect(Rectangle r)

{ area = new Rectangle(r.Location, r.Size); bc=Color.Dlack; fc=Color.Red; }

public void Move(int dx, int dy){ area.offset(dx, dy); } // Метод перемещения

public void Draw(Graphics g) // Метод рисования

{

SolidBrush br=new SolidBrush(fc); // Создание объекта кисти

g.FillRectangle(lgb, NodeRect); // Закрашивание области прямоугольника

Pen pencil = new Pen(bc, 1); // Создание объекта пера

g.DrawRectangle(pencil, NodeRect); // Рисование контура прямоугольника

br.Dispose(); // Освобождение ресурса кисти

pencil.Dispose(); // Освобождение ресурса пера

}

}

Как видно из листинга, в состав класса вошли переменные для описания положения и размера прямоугольника (area), цвета его контура (bc) и цвет закраски занимаемой области (fc), а также методы (Move и Draw), позволяющие перемещать и рисовать объекты этого класса.

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

class Ellipse : Rect

{

public void DrawEllipse(Graphics g) // Метод рисования

{

SolidBrush br=new SolidBrush(fc); // Создание объекта кисти

g.FillEllipse (br, area); // Закрашивание области эллипса

Pen pencil = new Pen(bc, 1); // Создание объекта пера

g.DrawEllipse(pencil, area); // Рисование контура эллипса

br.Dispose(); // Освобождение ресурса кисти

pencil.Dispose(); // Освобождение ресурса пера

}

}

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

В данном случае, в класс эллипса был добавлен метод DrawEllipse, имя которого отличается от имени унаследованного метода Draw родительского класса Rect. Но при таком подходе для каждого нового класса-потомка пришлось бы задавать уникальное имя каждому новому методу рисования. Например, для класса прямоугольника с текстовым комментарием – DrawRectangleWithText, для класса прямоугольника с комментарием и тенью – DrawRectangleWithTextAndShadow и т.д. Это не совсем удобно, потому, что приводит к необходимости создавать большое количество имен для методов, выполняющие действия одинакового смыслового назначения - визуальным отображением экземпляров класса. Более простым решением было бы сохранить имя унаследованного метода в классе-потомке, но определить в нем только новую реализацию этого метода. Вот как раз для этого и используется такая форма полиморфизма как переопределение.

При переопределении наследуемого метода в классе-наследнике объявляется новый метод с таким же именем и сигнатурой с модификатором new. Если этот модификатор в объявлении класса-наследника опустить, то ничего плохого не произойдёт. Транслятор всего лишь выдаст предупреждение об ожидаемом спецификаторе new в месте объявления переопределяемого метода класса.

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

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

class Ellipse : Rect

{

public Ellipse(Rectangle r) : Rect(r) // Конструктор

{ bc=Color.Dlack; fc=Color.Red; }

new public void Draw(Graphics g) // Метод рисования

{

SolidBrush br=new SolidBrush(fc); // Создание объекта кисти

g.FillEllipse (br, area); // Закрашивание области эллипса

Pen pencil = new Pen(bc, 1); // Создание объекта пера

g.DrawEllipse(pencil, area); // Рисование контура эллипса

br.Dispose(); // Освобождение ресурса кисти

pencil.Dispose(); // Освобождение ресурса пера

}

}

Теперь при вызове метода рисования объекты класса прямоугольника будут рисовать прямоугольники, объекты класса эллипса будет вырисовывать эллипсы.

Переопределение как форма полиморфизма позволяет методу выполнять действия, которые зависят не только от его имени (названия), а еще и от типа объекта, у которого это метод вызывается. А это позволяет унифицированным образом (одинаково) обращаться к методам объектов разного типа.

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

Недостатки переопределения методов.

Рассмотрим следующий пример. Пусть у нас есть три класса: класс окружности (назовем его классом А), класс прямоугольника (класс В) и класс прямоугольника с текстовым комментарием (класс С), связанные между собой отношением наследования (рис.1).

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

А circle = new A();

B rect = new B();

C rect_txt = new C();

circle.Draw();

rect.Draw();

rect_txt.Draw();

В результате на экране монитора можно будет увидеть окружность, прямоугольник и прямоугольник с текстовой надписью, как показано на рис.1.

Рис.1. Переопределение функции Draw в классах, связанных отношением наследования.

Теперь, предположим, что нам нужно создать по экземпляру каждого класса, но хранить их не в виде отдельных переменных, а в списке. Однако, поскольку все элементы списка должны быть одинакового типа, то, необходимо привести все три объекта к одному типу - типу базового класса А:

List<А> figures;

figures.Add(new A);

figures.Add(new B);

figures.Add(new C);

foreach(A figure in figures) figure.Draw();

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

Рис.2. Вызов метода Draw у объектов, размещаемых в списке.

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]