Переопределение реализаций интерфейсов в производных классах

Предположим, что у вас есть удобная реализация ComboBox, как в предыдущем разделе, и разработчик решил не герметизировать этот класс, чтобы вы могли наследоваться от него.

Я советую вам объявлять все свои классы как sealed, если только вы явно не собираетесь наследоваться от них.

Теперь представим, что вы создаете новый класс FancyComboBox и хотите, чтобы он как-то иначе себя рисовал — может быть, в некоторой новой психоделической теме. Вы можете попробовать что-нибудь вроде следующего:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
public interface IUIControl {
void Paint ();
void Show();
}
public interface IEditBox : IUIControl {
void SelectText ();
}
public interface IDropList : IUIControl {
void ShowList();
}
public class ComboBox : IEditBox, IDropList {
public void Paint ()  {
}
public void Show()   {
}
public void SelectText ()  {
}
 public void ShowList ()  {
}
}
public class FancyComboBox : ComboBox {
public void Paint ()  { }
}
public class EntryPoint {
static void Main()  {
FancyComboBox cb = new FancyComboBox();

Однако компилятор предупредит вас о том, что FancyComboBox. Paint скрывает ComboBox. Paint, и что вы, возможно, подразумевали использование ключевого слова new. Это покажется неожиданным, если вы предполагаете, что методы, реализующие методы интерфейса, должны быть автоматически виртуальными. В С# это не так.

“За кулисами” реализации методов интерфейсов вызываются так, будто они являются виртуальными. Любые реализации метода интерфейса, не помеченные virtual в С#, помечаются как virtual и final (герметизированный) в сгенерированном коде IL. Если же метод помечен как virtual в С#, то в сгенерированном коде IL он будет помечен как virtual и news lot (новый). Это может послужить причиной некоторой путаницы.

Столкнувшись с подобной проблемой, у вас есть два выбора. Один — заново реализовать интерфейс IUIControl в классе FancyComboBox:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
public interface IUIControl
{
void Paint ();
void Show ();
}
public interface IEditBox : IUIControl
{
void SelectText ();
}
public interface IDropList : IUIControl {
void ShowList ();
}
public class ComboBox : IEditBox, IDropList
{
public void Paint ()  {
Console .WriteLine ( "ComboBox. Paint () 11 );
}
public void Show()  { }
public void SelectText()  { }
public void ShowList()  { }
}
public class FancyComboBox : ComboBox, IUIControl {
public new void Paint ()  {
Console.WriteLine ( "FancyComboBox.Paint ()" );
}
}
public class EntryPoint {
static void Main()  {
FancyComboBox cb = new FancyComboBox ();
cb.Paint ();
( (IUIControl)cb).Paint();
( (IEditBox)cb).Paint();
}
}

В этом примере следует отметить два момента. Во-первых, FancyComboBox перечисляет IUIControl в списке наследования. Так вы указываете, что FancyComboBox собирается заново реализовать интерфейс IUIControl. Если бы IUIControl наследовался от другого интерфейса, то FancyComboBox пришлось бы повторно реализовать методы унаследованного интерфейса.

Я также должен был использовать ключевое слово new для FancyComboBox. Paint, поскольку он скрывает ComboBox.Paint. Это не было бы проблемой, если бы ComboBox реализовал метод IUIControl. Paint явно, поскольку он не был бы частью общедоступного контракта. Когда компилятор находит соответствие метода класса методу интерфейса, он также просматривает общедоступные методы базовых классов.

В реальности FancyComboBox должен был бы указать, что он заново реализует IUIControl.Paint, но без повторного объявления его методов, так что компилятор должен был бы просто связать интерфейс с методами базового класса. Конечно, это было бы бессмысленно, поскольку причина повторной реализации интерфейса в производном классе — изменение поведения.

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

Как уже неоднократно упоминалось, интерфейсы C#/CLR — это не что иное, как просто контракт, который означает согласие конкретного класса на реализацию всех методов, перечисленных в контракте, т.е. интерфейсе.
Если методы контракта интерфейса реализованы неявно, они должны быть общедоступными.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
public interface IUIControl
void Paint ();
void Show();
public interface IEditBox : IUIControl
void SelectText ();
public interface IDropList : IUIControl
void ShowList ();
public class ComboBox : IEditBox, IDropList
public virtual void Paint ()  {
Console.WriteLine ( "ComboBox.Paint ()" );
}
public void Show()  { }
public void SelectText ()  { }
public void ShowList ()  { }
}
public class FancyComboBox : ComboBox {
public override void Paint ()  {
Console.WriteLine ( "FancyComboBox.Paint () " );
}
public class EntryPoint {
static void Main()  {
FancyComboBox cb = new FancyComboBox ();
cb.Paint () ;
((IUIControl)cb) .Paint ();
((IEditBox)cb) .Paint ();
}
}

В этом случае класс FancyComboBox не обязан реализовать интерфейс IUIControl. Он должен просто переопределить виртуальный метод ComboBox. Paint. Намного яснее для ComboBox сразу объявлять Paint как virtual. Всякий раз, когда приходится использовать ключевое слово new для подавления предупреждений компилятора о сокрытии метода, следует рассмотреть возможность объявления метода базового класса как virtual.

Сокрытие методов вызывает путаницу и затрудняет понимание и отладку кода. Не забывайте: вы не должны делать что-либо только потому, что язык это позволяет.

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

Приходилось ли вам в прошлом работать с библиотекой Microsoft Foundation Classes (MFC) и попадать в ситуацию, когда при наследовании от класса MFC очень хотелось видеть какой-то определенный метод виртуальным? В таких случаях часто начинают обвинять проектировщиков MFC в чудовищной непредусмотрительности, заключающейся в том, что они не сделали метод виртуальным, когда в действительности у них, скорее всего, даже в мыслях не было, что кто-то захочет выполнить наследование от этого класса.

Вы можете следить за любыми ответами на эту запись через 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