Опубликован: 19.08.2004 | Уровень: для всех | Доступ: платный | ВУЗ: Национальный исследовательский ядерный университет «МИФИ»

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

Рассмотрим особенности реализации сокрытия данных в языке программирования 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 не требуется.