Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Вызов базовых версий функций




Наследование

Типы наследования

Наследование реализации

Модификаторы доступа

Интерфейсы

 

 

В разделе 6.3 рассматривалось применение индивидуальных классов в С#. Основное вни­мание в ней было сосредоточено на том, как определять методы, свойства, конструкто­ры и другие члены отдельного класса (или отдельной структуры). Хотя вы узнали, что все классы изначально наследуются от класса System.Object, однако пока не видели, как соз­давать иерархии унаследованных классов. Темой настоящей главы является наследование. В ней вы узнаете, как С# и.NET Framework обрабатывают наследование.

Типы наследования

Раздел начинается сразу с рассмотрения того, что именно С# поддерживает в отношении наследования, а что - нет.

Сравнение наследования реализации и наследования

Интерфейса

В объектно-ориентированном программировании (ООП) существуют два различных типа наследования: наследование реализации и наследование интерфейса.

1. Наследование реализации (implementation inheritance) означает, что тип происхо­дит от базового типа, получая от него все поля-члены и функции-члены. При наследо­вании реализации производный тип адаптирует реализацию каждой функции базово­го типа, если только в его определении не указано, что реализация функции должна быть переопределена. Такой тип наследования более полезен, когда нужно добавить функциональность к существующему типу или же когда несколько связанных типов разделяют существенный объем общей функциональности.

2. Наследование интерфейса (interface inheritance) означает, что тип наследует толь­ко сигнатуру функций, но не наследует никакой реализации. Этот тип наследования наиболее полезен, когда нужно специфицировать, что тип обеспечивает доступ к оп­ределенным средствам.

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

Множественное наследование

Некоторые языки, такие как С++, поддерживают то, что известно под названием мно­жественного наследования, когда класс происходлт более чем от одного базового класса. Преимущества множественного наследования спорны. С одной стороны, нет сомнений, что можно применять множественное наследование для написания чрезвычайно слож­ного, но при этом компактного кода, что демонстрирует библиотека С++ ATL. С другой стороны, код, использующий множественное наследование, часто трудно понять и сложно отлаживать (все это также демонстрирует библиотека С++ ATL). Как уже упоминалось, об­легчение написания устойчивого кода было одной из ключевых целей проектирования С#. Соответственно, поэтому в С# множественное наследование не поддерживается. Однако С# позволяет типу наследовать множество интерфейсов. Это значит, что класс С# может наследоваться от другого класса и любого количества интерфейсов. На самом деле можно сказать точнее: благодаря наличию System.Object как всеобщего базового типа, каждый класс С# (за исключением Object) имеет строго один базовый класс и дополнительно мо­жет иметь любое количество базовых интерфейсов.

Структуры и классы

В разделе 6.3 была описана разница между структурами (типами значений) и классами (ссы­лочными типами). Одним из ограничений, налагаемых на структуры, является то, что они не поддерживают наследования, несмотря на тот факт, что каждая структура автоматиче­ски наследуется от System.ValueType. Фактически, нужно соблюдать большую осторож­ность. Это правда, что невозможно закодировать иерархию типов структур; однако струк­туры могут реализовывать интерфейсы. Другими словами, структуры не поддерживают наследование реализации, но поддерживают наследование интерфейса. Таким образом, подытожить ситуацию с пользовательскими типами можно следующим образом.

Структуры всегда наследуются от System.ValueType. Они могут также наследовать любое количество интерфейсов.

Классы всегда наследуются от одного класса по вашему выбору. Они также могут наследовать любое количество интерфейсов.

Наследование реализации

Для объявления, что класс наследуется от другого класса, применяется следующий син­таксис:

class УнаследованныйКласс: БазовыйКласс

{

// Данные-члены и функции-члены

}

Этот синтаксис очень похож на синтаксис С++ и Java. Однако программисты на С++, знакомые с концепцией общедоступного и приватного наследования, должны обратить внимание, что С# не поддерживает приватного наследова­ния; этим объясняется отсутствие квалификатора public или private пе­ред именем базового класса. Поддержка приватного наследования значительно усложняет язык, при этом принося весьма небольшую выгоду. На практике при­ватное наследование в С++ все равно используется чрезвычайно редко.

Если класс также наследует интерфейсы, то список базового класса и интерфейсов раз­деляется запятыми:

public class MyDerivedClass: MyBaseClass, Ilnterface1, IInterface2

{

// и т.д.

}

Для структур синтаксис выглядит так:

public struct MyDerivedStruct: Ilnterfacel, IInterface2

{

// и т.д.

}

Если при определении класса базовый класс не указан, то компилятор С# предполагает, что базовым классом является System.Object. Поэтому следующие два фрагмента кода эквивалентны:

class MyClass: Object // наследуется от System.Object

{

// и т.д.

}

class MyClass // наследуется от System.Object

{

// и т.д.

}

Для простоты чаще применяется вторая форма.

Поскольку в С# поддерживается ключевое слово object, служащее псевдонимом класса System.Object, можно записать и так:

class MyClass: object // наследуется от System.Object

{

// и т.д.

}

Чтобы сослаться на класс Object, используйте ключевое слово object, которое распо­знается интеллектуальными редакторами вроде Visual Studio.NET. Это облегчит редакти­рование кода.

Виртуальные методы

Объявляя функцию базового класса как virtual, вы тем самым позволяете ее переоп­ределять в классах-наследниках:

class MyBaseClass

{

public virtual string VirtualMethod()

{

return "Это - виртуальный метод, определенный в MyBaseClass";

}

}

Также допускается объявление свойства как virtual. Для виртуального или переопре­деленного свойства используется такой же синтаксис, что и для невиртуального свойства, за исключением ключевого слова virtual, добавляемого к определению. Синтаксис выгля­дит следующим образом:

public virtual string ForeName

{

get { return foreName; }

set { foreName = value; }

}

private string foreName;

Для простоты далее речь пойдет в основном о методах, хотя все сведения касаются также и свойств.

Концепция, лежащая в основе виртуальных функций С#, идентична стандартной кон­цепции ООП. Виртуальную функцию можно переопределить в классе-наследнике, и когда этот метод будет вызван, запустится его версия, относящаяся к соответствующему типу объекта. В С# по умолчанию функции не являются виртуальными, но (в отличие от кон­структоров) могут быть явно объявлены как virtual. Зто следует методологии С++: по причинам, связанным с производительностью, функции не виртуальные, если это не ука­зано явно. В отличие от этого, в Java все функции виртуальные. С# имеет отличающийся от С++ синтаксис, поскольку требует явного объявления, когда функция класса-наследника переопределяет другую функцию, с помощью ключевого слова override:

class MyDerivedClass: MyBaseClass

{

public override string VirtualMethod ()

{

return "Этот переопределенный метод объявлен в MyDerivedClass";

}

}

Этот синтаксис переопределения метода исключает потенциальные ошибки времени выполнения, которые могут легко возникать в С++, когда сигнатура метода в классе-нас­леднике непреднамеренно оказывается отличной от базовой версии, в результате чего метод наследника не может переопределить базовый метод. В С# это всплывает в виде ошибки компиляции, поскольку компилятор легко обнаруживает метод, для которого указан модификатор override, но при этом не имеющий базового метода, который он переопределяет.

Ни поля-члены, ни статические функции не могут быть объявлены виртуальными. Эта концепция просто не имеет смысла ни для каких членов класса, за исключением функций-членов уровня экземпляра.

Сокрытие методов

Если методы с одинаковой сигнатурой объявлены и в базовом, и в унаследованном клас­се, но при этом не указаны, соответственно, как virtual и override, то говорят, что вер­сия метода в классе-наследнике скрывает версию метода базового класса.

В большинстве случаев требуется переопределять методы, а не скрывать их. Скрывая методы, вы рискуете вызывать "неверный" метод для экземпляра данного класса. Однако, как показано в следующем примере, синтаксис С# спроектирован так, чтобы гарантиро­вать, что разработчик будет предупрежден об этой потенциальной проблеме во время ком­пиляции, тем самым обеспечивая возможность более безопасного сокрытия методов, если это действительно нужно. Это также дает преимущества разработчикам библиотек классов при работе с разными версиями.

Предположим, что имеется класс HisBaseClass:

class HisBaseClass

{

// разнообразные члены

}

В какой-то момент в будущем вы напишете класс-наследник, добавляющий некоторую функциональность к HisBaseClass. В частности, добавите метод MyGroovyMethod(), которого нет в базовом классе:

class MyDerivedClass: HisBaseClass

{

public int MyGroovyMethod()

{

// некая превосходная реализация

return 0;

}

}

Годом позже вы решите расширить функциональность базового класса. Случайно вы добавите метод, также именуемый MyGroovyMethod(), имеющий то же имя и сигнатуру, что и в наследнике, но, возможно, решающий какую-то другую задачу. Компилируя код, использующий новую версию базового класса, чвы получаете потенциальный конфликт, поскольку программа не знает, какой именно метод вызывать. Это совершенно коррект­но с точки зрения С#, но поскольку ваш MyGroovyMethod() никак не связан с версией MyGroovyMethod() из базового класса, то при запуске этого кода вы не получите того, чего ожидали. К счастью, язык С# спроектирован так, что прекрасно справляется с кон­фликтами подобного рода.

В таких случаях при компиляции С# генерирует предупреждение. Оно напомнит о не­обходимости применения ключевого слова new при выражении намерения сокрыть метод базового класса:

class MyDerivedClass: HisBaseClass

{

public new int MyGroovyMethod()

{

// некая превосходная реализация

return 0;

}

}

Но поскольку версия MyGroovyMethod() не объявлена как new, компилятор укажет на тот факт, что она скрывает метод базового класса, несмотря на отсутствие указания делать это, выдав предупреждение (это произойдет независимо от того, объявлен метод MyGroovyMethod() виртуальным или нет). Если хотите, можете переименовать свою версию метода. И это будет наилучшим решением, поскольку оно исключает будущую пу­таницу. Однако если по каким-то причинам вы решите не переименовывать такой метод (например, вы поставляете свой код в виде библиотек другим компаниям, а потому не можете изменять имена методов), то весь существующий клиентский код будет работать корректно, выбирая вашу версию MyGroovyMethod(). Это объясняется тем, что любой су­ществующий код, который обращается к этому методу, должен делать это через ссылку на MyDerivedClass (или на будущий класс-наследник).

Существующий код не может обращаться к этому методу через ссылку на HisBaseClass - будет выдана ошибка при компиляции более ранней версии HisBaseClass. Проблема может возникнуть только в клиентском коде. Компилятор С# ведет себя так, что вы получаете предупреждение о потенциальных проблемах, которые могут возникнуть в будущем коде. На это предупреждение нужно обязательно обратить внимание и позабо­титься о том, чтобы не пытаться вызывать вашу версию MyGroovyMethod() через любую ссылку на HisBaseClass в любом коде, который будет написан позже. Тем не менее, весь существующий код будет работать нормально. Может показаться, что это довольно тонкий момент, но в то же время это - красноречивый пример того, как С# может справляться с разными версиями классов.

Вызов базовых версий функций

В С# предусмотрен специальный синтаксис вызова базовых версий метода из произ­водного класса: base.<ИмяМетода>(). Например, если необходимо, чтобы метод произ­водного класса возвращал 90% значения, возвращенного методом базового класса, можете воспользоваться следующим синтаксисом:

class CustomerAccount

{

public virtual decimal CalculatePrice ()

{

// реализация

return 0.0M;

}

}

class GoldAccount: CustomerAccount

{

public override decimal CalculatePrice()

{

return base.CalculatePrice() * 0.9M;

}

}

Отметим, что синтаксис base.<ИмяМетода>() можно использовать для вызова любого метода базового класса - вы не обязаны вызывать его только из переопределенной версии того же метода.

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...