Сокрытие членов в C#

Исходя из предыдущего примера, несложно заключить, что, несмотря на свою мощность, наследование может быть использовано некорректно. Когда программисты впервые узнают о наследовании, они склонны применять его слишком часто, создавая проекты и иерархические структуры, которые впоследствии трудно сопровождать.

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

Обратите внимание, что наследование расширяет функциональность, но не может ее исключать. Например, общедоступные методы базового класса доступны через экземпляры производного класса и классов, унаследованных от него. Удалить эти функциональные возможности из производного класса нельзя.

Рассмотрим следующий код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class А
{
public void DoSomething()
{
System.Console.WriteLine( "A.DoSomething" );
}
public class В : A {
public void DoSomethingElse ()
{
System.Console.WriteLine ( "B.DoSomethingElse" );
}
public class EntryPoint {
static void Main() {
В b = new В () ;
b.DoSomething();
b.DoSomethingElse ();
}

Здесь в Main создается новый экземпляр класса В, унаследованного от класса А. Класс В получает объединение членов обоих классов — А и В. Вот почему можно вызывать и DoSomething, и DoSomethingElse на экземпляре класса В. Это вполне очевидно, поскольку наследование расширяет функциональность.

Но что, если необходимо наследовать от класса А, но скрыть метод DoSomething? Другими словами, что если требуется расширить лишь часть функциональности А?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class А
{
public void DoSomething()
{
System.Console.WriteLine ( "A.DoSomething" );
}
}
public class В : A
{
public void DoSomethingElse ()
{
System.Console.WriteLine( "B.DoSomethingElse" );
}
public new void DoSomething() {
System.Console.WriteLine( "B.DoSomething" );
}
public class EntryPoint {
static void Main() {
В b = new В () ;
b.DoSomething();
b.DoSomethingElse ();
A a = b;
a.DoSomething();
}

Как видите, в этой версии в класс В введен новый метод по имени DoSomething. Также обратите внимание на добавление ключевого слова new к объявлению В.DoSomething. Если не добавить это ключевое слово, то компилятор выдаст предупреждение. Это его способ сообщить о необходимости выражаться яснее относительно сокрытия метода базового класса. Возможно, компилятор делает так потому, что подобное сокрытие членов обычно трактуется как плохой дизайн.

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

1
2
3
В.DoSomething
В.DoSomethingElse
A.DoSomething

Первое, на что следует обратить внимание: то, какой именно метод DoSomething будет вызван, зависит от типа ссылки, через которую он будет вызван. Это достаточно не очевидно, поскольку В является А, а вы знаете, что наследование моделирует отношение “является”. В данном случае должен ли весь общедоступный интерфейс

А быть доступным потребителям экземпляра класса В? Если кратко, то нет. Когда действительно необходимо, чтобы метод вел себя по-разному в подклассах, тогда в точке определения класса А метод DoSomething должен быть объявлен виртуальным. При таком подходе правильнее было бы воспользоваться полиморфизмом. В этом случае должна вызываться самая последняя версия (версия наследника) метода DoSomething, независимо от того, через какой тип ссылки он был вызван.

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

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

Несмотря на то что класс В теперь скрывает реализацию DoSomething класса А, помните, что он не удаляет ее.

Он скрывает ее при вызове этого метода через ссылку типа В на объект. Однако в методе Main это легко обойти, применив неявное преобразование ссылки на экземпляр типа В в ссылку на экземпляр типа А с последующим вызовом через нее реализации A.DoSomething. То есть реализация A.DoSomething не исчезла, она просто скрыта. И чтобы добраться до нее, понадобится просто проделать немного больше работы.

Предположим, что передается ссылка на экземпляр В методу, принимающему ссылку на экземпляр А, подобно тому, как это делается в примере с DrawShape.

Ссылка на экземпляр В должна быть неявно преобразована в ссылку на экземпляр А, и если за этим последует вызов метод DoSomething этого экземпляра А, то получится A. DoSomething вместо В. Some Thing. Наверное, это не то, чего ожидал тот, кто вызвал данный метод.

Это — классический пример того, что если язык позволяет делать нечто подобное, это не означает, что поступая так, вы создадите хороший дизайн. Почти любые существующие языки, включая С++, обладают такими средствами, которые, будучи использованными (у тому же неправильно), лишь привнесут излишнюю сложность и ухудшат дизайн.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100