учебники, программирование, основы, введение в,

 

Расширенные возможности полиморфизма в языке C#

Предыдущая лекция была посвящена базовым аспектам полиморфизма. Обсудим более тонкие механизмы, предусмотренные в языке C# для реализации сложных полиморфных объектов.
Проиллюстрируем особенности использования абстрактных свойств и методов следующим фрагментом программы на языке C#:
abstract class Sequence {
public abstract void Add(object x);
// метод
public abstract string Name{
get;
}
// свойство
public abstract object this [int i]
{
get;
set;
}
// индексатор
}

class List : Sequence {
public override void Add(object x)
{
...
}

   public override string Name
{
get {
...
}
}
public override object this [int i]
{
get {
...
}
set{
...
}
}
}
Как видно из приведенного примера, фрагмент программы на языке C# представляет собой описание абстрактных классов Sequence и List, реализующих потоковое чтение и запись данных (get и set).
Заметим, что описание абстрактного класса Sequence реализовано явно посредством зарезервированного слова abstract.
Необходимо обратить внимание на то обстоятельство, что поскольку в производных классах требуется замещение методов, методы Add и Name класса List оснащены описателем override.
Еще одним средством реализации расширенного полиморфизма в языке программирования C# является механизм, известный под названием «запечатанных» (sealed) классов.
Под «запечатанными» классами мы будем понимать нерасширяемые классы, которые могут наследовать свойства других классов. Решение об использовании механизма «запечатывания» при описании приоритетных или, иначе, замещенных (override) методов принимается в индивидуальном порядке.
Рассмотрим соображения, обосновывающие использование механизма «запечатанных» классов в языке программирования C#. Прежде всего, благодаря реализации данного механизма предотвращается изменение семантики класса. Таким образом, существенно повышается безопасность разрабатываемого программного обеспечения. А за счет того, что вызов «запечатанных» методов предполагает использование статического связывания, значительно возрастает эффективность реализуемых приложений.
Проиллюстрируем особенности использования механизма «запечатанных» классов следующим фрагментом программы на языке C#:
sealed class Account : Asset {
long val;
public void Deposit (long x)
{
...
}
public void Withdraw (long x)
{
...
}
...
}
Как видно из приведенного примера, фрагмент программы на языке C# представляет собой описание «запечатанного» класса Account, реализующего абстрактные методы Deposit и Withdraw для занесения средств на счет и их снятия со счета.
В ходе курса уже обсуждалась стратегия динамического связывания переменных со значениями при функциональном и объектно-ориентированном подходах.
Проиллюстрируем особенности реализации динамического связывания в языке программирования C# следующим фрагментом программы:
class A {
public virtual void WhoAreYou()
{
Console.WriteLine(“I am an A”);
}
}

class B : A {
public override void WhoAreYou()
{
Console.WriteLine(“I am a B”);
}
}
Как видно из приведенного примера, данный фрагмент программы содержит описания класса A с виртуальным методом WhoAreYou, который выводит на стандартное устройство вывода сообщение о принадлежности к классу A, а также замещенного класса B с виртуальным методом WhoAreYou, который выводит на стандартное устройство вывода сообщение о принадлежности к классу B. При таком подходе сообщение запускает метод, который динамически определяет принадлежность к классу (в приведенном ниже примере создается и идентифицируется объект класса B):
A a = new B();
a.WhoAreYou();
// “I am a B”
Заметим, что существенной особенностью рассматриваемого примера является то обстоятельство, что B – это замещенный метод.
В этой связи, каждый из методов, который способен обрабатывать A, может также обрабатывать B. Проиллюстрируем этот факт следующим фрагментом программы на языке C#:
void Use (A x) {
x.WhoAreYou();
}
Use(new A());
// “I am an A”
Use(new B());
// “I am a B”
Как видно из приведенного фрагмента программы, применение метода Use, реализующего идентификацию объекта класса A, приводит к результату для объекта “I am an A” класса A и к результату “I am a B” для объекта класса B. Корректность получаемого результата для класса B (несмотря на детерминированное описание класса A как параметра метода Use) объясняется тем обстоятельством, что класс B является замещенным.


Рассмотрим особенности реализации сокрытия данных в языке программирования C#.
Заметим, что в языке C# существует возможность описания объектов как экземпляров в составе подкласса с использованием зарезервированного слова new. При этом происходит сокрытие одноименных наследуемых экземпляров в подклассах.
Проиллюстрируем особенности использования механизма сокрытия данных в языке программирования C# следующим фрагментом программы:
class A {
public int x;
public void F() {
...
}
public virtual void G()
{
...
}
}
class B : A {
public new int x;
public new void F()
{
...
}
public new void G()
{
...
}
}

B b = new B();
b.x = ...;
// имеет доступ к B.x
b.F(); ... b.G();
// вызывает B.F и B.G
((A)b).x = ...;
// имеет доступ к A.x
((A)b).F();
...
((A)b).G();
// вызывает A.F и A.G
Как видно из приведенного фрагмента, базовый класс A и производный класс B характеризуются общедоступными полем x и методами F и G. Особенности доступа к элементам классов приведены в комментариях. Заметим, что при описании элементов производного класса используется зарезервированное слово new.
Рассмотрим особенности реализации сложного динамического связывания в языке программирования C#. Под сложным связыванием будем понимать связывание с сокрытием (hiding) данных. Для иллюстрации особенностей использования механизма сокрытия данных воспользуемся следующим фрагментом программы на языке C#. Приведем текст примера:
class A {
public virtual void WhoAreYou()
{
Console.WriteLine(“I am an A”);
}
}

class B : A {
public override void WhoAreYou()
{
Console.WriteLine(“I am a B”);
}
}

class C : B {
public new virtual void WhoAreYou()
{
Console.WriteLine(“I am a C”);
}
}

class D : C {
public override void WhoAreYou()
{
Console.WriteLine(“I am a D”);
}
}

C c = new D();
c.WhoAreYou();
// “I am a D”

A a = new D();
a.WhoAreYou();
// “I am a B”
Как мы видим, фрагмент программы содержит описания общедоступных классов: базового класса A и производных классов B и C с иерархией C ISA B ISA A. Каждый из классов характеризуется единственным общедоступным методом WhoAreYou для вывода в стандартный поток диагностического сообщения о принадлежности к данному классу. Заметим, что методы WhoAreYou для классов A и C описаны как виртуальные, причем в последнем описании используется зарезервированное слово new. Метод WhoAreYou для класса B описан как замещенный.
Продолжение фрагмента программы содержит описание общедоступного класса D как производного от C.
Таким образом, иерархия классов принимает вид: D ISA C ISA B ISA A. Класс D характеризуется аналогичным предыдущим классам общедоступным методом WhoAreYou для вывода в стандартный поток диагностического сообщения о принадлежности к данному классу. Заметим, что метод WhoAreYou для класса D является замещенным (но не виртуальным), и в его описании зарезервированное слово new не используется.
При инициализации объектов c и a как экземпляров класса D применение «отладочных» методов дает результат “I am a D” для объекта c и результат “I am a B” для объекта a. Полученный результат объясняется расположением описателей методов override в иерархии классов (более верхний описатель имеет более высокий приоритет).
Дальнейшим развитием динамического связывания в языке программирования C# является реализация механизма вызова методов с приоритетами.
При реализации рассматриваемого механизма на взаимосвязанные семейства методов накладывается ряд дополнительных ограничений.
Прежде всего, требуется обеспечить идентичность описаний методов в группе с приоритетами. При этом для каждого из методов должны выполняться следующие условия.
Во-первых, у всех методов семейства для вызова с приоритетами должны быть одинаковыми как количество, так и типы параметров. Типы функций не должны составлять исключение из этого правила. Во-вторых, все методы семейства для вызова с приоритетами должны иметь одинаковые области видимости (например, каждый из методов семейства должен иметь описатель public или, скажем, protected; наличие в семействе методов с разными описателями области видимости не допускается).
Кроме того, необходимо отметить, что свойства и индексаторы для семейства методов с приоритетами также могут иметь приоритет (с использованием описателя override). Поскольку метод с приоритетами является принципиально динамическим объектом языка программирования, статические методы не могут описываться как методы с приоритетами.
Заметим, что только методы, описанные как виртуальные (virtual), могут иметь приоритет в производных классах.
Наконец, при задании методов с приоритетами необходимо использовать описатель override.
Проиллюстрируем особенности использования механизма методов с приоритетами в языке C# следующим фрагментом программы:
class A {
public void F() {
...
}
// может иметь приоритет
public virtual void G() {
...
}
// может иметь приоритет
// в подклассе
}
class B : A {
public void F() {
...
}
// предупреждение: скрывается
// производныйметод F().
// Необходимо использовать
// оператор new.
public void G() {
...
}

    // предупреждение: скрывается
// производныйметод G().
// Необходимо использовать
// оператор new.
public override    void G() {   
// ok: перекрывает приоритетом
// производныйметод G
...
base.G();
// вызов производного
// метода G()
}
}
Как видно из приведенного примера, фрагмент программы включает описания базового класса A и производного класса B, каждый из которых содержит общедоступные методы F и G. При этом метод G класса использует описатель virtual. Как следует из комментариев, при задании методов F и G в производном классе B без использования описателя override происходит сокрытие производных методов F и G. Таким образом, для корректной работы F и G в классе необходимо использовать оператор new. В случае применения описателя override при задании метода G в классе B происходит замещение приоритетным методом G, и, таким образом, оператор new не требуется.

В ходе лекций неоднократно упоминалось понятие интерфейса. На данном этапе исследований становится возможным детально изучить данный механизм и рассмотреть особенности его реализации применительно к языку программирования C#.
Под интерфейсом будем понимать чисто абстрактный класс, который включает только описания языковых объектов и не содержит части, отвечающей за реализацию (implementation).
При реализации рассматриваемого механизма необходимо обеспечить выполнение следующих условий.
Прежде всего, требуется, чтобы в состав интерфейсов входили только такие объекты языка как методы, свойства, индексаторы и события. Интерфейсы не могут содержать полей, констант, конструкторов, деструкторов, операторов и вложенных типов.
Кроме того, все элементы интерфейсов должны быть описаны как общедоступные (public) и абстрактные виртуальные (virtual).
В состав интерфейсов не могут входить статические элементы.
Механизм множественного наследования, рассмотренный ранее в ходе курса, может быть реализован посредством классов и структур (далее будет рассмотрен еще один пример подобного рода наследования).
Заметим также, что произвольный интерфейс может быть расширен посредством одного или нескольких интерфейсов.
Проиллюстрируем особенности использования механизма методов с приоритетами в языке программирования C# следующим фрагментом программы:
public interface IList :
ICollection, IEnumerable {
int Add (object value);
// методы
bool Contains (object value);

    ...
bool IsReadOnly {
get;
}
// свойства
...
object this [int index] {
get;
set;
}   
// индексатор
}
Как видно из приведенного примера, фрагмент программы на языке C# содержит описание общедоступного интерфейса IList на основе стандартных классов ICollection и IEnumerable. Интерфейс моделирует списковую структуру данных и оснащен методами Add для добавления объекта в список, Contains для установления принадлежности объекта списку и IsReadOnly для определения возможности записи в список (а, возможно, и рядом других методов), тем или иным набором свойств, а также индексатором.
Рассмотрим более подробно особенности реализации множественного наследования в языке программирования C#. Для достижения этой цели потребуется применить механизм интерфейсов для классов и структур.
Сформулируем требования, необходимые для реализации данного варианта механизма интерфейсов.
Прежде всего, следует потребовать, чтобы класс имел возможность наследования свойств лишь единственного базового класса. Однако при этом класс должен иметь возможность реализации множественных интерфейсов.
Кроме того, необходимо выдвинуть условия для структур, которые сводятся к следующему. Структура может наследовать свойства любого типа данных и при этом имеет возможность реализации множественных интерфейсов.
Подчеркнем, что важным свойством элементов, входящих в состав интерфейса (в частности, методов, свойств и индексаторов) является возможность реализации или наследования свойств порождающего их класса.
Заметим далее, что реализованные методы интерфейса не могут быть замещенными, т.е. задаваться с помощью описателя override.
Наконец, методы, которые реализуются посредством интерфейса, могут снабжаться одним из описателей, virtual либо abstract. Таким образом, осуществляется возможность реализации механизма интерфейсов на основе абстрактного класса.
Проиллюстрируем особенности использования механизма методов с приоритетами в языке программирования C# следующим фрагментом программы:
class MyClass :
MyBaseClass, IList, ISerializable
{
public int Add (object value)
{
...
}
public bool Contains (object value)
{
...
}
...
public bool IsReadOnly
{
get {
...
}
}
...

    public object this [int index]
{
get {
...
}
set {
...
}
}
}
Как мы видим, фрагмент программы на языке C# содержит описание общедоступного интерфейса IList на основе стандартных классов ICollection и IEnumerable.
Данный пример во многом схож с предыдущим, однако имеет одно фундаментальное отличие от него.
Отличие заключается в том, что интерфейс IList в данном примере реализуется на основе базового класса MyBaseClass, определенного пользователем. При этом интерфейс IList в данном примере фактически получает возможность множественного наследования.
Заметим, что ни один из методов интерфейса Ilist не является замещенным.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика