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

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

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

{

protected abstract int F1();

public virtual int F2() { return 0; }

private virtual int F3() { return 0; } // Ошибка public virtual extern void F4();

public virtual int F5() { return 0; }

}

class Base : AbsBase

{

public override int F1() { return 1; } // Ошибка protected override int F1() { return 1; }

public override int F2() { return 1; }

}

class Child : Base

{

protected override void F1() { return 2; } // Ошибка public override int F2() { return 2; }

public new int F5() { return 2; }

}

По сравнению с языком C++, в синтаксис введены некоторые ограничения. Во-первых, виртуальные и абстрактные методы нельзя объявлять как закрытые (AbsBase.F3 в примере). В принципе, это не влияет на функциональность, поскольку такие методы по определению не будут видимы в производных классах. Во-вторых, для таких методов нельзя изменять модификатор доступа при перегрузке (Base.F1 в примере). В принципе, ужесточение уровня доступа к виртуальному методу в производном классе не имеет смысла, т.к. мы всегда можем преобразовать ссылку на производный класс к ссылке на базовый класс, в котором этот метод имеет более доступный модификатор. Пример для C++:

class Base

{

public:

virtual void F(void) { return 0; }

};

class Child : public Base

{

protected:

virtual void F(void) { return 1; }

};

int main(void)

{

Child x;

x.F(); // Ошибка

((Base &)x).F(); // Но так ошибки нет! return 0;

}

301

Однако смягчение уровня доступа иногда было полезно. Тем не менее, разработчики языка C# отказались и от этого.

В этом примере при наследовании был использован модификатор public. Мы помним, что в языке C++ при наследовании можно было использовать и другие модификаторы доступа, а по умолчанию использовался private. В языке C# от этого отказались, модификатор не указывается, а подразумевается public. Это логично, другие, более защищенные, модификаторы доступа не имеют особого смысла. По показанной выше схеме всегда можно преобразовать ссылку к типу базового класса, где уровень защиты ниже.

Эффект от перегрузки заключается в том, что при вызове, например, метода F2 для экземпляра класса Base всегда будет вызываться метод Base.F2, а для класса Child – Child.F2, независимо от преобразований типов. Метод F5 в классе Base не переопределен, поэтому для экземпляра этого класса будет вызываться AbsBase.F5. А в классе Child определен новый метод F5, не имеющий отношения к виртуальному методу AbsBase.F5. Поэтому здесь то, какой метод будет вызван, уже зависит от типа ссылки. Сравните:

Base b = new Base();

Child c = new Child();

Console.WriteLine(b.F2()); // 1

Console.WriteLine(b.F5()); // 0

Console.WriteLine(((AbsBase)b).F2()); // 1

Console.WriteLine(((AbsBase)b).F5()); // 0

Console.WriteLine(c.F2()); // 2

Console.WriteLine(c.F5()); // 2

Console.WriteLine(((Base)c).F2()); // 2

Console.WriteLine(((Base)c).F5()); // 0

Console.WriteLine(((AbsBase)c).F2()); // 2

Console.WriteLine(((AbsBase)c).F5()); // 0

Другой пример:

static void F(object obj)

{

Console.WriteLine(obj.GetType());

}

static int Main()

{

Child c = new Child();

F(c); // VirtualPolymorphismSample.Program+Child F(1); // System.Int32

F("abc"); // System.String return 0;

}

Если бы метод GetType() не был виртуальным, на консоли всегда бы

302

отображался текст «System.Object».

4.7.2.2. Доступ к виртуальному методу базового класса

В языке C++ мы могли получить доступ ко всей цепочке виртуальных методов, причем как из класса-потомка, так и из внешнего класса или процедуры (если это позволял модификатор доступа). Делалось это при помощи преобразования указателя или ссылки на экземпляр класса (как в примере выше), или при помощи квалификатора области действия

<имя класса>::<член>

Так, вместо записи «((Base &)x).F()» можно использовать запись «x.Base::F()». При этом можно указывать не только непосредственные базовые классы, но и любые другие базовые классы вверх по иерархии. В языке C# такой возможности нет. Извне класса мы вообще никак не можем повлиять на цепочку вызовов виртуальных методов, а в самом классе можем вызывать виртуальные методы лишь непосредственных предков класса, используя уже знакомое ключевое слово base:

base.<член>

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

class Child : Base

{

public override int F2() { return 2; } public new int F5() { return 2; }

public int F6() { return F2() + base.F2(); }

}

static int Main()

{

Child c = new Child();

Console.WriteLine(c.F6()); // 3 return 0;

}

4.7.2.3.Перегрузка свойств и индексаторов

Усвойств и индексаторов, как и методов, могут быть указаны модификаторы virtual, override или abstract. Это позволяет производным классам наследовать и перегружать свойства подобно любому другому члену, унаследованному от базового класса. При этом, если в базовом классе есть оба

303

метода – получатель и установщик – при перегрузке одного из них нужно также перегружать и второй.

Данные модификаторы указываются для всего свойства или индексатора, а не для его методов доступа. У абстрактного свойства или индексатора, в отличие от абстрактных методов, должно быть тело – в нем нужно определить, какие предусмотрены методы доступа. Методы доступа в абстрактном свойстве или индексаторе тел не имеют. При доступе к индексатору базового класса используется синтаксис

base"["<список фактических аргументов>"]"

В остальном действуют те же правила, что и при перегрузке методов. Пример:

abstract class AbsBase

{

public virtual int Prop { get { return 0; } } public abstract string this[int i]

{

get;

}

}

class Base : AbsBase

{

public override int Prop { get { return base.Prop + 1; } } public override string this[int i]

{

get { return "Base." + i; }

}

}

class Child : Base

{

public override int Prop { get { return base.Prop + 1; } } public override string this[int i]

{

get { return "Child." + base[i]; }

}

}

static int Main()

{

Base b = new Base(); Child c = new Child();

Console.WriteLine(b.Prop); // 1 Console.WriteLine(b[5]); // Base.5 Console.WriteLine(c.Prop); // 2 Console.WriteLine(c[7]); // Child.Base.7 return 0;

}

304

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

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

Пример:

class Child : Base

{

public override sealed int F2() { return 2; }

}

class Child2

{

public override int F2() { return 3; } // Ошибка

}

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

4.7.3. Перегрузка операторов

Перегрузка операторов позволяет переопределить стандартные операторы выражений языка C# для применения их к типам, определенным пользователем.

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

int[] a = { 1, 2, 3 }; int[] b = { 4, 5, 6 }; int[] c = a + b; // Ошибка

Однако, некоторые структуры (DateTime) и типы по ссылке (String) переопределяли ряд операторов таким образом, что их можно было использовать для аргументов данных типов. Мы можем поступить так же.

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

4.7.3.1.Ограничения синтаксиса

Сточки зрения класса, описание перегруженного оператора выглядит похоже на описание метода:

<перегрузка оператора> :: [<атрибуты>] public static [extern]

305

<описание оператора> <тело оператора>

<описание оператора> :: <унарный оператор> <описание оператора> :: <бинарный оператор>

<описание оператора> :: <оператор преобразования типа>

<унарный оператор> :: <тип> operator <оператор> (<тип1> <идентификатор1>)

<бинарный оператор> :: <тип> operator <оператор> (<тип1> <идентификатор1>, <тип2> <идентификатор2>)

<оператор преобразования типа> :: {implicit | explicit} operator <тип> (<тип1> <идентификатор1>)

<тело оператора> :: <блок> <тело оператора> :: ;

Все параметры операторов должны быть параметрами-значениями. Любые модификаторы параметров недопустимы.

При этом язык C# относится к перегрузке операций более строго, чем язык C++. Не останавливаясь на подробном сравнении подходов к перегрузке операторов в этих языках, рассмотрим ограничения, налагаемые на перегрузку операторов компилятором языка C#:

1)Из унарных можно перегрузить только операторы «+», «-», «!», «~»,

«++», «--», «true» и «false»;

2)Из бинарных можно перегрузить арифметические операторы «+», «- », «*», «/», «%», побитовые операторы «&», «|», «^», «<<», «>>» и операторы сравнения «==», «!=», «>», «<», «>=», «<=»;

3)Все методы, представляющие перегружаемые операторы, должны иметь модификаторы public и static;

4)Нельзя перегрузить оператор индексации «[]», но вместо этого в классе можно определить индексатор, обладающий даже более широкими возможностями;

5)Нельзя перегрузить оператор присваивания «=». Он реализован на системном уровне, и обеспечивает побитовое копирование для типов по значению и копирование ссылок для ссылочных типов. Также запрещена перегрузка составных операторов присваивания. Однако, если в классе определен разрешенный для перегрузки арифметический или побитовый оператор «op», то составной оператор «op=» определяется автоматически;

6)Операторы, которые в настоящее время в языке C# не определены, также не перегружаются. Например, мы не можем определить оператор «**» как разновидность возведения в степень, поскольку в языке C# не определен оператор «**»;

306

7)Нельзя изменить синтаксис операторов. Мы не можем изменить бинарный оператор «*» так, чтобы он принимал три аргумента, поскольку его синтаксис по определению подразумевает два аргумента;

8)Приоритет операторов при перегрузке не изменяется. Дополнительные ограничения приведены в дальнейшем материале.

4.7.3.2. Унарные операторы

К переопределенным в классе T унарным операторам применяются следующие дополнительные ограничения:

унарные операторы «+», «-», «!» или «~» должны принимать единственный параметр типа T или T? и могут возвращать любой тип;

унарные операторы «++» или «--» должны принимать единственный параметр типа T или T? и должны возвращать тип T или тип, производный от него;

унарные операторы «true» или «false» должны принимать единственный параметр типа T или T? и должны возвращать тип bool. При этом эти операторы перегружаются только попарно.

Например, определим класс, содержащий вектор с целочисленными координатами. Операторы инкремента и декремента будут увеличивать или уменьшать количество компонентов в векторе. Также определим ряд конструкторов, метод ToString(), свойство, возвращающее размер вектора и индексатор для доступа к компонентам вектора:

class Vector

{

private int[] V;

public int Length

{

get { return V == null ? 0 : V.Length; }

}

public int this[int i]

{

get { return V[i]; } set { V[i] = value; }

}

public Vector()

{

V = null;

}

public Vector(int size)

307

{

V = new int[size];

}

public Vector(int[] arr)

{

V = (int[])arr.Clone();

}

public override string ToString()

{

string rez = "[ ";

for (int i = 0; i < Length; i++) rez += V[i] + " "; return rez + "]";

}

void Copy(Vector v, int pos, int len)

{

if (v.Length > 0) Array.Copy(v.V, 0, V, pos, len);

}

public static Vector operator ++(Vector v)

{

Vector rez = new Vector(v.Length + 1); rez.Copy(v, 0, v.Length);

return rez;

}

public static Vector operator --(Vector v)

{

Vector rez = new Vector(v.Length - 1); rez.Copy(v, 0, rez.Length);

return rez;

}

}

static int Main()

{

int[] a = { 1, 2, 3 }; int[] b = { 4, 5, 6 };

Vector v1 = new Vector(a); Vector v2 = new Vector(b); v1--;

v2++;

Console.WriteLine("v1 = " + v1); // v1 = [ 1 2 ] Console.WriteLine("v2 = " + v2); // v2 = [ 4 5 6 0 ] return 0;

}

Обратите внимание на следующие аспекты:

1) Мы не указываем, какую именно форму инкремента и декремента перегружаем (префиксную или постфиксную). Это определяется из контекста применения оператора. При этом операторы ведут себя естественно:

Vector vtest1 = v1++; Vector vtest2 = --v2;

Console.WriteLine("vtest1 = " + vtest1); // vtest1 = [ 1 2 ]

308

Console.WriteLine("v1 = " + v1); // v1 = [ 1 2 0 ] Console.WriteLine("vtest2 = " + vtest2); // vtest2 = [ 4 5 6 ] Console.WriteLine("v2 = " + v2); // v2 = [ 4 5 6 ] Console.WriteLine();

Т.е. результатом выражения «v1++» будет старое значение вектора v1, а результатом «--v2» – новое значение v2.

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

4.7.3.3. Бинарные операторы

К переопределенным в классе T бинарным операторам применяются следующие дополнительные ограничения:

бинарные операторы побитового сдвига «<<» или «>>» должны принимать два параметра, первый из которых должен иметь тип T или T?, а второй – int или int?, и может возвращать любой тип;

остальные бинарные операторы должны принимать два параметра, по крайней мере один из которых должен иметь тип T или T?, и может возвращать любой тип;

следующие операторы должны быть объявлены попарно: «==» и «!=», «>» и «<», «>=» и «<=». Для каждого объявления одного оператора из пары должно быть соответствующее объявление другого оператора из пары. Два объявления операторов соответствуют, если у них одинаковый тип возвращаемого значения и одинаковый тип каждого параметра.

Например, перегрузим в классе Vector операторы сложения и вычитания векторов:

void CheckSize(Vector v)

{

if (Length != v.Length)

{

throw new ArgumentException("Размеры векторов не

совпадают!");

}

}

public static Vector operator +(Vector v1, Vector v2)

{

v1.CheckSize(v2);

Vector rez = new Vector(v1.V);

for (int i = 0; i < rez.Length; i++) rez[i] += v2[i]; return rez;

}

309

public static Vector operator -(Vector v1, Vector v2)

{

v1.CheckSize(v2);

Vector rez = new Vector(v1.V);

for (int i = 0; i < rez.Length; i++) rez[i] -= v2[i]; return rez;

}

Если размеры векторов не совпадают, генерируем исключительную ситуацию. Как уже отмечалось, теперь для векторов можно также применять операции «+=» и «-=»:

Console.WriteLine("v1 + v2 = {0}", v1 + v2); // v1 + v2 = [ 5 6 7 ] v1 -= v2;

Console.WriteLine("v1 = {0}", v1); // v1 = [ -3 -3 -6 ]

4.7.3.4. Операторы преобразования типов

Ранее упоминалось, что оператор «()», применяемый при приведении типов, не может быть перегружен, и вместо этого используется нестандартное преобразование.

Объявление оператора преобразования, включающее зарезервированное слово implicit, вводит неявное, а explicit – явное преобразование, определенное пользователем. Оператор преобразования преобразует от исходного типа, указанного типом параметра оператора преобразования, к конечному типу, указанному типом возврата оператора преобразования.

Для данного исходного типа S и конечного типа T, если S или T являются типами, допускающими присваивание null, разрешено объявлять преобразование типа, если:

S и T являются разными типами;

либо S, либо T является типом структуры или класса, где имеет место объявление этого оператора;

ни S, ни T не является типом интерфейса;

без преобразований, определенных пользователем, не существует преобразование от S к T или от T к S.

Примеры неправильных описаний операторов:

class TypeCast

{

public static implicit operator TypeCast(TypeCast c) { return

null; }

public static implicit operator Vector(int i) { return null; } public static implicit operator TypeCast(ICloneable c) { return

null; }

310