Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-6

.pdf
Скачиваний:
6
Добавлен:
05.02.2023
Размер:
4.5 Mб
Скачать

§ 4.6. Наследование

Чтобы построить – в терминах данных или поведения – класс на основе другого класса, применяется наследование. Выделим из синтаксиса описания класса часть, связанную с наследованием:

class <идентификатор> [<параметры типа>] [: <список наследования>] <список наследования> :: {<тип класса> | <тип интерфейса>} [, ...]

Если список наследования не указан, то класс неявно наследуется от класса System.Object. В списке наследования может быть только один базовый класс, но неограниченное количество интерфейсов.

Базовый класс не должен быть классом System.Array, System.Delegate, System.MulticastDelegate, System.Enum или System.ValueType. Кроме того,

объявление универсального класса (см. § 5.1) не может использовать System.Attribute в качестве базового класса.

4.6.1. Свойства наследования

Класс наследует члены своего прямого базового класса. Наследование означает, что класс неявно содержит все члены его прямого базового класса, исключая конструкторы экземпляров, деструкторы и статические конструкторы базового класса. Важные аспекты наследования:

Наследование транзитивно. Если C произведено из B, а B произведено из A, то C унаследует как члены, объявленные в B, так и члены, объявленные

вA;

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

Конструкторы экземпляров, деструкторы и статические конструкторы не наследуются, но все другие члены, независимо от их объявленной доступности, наследуются. Однако, в зависимости от их объявленной доступности, унаследованные члены могут быть недоступны в производном классе;

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

291

водного класса;

Экземпляр класса содержит неявное преобразование из типа производного класса в любой из его типов базового класса. Таким образом, ссылка на экземпляр некоторого производного класса может обрабатываться как ссылка на экземпляр какого-либо из его базовых классов;

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

В отличие от языка C++, множественное наследование в языке C# не поддерживается, а структуры вообще нельзя наследовать. Класс или структура может реализовывать несколько интерфейсов, но такой вид наследования существенно отличается от множественного наследования классов.

Пример:

class X { } class Y { }

class Z1 : X, Y { } // Ошибка

interface IMy1 { } interface IMy2 { }

class Z2 : X, IMy1, IMy2 { } // ОК

Запрещено циклическое наследование. Так, если класс A наследуется от B, а класс B наследуется от C, то класс C не может наследоваться от A. Пример:

class A1: B1

{ }

 

class B1: C1

{ }

 

class C1: A1

{ }

// Ошибка

class A2

: B2.C2

{ }

class B2

: A2 //

Ошибка

{

 

 

 

public class C2 { }

}

class A3

{

class B3 : A3 { } // ОК

}

Пример: Samples\4.6\4_6_1_inherit.

292

4.6.2. Доступ к членам при наследовании

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

Производный класс имеет доступ к членам базового класса, которые:

Объявлены с модификатором public – без ограничений;

Объявлены с модификатором protected или protected internal – без ограничений;

Объявлены с модификатором internal – если базовый класс описан в той же программе, что и производный.

К полям с модификатором private доступа нет, хотя они могут опосредованно вызываться при обращении к другим членам базового класса (методам, свойствам и т.д.).

Пример:

class MyBaseClass1

{

public int A = 0; internal int B = 1;

protected internal int C = 2; protected int D = 3;

private int E = 4;

}

class MyDerClass1 : MyBaseClass1

{

void F()

{

A = 5;

B = 5;

C = 5;

D = 5;

E = 5; // Ошибка

}

}

4.6.2.2. Сокрытие членов

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

1) Указать у данного члена в производном классе модификатор new. Тем самым мы покажем компилятору, что сокрытие совершено сознательно. Соответствующий член базового класса нам больше не нужен, вместо него

293

определяется новый член.

2) Если соответствующий член базового класса является виртуальным, указать модификатор override. Тогда новый член перегрузит член базового класса (см. § 4.7).

Если же сигнатуры членов не совпадают, то единственный способ избавиться от предупреждений компилятора – использовать модификатор new. Или дать члену в производном классе другое имя. Пример:

abstract class MyBaseClass2

{

abstract public void A(); virtual protected void B() { }

virtual public void C() { Console.WriteLine("B.C"); } virtual public void D() { Console.WriteLine("B.D"); } virtual public void E() { Console.WriteLine("B.E"); }

public void M1() { } private void M2() { } protected struct M3 { }

}

class MyDerClass2 : MyBaseClass2

{

public override void A() { }

//Предупреждение

//void B() { }

//Ошибка

//public override void B() { }

new void C() { Console.WriteLine("D.C"); }

new public void D() { Console.WriteLine("D.D"); } override public void E() { Console.WriteLine("D.E"); }

new public void M3() { } private void M2() { }

new protected struct M1 { }

}

static int Main()

{

MyDerClass2 der = new MyDerClass2(); der.C(); // B.C ((MyBaseClass2)der).C(); // B.C der.D(); // D.D ((MyBaseClass2)der).D(); // B.D der.E(); // D.E ((MyBaseClass2)der).E(); // D.E return 0;

}

Предупреждение для «void B()» связано как раз с тем, что в базовом классе тоже есть член B, но ни модификатор new, ни модификатор override не указаны. Хотя в данном случае override указывать нельзя, т.к. сигнатуры членов не совпадают. Правила для перегрузки виртуальных методов очень

294

строгие – не должны различаться даже уровни доступа (в отличие от языка C++, например). С этим связана ошибка для «public override void B()» – в базовом классе указан другой уровень доступа protected. При выводе на консоль метод C() всегда вызывается для базового класса, потому что в производном классе он закрыт (private). Метод D() мы можем вызвать как для базового (используя операцию преобразования типа), так и для наследуемого класса. Метод E() виртуальный, поэтому всегда вызывается для классапотомка.

Для членов M1 и M3 предупреждения не показываются, т.к. указан модификатор new. Иначе компилятор бы отобразил их, несмотря на то, что сигнатуры у них совершенно разные – один из членов является методом, а другой – структурой. Для M2 предупреждение не отображается, т.к. член является закрытым (private) в базовом классе. Следовательно, класс-потомок никак не может получить к нему доступ, и смысл перекрытия теряется.

4.6.2.3. Доступ к конструктору базового класса

В языке C++ доступ к конструктору базового класса осуществлялся следующим образом:

class MyClass1

{

public:

MyClass1( /* список формальных параметров */ ) {}

};

class MyClass2 : public MyClass1

{

public:

MyClass2( /* список формальных параметров */ ) : MyClass1( /* список фактических параметров */ ) {}

};

В языке C# вместо имени базового класса используется ключевое слово base (см. пункт 4.4.2). Как и в языке C++, конструктор по умолчанию (если он определен в базовом классе, или если в базовом классе не определен ни один конструктор) указывать не обязательно. Если в базовом классе есть конструкторы с параметрами, и нет конструктора по умолчанию, в производном классе обязательно должны быть конструкторы, явно обращающиеся к конструкторам базового класса.

Пример:

class MyBaseClass3

295

{

public MyBaseClass3(int i) { }

}

class MyDerClass31 : MyBaseClass3 // Ошибка

{

}

class MyDerClass32 : MyBaseClass3 // ОК

{

protected MyDerClass32() : base(5) { } // ОК protected MyDerClass32(string s) { } // Ошибка

}

class MyBaseClass4

{

public MyBaseClass4() { } public MyBaseClass4(int i) { }

}

class MyDerClass41 : MyBaseClass4 // ОК

{

}

class MyDerClass42 : MyBaseClass4 // ОК

{

public MyDerClass42() : base(5) { } // ОК public MyDerClass42(string s) { } // ОК

}

Пример: Samples\4.6\4_6_2_access.

4.6.3. Абстрактные классы

Как видно из примера выше, класс, содержащий хотя бы один абстрактный член (объявленный в нем самом или базовом классе), также должен быть абстрактным. Хотя обратное утверждение неверно – в абстрактном классе может не быть абстрактных членов. Абстрактные члены не имеют тела и не могут быть закрытыми (private) – иначе как класс-потомок сможет их перегрузить?

Запрещено создавать экземпляры абстрактных классов. Абстрактный класс – это некоторый «набросок», довершать картину которого будут клас- сы-потомки. Мы уже знакомы с абстрактными классами файловых потоков, но есть множество других.

Пример:

class C1

{

abstract void F(); // Ошибка

protected abstract void F() { } // Ошибка

}

296

class C2 // Ошибка

{

protected abstract void F();

}

abstract class C3

{

protected abstract void F(); // ОК

}

class C4 : C3 // Ошибка

{

}

class C5 : C3 // ОК

{

protected override void F() { } // ОК

}

abstract class C6 : C3 // ОК

{

abstract C6(); // Ошибка abstract ~C6(); // Ошибка

}

abstract class C7 { } class C8 : C7 { }

static int Main()

{

C7 x = new C7(); // Ошибка

C8 x = new C8();

return 0;

}

Класс C7 описан как абстрактный, хотя не содержит абстрактных чле-

нов.

Конструкторы и деструкторы, в отличие от других методов, не могут быть абстрактными.

Пример: Samples\4.6\4_6_3_abstract.

4.6.4. Изолированные классы

Если мы хотим быть уверенными, что класс никогда не будет использован как базовый, необходимо объявить его как изолированный (запечатанный) класс. При этом применяется модификатор sealed. Единственное ограничение: абстрактный класс не может быть изолированным, так как в силу своей природы абстрактные классы предназначены для использования в качестве базовых классов. Хотя изолированные классы предназначены для

297

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

Также изолированный класс не может объявлять новые виртуальные члены. Пример:

class C1

{

public virtual void F() { }

}

sealed class C2 : C1 // ОК

{

public virtual void F2() { } // Ошибка public override void F() { } // ОК

}

abstract class C3 // ОК

{

public abstract void F(); // ОК

}

sealed class C4 : C3 // Ошибка

{

}

class C5 : C3 // ОК

{

public override void F() { } // ОК

}

Пример: Samples\4.6\4_6_4_sealed.

298

§ 4.7. Перегрузка и полиморфизм

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

4.7.1. Статический полиморфизм

Перегрузка полезна, по крайней мере, в трех сценариях. Первый: нам нужно иметь единое имя метода, поведение которого немного различается в зависимости от типа переданных аргументов. Второй сценарий, в котором выгодно применять перегрузку метода, – использование конструкторов, которые, в сущности, представляют собой методы, вызываемые при создании экземпляра объекта. Допустим, мы хотите создать класс, который может быть построен несколькими способами. Например, он использует описатель (int) или имя (string) файла, чтобы открыть его. Поскольку правила C# диктуют, что у конструктора класса должно быть такое же имя, как и у самого класса, мы не можете просто создать разные методы для переменных каждого типа. Вместо этого нужно использовать перегрузку конструктора. Третий сценарий – перегрузка индексатора. Т.к. он имеет фиксированное имя this, перегрузка является единственным вариантом для определения в классе нескольких индексаторов. Такой вариант перегрузки иногда называют статическим полиморфизмом.

О перегрузке метода важно помнить следующее: список аргументов каждого метода должен отличаться. При этом модификаторы ref и out не различаются. Если при перегрузке возникают конфликты с членами базового класса, как мы уже отмечали, необходимо использовать модификатор new.

Пример:

class MyClass

{

public MyClass(int i) { } // ОК public MyClass(string s) { } // ОК

299

void F(int x, ref

double

y) { }

// ОК

 

int F(int x, ref double y)

{

return

0; }

// Ошибка

void F(out int x,

out

double

y)

{ x

= 1;

y = 1; } // ОК

void

F(int

x,

out

double

y) { }

// Ошибка

 

void

F(int

x,

ref

int

y)

{

}

//

ОК

 

 

public int this[int i] { get { return 0; } } // ОК public int this[string s] { get {return 0; } } // ОК

public int this[params int[] a] { get { return 0; } } // ОК

}

Пример: Samples\4.7\4_7_1_stat.

4.7.2.Виртуальный полиморфизм

Вотличие от статического полиморфизма, виртуальный полиморфизм работает несколько по-другому. Перегрузка члена класса с помощью ключевого слова new возможна, если у нас есть ссылка на экземпляр производного класса. А что будет, если есть ссылка, приведенная к типу базового класса, но нужно, чтобы компилятор вызывал реализацию метода из производного класса? Это именно тот момент, когда в дело вступает виртуальный полиморфизм. Он позволяет переопределить часть функциональности базового класса в классах-потомках.

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

Пример: Samples\4.7\4_7_2_virt.

4.7.2.1. Перегрузка методов

Для объявления виртуального метода в базовом классе следует использовать модификаторы virtual и abstract. В первом случае виртуальный метод должен содержать тело метода (если он не является внешним), во втором случае имеем абстрактный метод. Он не может иметь тела и не может являться внешним (extern). Допускается объявление абстрактных членов только в абстрактных классах. При перегрузке метода применяется модификатор override, причем сигнатуры перегружаемого и перегружающего методов должны полностью совпадать (тип возвращаемого значения, список формальных параметров, дополнительные модификаторы). Пример:

abstract class AbsBase

300