
Объектно-ориентированное программирование.-6
.pdf
{
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