Существует два способа повторного использования ранее разработанного и откомпилированного кода (простое копирование исходного кода в новый - не в счет):
Приведем пример кода, иллюстрирующего композицию. Пусть в нашем распоряжении имеется ранее разработанный (нами или кем-то другим) класс, моделирующий точку. Назовем его Point. Этот класс может существовать и в откомпилированном виде (или в исходном коде) и храниться где-то в подключаемой к проекту библиотеке. Пусть исходный код этого класса будет таким
using System;
namespace Test
{
class Point
{
int x, y;
public int X
{
get { return x; }
set { x = value; }
}
public int Y
{
get { return y; }
set { y = value; }
}
public Point()
{
x = y = 0;
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public void Show()
{
Console.WriteLine("x={0}; y={1}", x, y);
}
}
}
Используем класс Point для моделирования прямоугольника, представленного классом Rectangle. Состояние прямоугольника характеризуется двумя точками (обычно это координаты левого верхнего и правого нижнего углов), из которых можно вычислить другие характеристики, например, ширину, высоту и площадь.
Всегда в классе стремятся хранить только те данные, которые минимально необходимы для однозначного описания состояния объекта, а все, что можно легко вычислить, упаковывают в свойства или методы. Вот код модели прямоугольника, который иллюстрирует композицию объектов
namespace Test
{
class Rectangle
{
// Объявляем закрытые ссылки - поля
Point point1; // Левый верхний угол
Point point2; // Правый нижний угол
// Конструкторы на все случаи жизни
public Rectangle()
{
point1 = new Point();
point2 = new Point();
}
public Rectangle(Point point1, Point point2)
{
// Объекты созданы в клиенте
this.point1 = point1;
this.point2 = point2;
}
public Rectangle(int x1, int y1, int width, int height)
{
point1 = new Point(x1, y1);
point2 = new Point(x1 + width, y1 + height);
}
public Rectangle(int x1, int y1, Int64 x2, int y2)
{
point1 = new Point(x1, y1);
point2 = new Point((int)x2, y2);
}
// Свойства - сервисы
// Ширина прямоугольника
public int Width
{
get { return Math.Abs(point2.X - point1.X); }
}
// Высота прямоугольника
public int Height
{
get { return Math.Abs(point2.Y - point1.Y); }
}
// Методы - сервисы
// Площадь прямоугольника
public int Area()
{
return Width * Height;
}
// Точка внутри прямоугольника
public bool PointIsRect(Point point)
{
bool pointIsHorizontal = point.X >= point1.X
point.X <= point2.X;
bool pointIsVertical = point.Y >= point1.Y
point.Y <= point2.Y;
if (pointIsHorizontal pointIsVertical)
return true;
else
return false;
}
// Получение координат прямоугольника
public void GetCoord(out int x1, out int y1,
out int x2, out int y2)
{
x1 = point1.X;
y1 = point1.Y;
x2 = point2.X;
y2 = point2.Y;
}
}
}
Использование класса Rectangle
Результат выполнения программы будет таким

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

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

Говорят, что базовый класс наследуется производным классом, а производный класс наследует базовый класс.
Одно из отличий производного класса, сцепленного механизмом наследования с базовым классом, от составного класса, содержащего в себе экземпляры других классов, состоит в том, что производный класс наследует public -сервисы своего базового класса в готовом виде. Составному же классу нужно самостоятельно позаботиться о создании сервисов, если это необходимо, для включенных в него экземпляров.
Классы, стоящие на верхнем уровне иерархии наследования считаются более абстрактыми по сравнению с нижестоящими классами. Возможно, что более абстрактные классы не имеют конкретной реализации, а если и имеют, то таких реализаций значительно меньше, чем реализаций более конкретных классов. Говорят, что производный класс специфицирует ( расширяет конкретизацию) базовый класс дополнительными свойствами и поведением.
Базовый класс может наследоваться любым количеством других классов. Базовый класс не обязан знать, какой класс будет его наследовать. На данном этапе он считается самодостаточным и способным выступить в качестве родителя любого производного класса. Но если базовый класс в свою очередь произошел от другого базового класса, то он знает своего родителя и всех своих более дальних предков, так как содержит в себе все их свойства и функциональность.
Еще раз вспомним, что класс на этапе проектирования, это только описание возможностей будущей программной единицы. Сама программная единица создается во время выполнения программы как экземпляр этого описания, т.е. становится реальным объектом: объектом-типом, а потом, возможно, объектом-экземпляром. При наследовании на этапе проектирования мы создаем многоступенчатое описание, по которому обязательно будет создан слоеный объект-экземпляр.
Различают простое (одиночное) и множественное наследование. Связь в виде простого наследования предполагает, что производный класс имеет только одного родителя (происходит только от одного базового класса). Множественное наследование подразумевает, что

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

Наследование обеспечивает возможность постепенного наращивания кода путем объявления класса, встраивающего в себя другой ранее разработанный класс. В языке C# могут наследоваться классы и интерфейсы. Классы C# допускают только одиночное наследование других классов, но множественное наследование интерфейсов. Интерфейсы могут наследовать другие интерфейсы, причем множественно. С наследованием связано множество
специфических приемов работы с кодом, поддерживаемых языком C#.
Пусть имеется простой класс с именем Base. Выведем из него другой класс с именем Derived, в который будет встроен код класса Base. Код программы будет таким
using System;
namespace Test
{
class Base
{
public string fieldBase = "Поле в Base";
public string PropertiesBase
{
get { return "Свойство в Base"; }
set { fieldBase = value; }
}
public void MethodBase()
{
Console.WriteLine("Метод в Base");
}
}
class Derived : Base
{
public string fieldDerived = "Поле в Derived";
public string PropertiesDerived
{
get { return "Свойство в Derived"; }
set { fieldDerived = value; }
}
public void MethodDerived()
{
Console.WriteLine("Метод в Derived");
}
}
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Наследование классов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
// Создаем объект класса Derived
Derived derived = new Derived();
// Используем члены унаследованного класса Base
Console.WriteLine("Унаследованная часть
класса Derived");
Console.WriteLine(derived.fieldBase);
Console.WriteLine(derived.PropertiesBase);
derived.MethodBase();
Console.WriteLine();
// Используем члены расширения
Console.WriteLine("Собственное
расширение класса Derived");
Console.WriteLine(derived.fieldDerived);
Console.WriteLine(derived.PropertiesDerived);
derived.MethodDerived();
Console.ReadLine();
}
}
}
Здесь для удобства все классы помещены в один файл с именем Program.cs. Результат выполнения выглядит так

Класс, который наследуется, называется базовым, а класс, который является наследником, называется производным или расширением базового класса. Говорят, что производный класс выведен из базового, а базовый класс наследует производному. Также говорят, что производный класс является потомком базового класса, а базовый класс его предком. Любой класс, участвующий в многоуровневой цепочке наследования, не может быть потомком самого себя, т.е не может участвовать более чем один раз.
Базовый и производный классы составляют цепочку наследования и создают иерархию классов. Оболочка Visual Studio 2005 имеет встроенные средства графического представления иерархии классов, которые вызываются в панели Solution Explorer командой View Class Diagram контекстного для файла меню или одноименной кнопкой этой панели.

Графическое представление цепочки наследования для нашего примера выглядит так

Наследственную зависимость принято указывать стрелками по направлению от производного класса к базовому и располагать классы так, чтобы на вершине многоуровневой иерархической лестницы стоял базовый класс.
Код производного класса по отношению к присоединенному базовому классу является внешним, хотя и родственным. Поэтому внутренние private -члены базового класса недоступны в коде производного, общедоступные public -члены доступны также, как и для любого неродственного кода, а защищенные protected -члены базового класса недоступны коду неродственного класса, но доступны в коде производного класса. Например
using System;
namespace Test
{
// Базовый класс
class Base
{
int a; // Внутренний член базового класса
protected int b; // Защищенный член базового класса
public int c; // Общедоступный член базового класса
public Base() // Общедоступный конструктор
{
a = 1; b = 2; c = 3;
}
// Недоступен внешнему коду, но доступен наследнику
// Сумма квадратов
protected int SumOfSquares()
{
return a * a + b * b + c * c;
}
}
// Производный класс
class Derived : Base
{
public void Show()
{
Console.WriteLine("Сумма квадратов: {0}",
SumOfSquares());// Доступен наследнику
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Доступ к членам в наследовании";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
Derived derived = new Derived();
derived.Show();
Base @base = new Base(); // Скрыли ключевое слово base
// @base.SumOfSquares(); // Недоступен в этом внешнем коде
Console.ReadLine();
}
}
}
Если конструктор базового класса имеет параметры, то их значения при наследовании нужно ему передать через конструктор производного класса по следующему синтаксису
конструктор_производного_класса(список_параметров) : base(список_аргументов){тело_конструктора_производного_класса}
При этом конструктор производного класса может содержать дополнительные параметры и для своих нужд, а также использовать транзитные для базового класса параметры. Например
using System;
namespace Test
{
// Базовый класс
class Base
{
// Все поля закрытые
private int a, b, c;
public Base() // Конструктор по умолчанию
{
a = 1; b = 2; c = 3;
}
public Base(int a, int b, int c) // Конструктор с параметрами
{
this.a = a;
this.b = b;
this.c = c;
}
// Недоступен внешнему коду, но доступен наследнику
protected int SumOfSquares()
{
return a * a + b * b + c * c;
}
}
// Производный класс
class Derived : Base
{
// Собственное поле производного класса
int d;
// Конструктор по умолчанию
public Derived()
{
d = 4;
}
// Конструктор с параметрами
public Derived(int a, int b, int c, int d)
: base(a, b, c) // Передача значений базовому классу
{
this.d = d;
}
public void Show()
{
// Часть работы выполняет код базового класса
Console.WriteLine("Сумма квадратов: {0}",
SumOfSquares() + d * d);
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Доступ к членам в наследовании";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
Derived derived = new Derived(5, 6, 7, 8);
derived.Show();
Console.WriteLine("Контрольная сумма квадратов:
{0}", 25 + 36 + 49 + 64);
Console.ReadLine();
}
}
}
Ключевое слово base со списком аргументов в объявлении конструктора производного класса фактически означает вызов конструктора базового класса с подходящей сигнатурой.
Если конструируется многоуровневая цепочка наследования классов, то последний потомок через свой конструктор должен обеспечить информацией всех своих предков. Например
using System;
namespace Test
{
class A
{
int a; // Закрытое поле
public A() // Конструктор по умолчанию
{
a = 1;
}
public A(int a) // Конструктор с параметрами
{
this.a = a;
}
protected int GetA()
{
return a;
}
}
class B : A
{
int b; // Закрытое поле
public B()
{
b = 2;
}
public B(int a, int b)
: base(a)
{
this.b = b;
}
protected int GetB()
{
return b;
}
}
class C : B
{
int c; // Закрытое поле
public C()
{
c = 3;
}
public C(int a, int b, int c)
: base(a, b)
{
this.c = c;
}
protected int GetC()
{
return c;
}
public void Show()
{
// Доступны все защищенные члены предков
int sumOfSquares =
GetA() * GetA() + GetB() * GetB() + GetC() * GetC();
Console.WriteLine("Сумма квадратов:
{0}", sumOfSquares);
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Доступ к членам в наследовании";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
C obC = new C(4, 5, 6);
obC.Show();
Console.WriteLine("Контрольная сумма
квадратов: {0}", 16 + 25 + 36);
Console.ReadLine();
}
}
}
Диаграмма наследственных связей для этого примера будет такой

То, что мы рассмотрели, можно проиллюстрировать рисунком, на котором клиент передает данные инициализации через конструктор производного класса конструкторам базовых классов
Находясь внутри производного класса мы можем видеть как-бы два слоя членов: члены производного класса на переднем плане и члены базового класса на заднем плане. Производный и базовый классы могут иметь члены с одинаковыми именам и. Это не является ошибкой, и даже компилятор не выдаст предупреждение, что будет использован член производного класса. Лишь текстовый редактор оболочки напомнит подчеркиванием, что имя члена в производном классе, находящееся в переднем слое, скрывает имя члена базового класса и желательно использовать ключевое слово new, но можно обойтись и без него. Употребление ключевого слова new в данном контексте свидетельствует лишь о том, что программист извещен и знает, что делает. Вот пример
using System;
namespace Test
{
class A
{
protected int x; // Член класса A будет виден в наследниках
public A(int a)
{
x = a;
}
public A()// Обязаны определить
{ }
protected int Get() // Член класса A будет виден в наследниках
{
return x;
}
}
class Empty : A
{
public Empty(int a)
: base(a)
{ }
public Empty() { } // Обязаны определить,
// если определен в базовом классе
}
class B : Empty
{
new int x; // Член класса B, одноименный с унаследованным
public B(int a, int b)
: base(a)
{
x = b; // Значение присваивается члену текущего класса
}
public B(int x)
{
base.x = 10;// Значение присваивается члену базового класса
this.x = x; // Значение присваивается члену текущего класса
}
new int Get() // Член класса B, одноименный с унаследованным
{
return x;
}
public void Show()
{
int sumOfSquares =
base.Get() * base.Get() // Используется унаследованный член
+ Get() * Get(); // Используется член текущего класса
Console.WriteLine("Сумма квадратов:
{0}", sumOfSquares);
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Сокрытие унаследованных имен";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
B ob = new B(1, 2);
ob.Show();
Console.WriteLine("Контрольная сумма
квадратов: {0}", 1 + 4);
Console.WriteLine();
ob = new B(5);// Использование ссылки для другого объекта
ob.Show();
Console.WriteLine("Контрольная сумма квадратов:
{0}", 125);
Console.ReadLine();
}
}
}
Ключевое слово base в данном контексте используется для доступа к члену базового класса, ближайшему в многоуровневой цепочке наследования, содержащему одноименный член. Промежуточный класс Empty здесь как раз и приведен для иллюстрации того, что одноименный член не обязательно может находиться в соседнем базовом классе.
В многоуровневой цепочке наследования производный класс стоит в самом низу иерархической лестницы. Если создается объект этого составного класса, то срабатывание конструкторов выполняется в сторону производного класса, начиная с конструктора базового класса, в порядке их выведения. Деструкторы срабатывают в обратном порядке и только при активизации сборщика мусора ( GC - Garbare Collector ). Вот пример
using System;
namespace Test
{
class A
{
public A()
{
Console.WriteLine("Сработал конструктор A");
}
~A()
{
Console.WriteLine("Сработал деструктор A");
}
}
class B : A
{
public B()
{
Console.WriteLine("Сработал конструктор B");
}
~B()
{
Console.WriteLine("Сработал деструктор B");
}
}
class C : B
{
public C()
{
Console.WriteLine("Сработал конструктор C");
}
~C()
{
Console.WriteLine("Сработал деструктор C");
}
}
class D : C
{
public D()
{
Console.WriteLine("Сработал конструктор D");
}
~D()
{
Console.WriteLine("Сработал деструктор D");
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Вызовы конструкторов и деструкторов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
D ob = new D(); // Создаем объект
ob = null; // Теряем объект, чтобы сработали деструкторы
Console.WriteLine();// Новая строка в выводе
GC.Collect();// Принудительно вызываем сборщик мусора
GC.WaitForPendingFinalizers(); // Приостанавливаем текущий поток
// до завершения уборки
Console.ReadLine();
}
}
}
Результат вывода будет таким

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

Конструкторы устанавливают
Все ссылки являются четырехбайтными адресными переменными. Главное их отличие между собой состоит в способности адресовать объекты только определенного типа. Поскольку язык C# поддерживает парадигму строгого
using System;
namespace Test
{
class A
{
}
class B
{
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Строгий контроль типов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
// Объявляем ссылки
A obA;
B obB;
// Создаем объекты одного типа,
// а пытаемся адресовать их ссылками другого типа
//obA = new B(); // Ошибка компиляции
//obB = new A(); // Ошибка компиляции
// Создаем объекты и адресуем их ссылками того же типа
obA = new A();// Все нормально! Типы соответствуют
obB = new B();// Все нормально! Типы соответствуют
// Пробуем явно приводить типы
//obA = (A)obB; // Ошибка компиляции
//obB = (B)(new A()); // Ошибка компиляции
Console.ReadLine();
}
}
}
Мы уже говорили, что строгий
Но есть некоторое отступление от строгого

Представим себе цепочку классов, связанных одиночным наследованием, расположенную сверху вниз так, что на вершине ее находится базовый класс. Тогда ссылка, объявленная любым классом этой цепи, может адресоваться как на объект этого класса, так и на любой объект, порожденный любым нижележащим классом. Но все это в рамках своих полномочий, то есть только на члены, определенные классом объявления ссылки. Чтобы тонкая ссылка увеличила свои полномочия, ее нужно явно привести к нижележащему типу, породившему объект.
Обратное утверждение неверно, то есть ссылка
Итак, ссылки базового типа могут адресовать объекты производного типа (тонкие ссылки могут адресовать толстые объекты), а ссылки
using System;
namespace Test
{
class Point
{
protected string name = "Точка: ";
protected int x, y;
public Point()
{
x = y = 0;
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public void Show()
{
Console.WriteLine(name + "x={0}; y={1}", x, y);
}
}
class Circle : Point
{
new protected string name = "Круг: ";
protected int radius;
public Circle()
{
radius = 1;
}
public Circle(int x, int y, int radius)
: base(x, y)
{
this.radius = radius;
}
public double Length()
{
return 2.0 * Math.PI * radius;
}
public double Area()
{
return Math.PI * radius * radius;
}
new public void Show()
{
Console.WriteLine(name + "x={0}; y={1};
r={2}", x, y, radius);
}
}
class Cylinder : Circle
{
new string name = "Цилиндр: ";
int height;// Высота цилиндра
public Cylinder()
{
height = 0;
}
public Cylinder(int x, int y, int radius, int height)
: base(x, y, radius)
{
this.height = height;
}
new public double Area()
{
double area = 2 * base.Area()
+ base.Length() * height;
return area;
}
public double Volume()// Объем
{
return base.Area() * height;
}
new public void Show()
{
Console.WriteLine(name + "x={0}; y={1};
r={2}; h={3}", x, y, radius, height);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их тонкой ссылкой
Point[] point = {
new Point(1, 2),
new Circle(3, 4, 5),
new Cylinder(6, 7, 8, 9)
};
// Распечатываем
string str =
@"Point[] point = {
new Point(1, 2),
new Circle(3, 4, 5),
new Cylinder(6, 7, 8, 9)
};";
Console.WriteLine("Вот такие мы создали
слоеные объекты:");
Console.WriteLine(str);
Console.WriteLine();
Console.WriteLine("Содержимое слоя Point");
for (int i = 0; i < point.Length; i++)
point[i].Show();
Console.WriteLine();
Console.WriteLine("Содержимое слоя Circle");
for (int i = 1; i < point.Length; i++)
((Circle)point[i]).Show();// Увеличиваем полномочия тонкой ссылки
Console.WriteLine();
Console.WriteLine("Содержимое слоя Cylinder");
for (int i = 2; i < point.Length; i++)
((Cylinder)point[i]).Show();// Увеличиваем полномочия тонкой ссылки
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Ссылки родственных типов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 20;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Результат выполнения будет таким

Увеличение полномочий тонких ссылок называется явным приведением типов. Вначале все объекты цепочки наследования адресовались тонкими ссылками, имеющими базовый тип. Это допустимо и при формировании массива базового типа компилятор осуществлял
Такие свойства преобразования типов очень часто применяются в функциях. Если в качестве параметра функции используется ссылка на объект базового типа, то в качестве аргумента может быть передан объект любого родственного
Чтобы проиллюстрировать использование ссылок базового типа в параметрах функции, заменим в предыдущем примере класс MyClass следующей модификацией (весь остальной код предыдущего примера остается неизменным)
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
Point point = new Point(1, 2);
Circle circle = new Circle(3, 4, 5);
Cylinder cylinder = new Cylinder(6, 7, 8, 9);
// Последовательно препарируем объекты
Console.WriteLine("Анализируется
объект Point(1, 2)");
PrepareObject(point);
Console.WriteLine("\n\nАнализируется
объект Circle(3, 4, 5)");
PrepareObject(circle);
Console.WriteLine("\n\nАнализируется
объект Cylinder(6, 7, 8, 9)");
PrepareObject(cylinder);
}
void PrepareObject(Point ob)// Функция ожидает базовый тип, поэтому
// ей можно передавать объекты любого производного типа
{
Console.WriteLine("Слой типа Point");
ob.Show(); // Всегда есть в переданном
// Безопасно проверяем, чтобы не
//адресоваться к несуществующему слою
// Проверяем существование слоя Circle одним способом
if (ob is Circle)// Выражение равно true, если слой есть
{
Console.WriteLine("\nСлой типа Circle");
Circle circle = (Circle)ob;// Повышаем полномочия ссылки ob
circle.Show();
Console.WriteLine("Длина окружности: {0}",
circle.Length());
Console.WriteLine("Площадь круга: {0}",
circle.Area());
}
// Проверяем существование слоя Cylinder другим способом
Cylinder cylinder = ob as Cylinder;
// Ссылка нулевая, если слоя нет
if (cylinder != null)
{
Console.WriteLine("\nСлой типа Cylinder");
((Cylinder)ob).Show();
// Повышаем полномочия ссылки ob налету
Console.WriteLine("Площадь всей поверхности:
{0}", cylinder.Area());
Console.WriteLine("Объем: {0}",
cylinder.Volume());
}
}
}
Результат выполнения программы с модифицированным классом MyClass следующий

Чтобы не вызвать исключение времени выполнения при попытке адресации к несуществующему слою переданного объекта, мы внутри функции вначале проверяем наличие этого слоя в адресуемом объекте. Для этого используются равноценные конструкции языка с ключевыми словами as и is.
В пределе тип ссылки в аргументе функции можно объявить как object (или Object ), поскольку класс Object является базовым для всех типов без исключения. Но в этом случае внутри функции ссылку на объект Point также нужно привести к типу Point и начало функции выглядело бы так
void PrepareObject(object ob)// Функция ожидает самый базовый тип, поэтому
// ей можно передавать объекты любого типа
{
Console.WriteLine("Слой типа Point");
((Point)ob).Show(); // Теперь тоже нужно приводить
...........Остальное без изменений...........
}
Таким образом, когда функция ожидает поступления одного из родственных объектов, связанных цепочкой наследования, но не знает, какого именно родственника ей передаст вызывающий код, правильнее всего указать в качестве аргумента тип самого базового родственника, способного представлять любого наследника. А затем уже внутри функции безопасно проверять, к какому типу принадлежит фактически поступивший родственник.
В этом и заключается динамический полиморфизм базовых ссылок, когда одна и та же ссылка может адресовать множество родственных объектов. И все это на этапе выполнения. Нужно только привести заполненную адресом конкретного объекта базовую ссылку к
Статус ссылки на родственные объекты можно не только повышать, но и снижать для адресации к тонким слоям объекта. Вот новая модификация класса MyClass нашего примера
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем многослойный толстый объект
Cylinder cylinder = new Cylinder(6, 7, 8, 9);
// Передаем объект функции для послойной препарации
PrepareObject(cylinder);
// Тонкие объекты передавать нельзя,
// когда функция ожидает толстый объект,
// поскольку функция может потребовать
// от них несуществующих возможностей
// PrepareObject((Circle)cylinder);// Ошибка компиляции!!!
// PrepareObject((Point)cylinder); // Ошибка компиляции!!!
}
void PrepareObject(Cylinder ob)// Функция ожидает толстый тип
{
// Проверяем, что это именно толстый тип
// if (ob is Cylinder)
// {
// ...
// }
// Проверять необязательно, поскольку тоньше объекта,
// чем ожидается, компилятор не пропустит, а если будет
// передан более толстый объект, мы его приведем к исходному
Cylinder cylinder = (Cylinder)ob;
// На случай, если передадут толще
// Заготавливаем адреса разных слоев составного объекта,
// снижая тем самым статус ссылки на исходный объект
Point point = (Point)ob;
Circle circle = (Circle)ob;
// Адресуемся к членам слоя Point
Console.WriteLine("Слой типа Point");
point.Show();
// Адресуемся к членам слоя Circle
Console.WriteLine("\nСлой типа Circle");
circle.Show();
Console.WriteLine("Длина окружности: {0}",
circle.Length());
Console.WriteLine("Площадь круга: {0}",
circle.Area());
// Адресуемся к членам слоя Cylinder
Console.WriteLine("\nСлой типа Cylinder");
cylinder.Show();
Console.WriteLine("Площадь всей поверхности: {0}",
cylinder.Area());
Console.WriteLine("Объем: {0}",
cylinder.Volume());
}
}
Очень важно, что компилятор принципиально не пропускает в функцию объект, который имеет тип тоньше ожидаемого, поскольку для корректной работы внутри функции переданной информации может быть недостаточно. В то же время, внутри функции мы можем адресоваться к более тонким слоям объекта, надеясь, что компилятор следит за тем, чтобы в функцию были переданы все части ожидаемого объекта. А ожидается именно заявленный в параметрах толстый объект, или еще более толстый его наследник.
Результат работы программы будет такой
В поведении базовых ссылок в рассмотренных примерах усматривается один существенный недостаток. Когда базовая ссылка адресует многослойный объект, то именно тип, к которому она принадлежит, определяет ее способность адресоваться к членам соответствующего слоя этого объекта. И для того, чтобы получить доступ к членам расширенных слоев объекта, мы вынуждены приводить ссылку к типу соответствующего слоя, вручную изменяя ее полномочия (статус). Здесь именно тип ссылки определяет ее полномочия в адресации членов многослойного объекта.
А как бы сделать так, чтобы полномочия базовой ссылки автоматически устанавливались типом переданного родственного объекта. Есть такой механизм в объектно-ориентированных языках, который называется виртуальными функциями в наследовании. Его мы сейчас и рассмотрим применительно к C#.
Вернемся к нашему примеру. Посмотрим на члены в цепочке наследования классов Cylinder:Circle:Point, например на функцию Show(), которая есть в каждом слое. Для того, чтобы вызвать эту функцию из нужного слоя
Есть более простой способ выполнить то же самое, если в базовом классе объявить функцию Show() виртуальной с помощью ключевого слова virtual, а в каждом классе-наследнике подтвердить новую версию этой функции с помощью ключевого слова override (переопределенная). После такого объявления компилятором будет создана таблица виртуальных функций. Она реализует механизм виртуальной адресации и позволит нам с помощью ссылки базового типа, которой будет присвоен адрес любого из объектов-родственников, вызывать переопределенные в этом объекте виртуальные функции. При этом явного (вручную) приведения ссылки базового типа к типу фактически адресуемого объекта не потребуется, среда исполнения сама извлечет из нужного слоя объекта подходящий член, определив его по типу самого объекта.
Одно важное условие нужно соблюдать при объявлении и переопределении виртуальных функций:
Чаще всего, когда разрабатывается новый класс как расширение наследуемого (или цепочки наследуемых) класса, программист внутри этого расширения задействует весь потенциал наследуемых классов, вводя свои настройки и сервисы для вызывающего кода. При создании экземпляров этого класса в вызывающем коде программист охотнее использует только добавленное расширение, то есть добавленный слой
Для всей иерархии виртуальных функций автоматически создается и поддерживается таблица виртуальных функций, которая при создании объекта производного класса является присоединенной к нему и заполняется адресами всех переопределений виртуальных функций.
Если в иерархической цепочке наследования в каком-то одном или нескольких производных классах переопределение виртуальной функции пропущено, то для клиента производного класса будет вызвана та ее версия, которая является ближайшей по иерархическим меркам в направлении базового класса.
Объявим в нашем примере функцию Show() виртуальной в базовом классе, а все ее версии в производных классах переопределим. Упрощенный код примера будет таким
using System;
namespace Test
{
class Point
{
string name = "Слой Point";
public void Show0()
{
Console.WriteLine(name);
}
public virtual void Show()
{
Console.WriteLine(name);
}
}
class Circle : Point
{
string name = "Слой Circle";
new public void Show0()
{
Console.WriteLine(name);
}
public override void Show()
{
Console.WriteLine(name);
}
}
class Cylinder : Circle
{
string name = "Слой Cylinder";
new public void Show0()
{
Console.WriteLine(name);
}
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их базовой ссылкой
Point[] point = {
new Point(),
new Circle(),
new Cylinder()
};
// Распечатываем
Console.WriteLine("Адресация к невиртуальной функции");
for (int i = 0; i < point.Length; i++)
{
point[i].Show0();
}
Console.WriteLine("\nАдресация к виртуальной функции");
for (int i = 0; i < point.Length; i++)
point[i].Show();
Console.WriteLine("\nТо же самое, только вручную");
point[0].Show0();
((Circle)point[1]).Show0();
((Cylinder)point[2]).Show0();
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Механизм виртуальных функций";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Обратите внимание, что в каждом производном классе есть переменная с одинаковым именем name. Но ее помечать как новую не нужно, поскольку она невидима в производном классе. При адресации ссылкой базового типа к не виртуальным функциям родственных объектов всегда адресуется только базовый слой. При подобной адресации к виртуальным функциям адресуется член расширения самого объекта или ближайший в глубину наследования. Поиск адресуемого члена ищется послойно, начиная с адресуемого слоя в сторону базового.
Результат, выдаваемый программой, будет таким
Если в крайнем слое адресуемого объекта вызываемая функция непереопределена или отсутствует в нем, ищется ее переопределение в ближайшем слое в сторону слоя базового класса.
Пример
using System;
namespace Test
{
class Point
{
string name = "Слой Point";
public virtual void Show()
{
Console.WriteLine(name);
}
}
class Circle : Point
{
string name = "Слой Circle";
public override void Show()
{
Console.WriteLine(name);
}
}
class Cylinder : Circle
{
string name = "Слой Cylinder";
new public void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их базовой ссылкой
Point[] point = {
new Point(),
new Circle(),
new Cylinder()
};
// Распечатываем
Console.WriteLine("Адресация к ближайшей
виртуальной функции");
for (int i = 0; i < point.Length; i++)
point[i].Show();
Console.WriteLine();
Console.WriteLine("Адресуемся к слою
Cylinder напрямую");
((Cylinder)point[2]).Show();// Адресуемся напрямую
// Извращенный синтаксис использования массива
Console.WriteLine();
Console.WriteLine("Нерациональный
синтаксис использования массива");
for (int i = 0; i < 3; i++)
{
Point point1 = new Point[]{
new Point(),
new Circle(),
new Cylinder()
}[i];
point1.Show();
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Механизм виртуальных функций";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 15;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Результат выполнения будет такой

Мы видим, что при попытке адресации виртуальной функции в объекте Cylinder, которой в расширяющем слое этого объекта нет, вызвался вариант из слоя Circle. Тонкая ссылка при адресации к толстому объекту игнорирует невиртуальные функции в толстых слоях, поскольку они не включены в таблицу виртуальных функций. Если бы и в слое Circle объекта Cylinder этой функции не было, то вызвался бы вариант слоя Point. Если бы вообще этой функции не было ни в одном слое, то была бы сгенерирована ошибка компиляции как попытка вызова несуществующего члена.
Вот еще один отвлеченный пример адресации к ближайшей виртуальной функции
using System;
namespace Test
{
class A
{
string name = "Слой A";
public virtual void Show()
{
Console.WriteLine(name);
}
}
class B : A
{
string name = "Слой B";
}
class C : B
{
string name = "Слой C";
public override void Show()
{
Console.WriteLine(name);
}
}
class D : C
{
string name = "Слой D";
}
class E : D
{
string name = "Слой E";
public override void Show()
{
Console.WriteLine(name);
}
}
class F : E
{
string name = "Слой F";
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их базовой ссылкой
A[] a = {
new A(),
new B(),
new C(),
new D(),
new E(),
new F()
};
// Распечатываем
Console.WriteLine("Адресация к ближайшей
виртуальной функции");
Console.WriteLine("В классах B и D виртуальная функция отсутствует");
for (int i = 0; i < a.Length; i++)
{
Console.Write("Объект " +
a[i].GetType().Name + "\t");
a[i].Show();
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Поиск виртуальной функции в
глубину наследования";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
и результат выполнения программы

Мы видим, что при адресации к слою, где функция не переопределена (слой B и D ), поиск по порядку ведется в ближайших от адресуемого слоях в направлении базового слоя.
using System;
namespace Test
{
class A
{
string name = "Слой A";
public virtual string Name
{
get { return name; }
}
public virtual void Show()
{
Console.WriteLine(name);
}
}
class B : A
{
string name = "Слой B";
public override string Name
{
get { return name; }
}
public override void Show()
{
Console.WriteLine(name);
}
}
class C : B
{
string name = "Слой C";
public override string Name
{
get { return name; }
}
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
A[] a = {
new A(),
new B(),
new C()
};
// Распечатываем
Console.WriteLine("Виртуальные методы:");
for (int i = 0; i < a.Length; i++)
{
Console.Write("Объект " +
a[i].GetType().Name + "\t");
a[i].Show();
}
// Распечатываем
Console.WriteLine("\nВиртуальные свойства:");
for (int i = 0; i < a.Length; i++)
{
Console.WriteLine("Объект " +
a[i].GetType().Name + "\t" +
"{0}", a[i].Name);
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Виртуальные методы и свойства";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Применение наследования позволяет поэтапно наращивать класс, вводя все новые расширения наследуемого кода, т.е. постепенно продвигаясь от абстрактного к конкретному. Когда программист разрабатывает очередное расширение (описание производного класса), он может планировать такие задачи, для реализации которых в текущем расширении еще нет достаточных условий.
Например, разрабатывая расширение Circle, можно предположить, что в дальнейшем на базе этого кода будет построено новое расширение типа Cylinder. А там может потребоваться функция вычисления объема Volume(). Чтобы не забыть об этом факте, можно вставить в текущее расширение только объявление этой будущей виртуальной функции, без ее реализации, надеясь реализовать ее в следующих расширениях, например в Cylinder. Такие виртуальные функции ( методы с отложенной реализацией ) называются чисто виртуальными или просто абстрактными.
Как и в ряде других случаев, при объявлении абстрактных классов введена некоторая синтаксическая избыточность ( контролирующее обременение ) для усиления контроля компилятора за действиями программиста: если в классе объявлен хоть один
Вот модификация предыдущего примера, иллюстрирующая сказанное
using System;
namespace Test
{
abstract class A
{
string name = "Слой A";
public abstract void Show();// Отложили реализацию
}
// Реализации наследуемого абстрактного метода еще нет,
// значит слой тоже нужно обозначить абстрактным
abstract class B : A
{
string name = "Слой B";
}
// Есть первая реализация, абстрактные слои закончились
class C : B
{
string name = "Слой C";
public override void Show()
{
Console.WriteLine(name);
}
}
class D : C
{
string name = "Слой D";
}
class E : D
{
string name = "Слой E";
public override void Show()
{
Console.WriteLine(name);
}
}
class F : E
{
string name = "Слой F";
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех неабстрактных
// типов иерархической цепочки наследования,
// применяя ссылку абстрактного базового типа
A[] a = {
// new A(), // Ошибка компиляции!!!
// new B(), // Ошибка компиляции!!!
new C(),
new D(),
new E(),
new F()
};
// Распечатываем
Console.WriteLine("Реализация абстрактных методов");
for (int i = 0; i < a.Length; i++)
{
Console.Write("Объект " +
a[i].GetType().Name + "\t");
a[i].Show();
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Абстрактные методы и классы";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Результат будет таким

Обратите внимание, что создавать экземпляры абстрактных классов компилятор не дает, но в тоже время объявлять и использовать ссылки абстрактных типов можно (что мы и сделали для базового абстрактного типа A ). Он же следит и затем, чтобы класс, наследующий
Способ 1. Класс с защищенным конструктором не может иметь экземпляров, но может участвовать в наследовании
using System;
namespace Test
{
class A
{
string name = "Класс A";
protected A()
{
}
public virtual void Show()
{
Console.WriteLine(name);
}
}
class B : A
{
string name = "Класс B";
public override void Show()
{
Console.WriteLine(name);
}
}
class C : B
{
string name = "Класс C";
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем и распечатываем объекты
//A a = new A(); a.Show(); // Ошибка компиляции
B b = new B(); b.Show();
C c = new C(); c.Show();
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Защищенный конструктор класса";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Способ 2. Аналогичным образом, класс, объявленный как статический, тоже не может иметь экземпляров. Но статический класс нельзя использовать и в наследовании, поскольку механизм наследования работает только для экземплярных классов.
Способ 3. Класс нужно обязательно объявлять абстрактным, если он содержит или наследует, а сам не реализует, хотя-бы один C#, потому что класс сырой и не готов к производству
объектов. Но любой класс, даже не содержащий абстрактных или виртуальных методов, мы можем объявить абстрактным, если не хотим, чтобы кто-то или мы сами использовали его для создания экземпляров. Обычно это промежуточные вспомогательные классы-заготовки, планируемые для дальнейшего развития с помощью механизма наследования.
Вот простой пример, когда объявление любого класса абстрактным запрещает его реализацию в объекты
using System;
namespace Test
{
// Хоть нет абстрактных методов
// и класс готов к производству объектов,
// захотели объявить абстрактным
abstract class A
{
string name = "Класс A";
public virtual void Show()
{
Console.WriteLine(name);
}
}
// Может производить объекты,
// хоть и наследует абстрактный класс
class B : A
{
string name = "Класс B";
public override void Show()
{
Console.WriteLine(name);
}
}
// Может производить объекты
class C
{
string name = "Класс C";
public void Show()
{
Console.WriteLine(name);
}
}
// Нет виртуальных функций и ничего не наследует,
// все равно можно запретить производить объекты
abstract class D
{
string name = "Класс D";
public void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем и распечатываем объекты
// A a = new A(); a.Show(); // Ошибка компиляции!!!
B b = new B(); b.Show();
A b1 = new B(); b1.Show();// Тонкая ссылка - толстый объект
C c = new C(); c.Show();
// D d = new D(); d.Show(); // Ошибка компиляции!!!
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Производить объекты
абстрактных классов запрещено";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Вот результат после закомментирования выделенных строк
Иногда разработанный класс может оказаться настолько совершенным и самодостаточным, что не предполагает никаких дальнейших усовершенствований путем новых расширений. В этом случае его можно объявить с модификатором sealed и компилятор будет пресекать все попытки использовать его в качестве базового для дальнейших расширений. При этом будет безразлично, включен ли такой класс в текущий проект в исходном виде, или существует в откомпилированном виде в подключенной к проекту библиотечной сборке.
Вот пример
using System;
namespace Test
{
// Не может иметь наследников
sealed class A
{
}
/* Чтобы быстро отменить комментарий, поставьте спереди слеш /
// Ошибка компиляции здесь!!! Запрещено наследоваться
class B : A
{
}
//*/
// Может наследоваться
class C
{
}
// Может наследоваться
class D : C
{
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Просто создаем объекты в управляемой куче
A a = new A(); // Создавать объекты можно,
// а наследоваться нельзя
//B b = new B();// Нет такого класса, он закомментирован
C c = new C();
D d = new D();
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Запрет классу иметь наследников";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Существует два способа повторного использования ранее разработанного и откомпилированного кода (простое копирование исходного кода в новый - не в счет):
Приведем пример кода, иллюстрирующего композицию. Пусть в нашем распоряжении имеется ранее разработанный (нами или кем-то другим) класс, моделирующий точку. Назовем его Point. Этот класс может существовать и в откомпилированном виде (или в исходном коде) и храниться где-то в подключаемой к проекту библиотеке. Пусть исходный код этого класса будет таким
using System;
namespace Test
{
class Point
{
int x, y;
public int X
{
get { return x; }
set { x = value; }
}
public int Y
{
get { return y; }
set { y = value; }
}
public Point()
{
x = y = 0;
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public void Show()
{
Console.WriteLine("x={0}; y={1}", x, y);
}
}
}
Используем класс Point для моделирования прямоугольника, представленного классом Rectangle. Состояние прямоугольника характеризуется двумя точками (обычно это координаты левого верхнего и правого нижнего углов), из которых можно вычислить другие характеристики, например, ширину, высоту и площадь.
Всегда в классе стремятся хранить только те данные, которые минимально необходимы для однозначного описания состояния объекта, а все, что можно легко вычислить, упаковывают в свойства или методы. Вот код модели прямоугольника, который иллюстрирует композицию объектов
namespace Test
{
class Rectangle
{
// Объявляем закрытые ссылки - поля
Point point1; // Левый верхний угол
Point point2; // Правый нижний угол
// Конструкторы на все случаи жизни
public Rectangle()
{
point1 = new Point();
point2 = new Point();
}
public Rectangle(Point point1, Point point2)
{
// Объекты созданы в клиенте
this.point1 = point1;
this.point2 = point2;
}
public Rectangle(int x1, int y1, int width, int height)
{
point1 = new Point(x1, y1);
point2 = new Point(x1 + width, y1 + height);
}
public Rectangle(int x1, int y1, Int64 x2, int y2)
{
point1 = new Point(x1, y1);
point2 = new Point((int)x2, y2);
}
// Свойства - сервисы
// Ширина прямоугольника
public int Width
{
get { return Math.Abs(point2.X - point1.X); }
}
// Высота прямоугольника
public int Height
{
get { return Math.Abs(point2.Y - point1.Y); }
}
// Методы - сервисы
// Площадь прямоугольника
public int Area()
{
return Width * Height;
}
// Точка внутри прямоугольника
public bool PointIsRect(Point point)
{
bool pointIsHorizontal = point.X >= point1.X
point.X <= point2.X;
bool pointIsVertical = point.Y >= point1.Y
point.Y <= point2.Y;
if (pointIsHorizontal pointIsVertical)
return true;
else
return false;
}
// Получение координат прямоугольника
public void GetCoord(out int x1, out int y1,
out int x2, out int y2)
{
x1 = point1.X;
y1 = point1.Y;
x2 = point2.X;
y2 = point2.Y;
}
}
}
Использование класса Rectangle
Результат выполнения программы будет таким

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

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

Говорят, что базовый класс наследуется производным классом, а производный класс наследует базовый класс.
Одно из отличий производного класса, сцепленного механизмом наследования с базовым классом, от составного класса, содержащего в себе экземпляры других классов, состоит в том, что производный класс наследует public -сервисы своего базового класса в готовом виде. Составному же классу нужно самостоятельно позаботиться о создании сервисов, если это необходимо, для включенных в него экземпляров.
Классы, стоящие на верхнем уровне иерархии наследования считаются более абстрактыми по сравнению с нижестоящими классами. Возможно, что более абстрактные классы не имеют конкретной реализации, а если и имеют, то таких реализаций значительно меньше, чем реализаций более конкретных классов. Говорят, что производный класс специфицирует ( расширяет конкретизацию) базовый класс дополнительными свойствами и поведением.
Базовый класс может наследоваться любым количеством других классов. Базовый класс не обязан знать, какой класс будет его наследовать. На данном этапе он считается самодостаточным и способным выступить в качестве родителя любого производного класса. Но если базовый класс в свою очередь произошел от другого базового класса, то он знает своего родителя и всех своих более дальних предков, так как содержит в себе все их свойства и функциональность.
Еще раз вспомним, что класс на этапе проектирования, это только описание возможностей будущей программной единицы. Сама программная единица создается во время выполнения программы как экземпляр этого описания, т.е. становится реальным объектом: объектом-типом, а потом, возможно, объектом-экземпляром. При наследовании на этапе проектирования мы создаем многоступенчатое описание, по которому обязательно будет создан слоеный объект-экземпляр.
Различают простое (одиночное) и множественное наследование. Связь в виде простого наследования предполагает, что производный класс имеет только одного родителя (происходит только от одного базового класса). Множественное наследование подразумевает, что

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

Наследование обеспечивает возможность постепенного наращивания кода путем объявления класса, встраивающего в себя другой ранее разработанный класс. В языке C# могут наследоваться классы и интерфейсы. Классы C# допускают только одиночное наследование других классов, но множественное наследование интерфейсов. Интерфейсы могут наследовать другие интерфейсы, причем множественно. С наследованием связано множество
специфических приемов работы с кодом, поддерживаемых языком C#.
Пусть имеется простой класс с именем Base. Выведем из него другой класс с именем Derived, в который будет встроен код класса Base. Код программы будет таким
using System;
namespace Test
{
class Base
{
public string fieldBase = "Поле в Base";
public string PropertiesBase
{
get { return "Свойство в Base"; }
set { fieldBase = value; }
}
public void MethodBase()
{
Console.WriteLine("Метод в Base");
}
}
class Derived : Base
{
public string fieldDerived = "Поле в Derived";
public string PropertiesDerived
{
get { return "Свойство в Derived"; }
set { fieldDerived = value; }
}
public void MethodDerived()
{
Console.WriteLine("Метод в Derived");
}
}
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Наследование классов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
// Создаем объект класса Derived
Derived derived = new Derived();
// Используем члены унаследованного класса Base
Console.WriteLine("Унаследованная часть
класса Derived");
Console.WriteLine(derived.fieldBase);
Console.WriteLine(derived.PropertiesBase);
derived.MethodBase();
Console.WriteLine();
// Используем члены расширения
Console.WriteLine("Собственное
расширение класса Derived");
Console.WriteLine(derived.fieldDerived);
Console.WriteLine(derived.PropertiesDerived);
derived.MethodDerived();
Console.ReadLine();
}
}
}
Здесь для удобства все классы помещены в один файл с именем Program.cs. Результат выполнения выглядит так

Класс, который наследуется, называется базовым, а класс, который является наследником, называется производным или расширением базового класса. Говорят, что производный класс выведен из базового, а базовый класс наследует производному. Также говорят, что производный класс является потомком базового класса, а базовый класс его предком. Любой класс, участвующий в многоуровневой цепочке наследования, не может быть потомком самого себя, т.е не может участвовать более чем один раз.
Базовый и производный классы составляют цепочку наследования и создают иерархию классов. Оболочка Visual Studio 2005 имеет встроенные средства графического представления иерархии классов, которые вызываются в панели Solution Explorer командой View Class Diagram контекстного для файла меню или одноименной кнопкой этой панели.

Графическое представление цепочки наследования для нашего примера выглядит так

Наследственную зависимость принято указывать стрелками по направлению от производного класса к базовому и располагать классы так, чтобы на вершине многоуровневой иерархической лестницы стоял базовый класс.
Код производного класса по отношению к присоединенному базовому классу является внешним, хотя и родственным. Поэтому внутренние private -члены базового класса недоступны в коде производного, общедоступные public -члены доступны также, как и для любого неродственного кода, а защищенные protected -члены базового класса недоступны коду неродственного класса, но доступны в коде производного класса. Например
using System;
namespace Test
{
// Базовый класс
class Base
{
int a; // Внутренний член базового класса
protected int b; // Защищенный член базового класса
public int c; // Общедоступный член базового класса
public Base() // Общедоступный конструктор
{
a = 1; b = 2; c = 3;
}
// Недоступен внешнему коду, но доступен наследнику
// Сумма квадратов
protected int SumOfSquares()
{
return a * a + b * b + c * c;
}
}
// Производный класс
class Derived : Base
{
public void Show()
{
Console.WriteLine("Сумма квадратов: {0}",
SumOfSquares());// Доступен наследнику
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Доступ к членам в наследовании";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
Derived derived = new Derived();
derived.Show();
Base @base = new Base(); // Скрыли ключевое слово base
// @base.SumOfSquares(); // Недоступен в этом внешнем коде
Console.ReadLine();
}
}
}
Если конструктор базового класса имеет параметры, то их значения при наследовании нужно ему передать через конструктор производного класса по следующему синтаксису
конструктор_производного_класса(список_параметров) : base(список_аргументов){тело_конструктора_производного_класса}
При этом конструктор производного класса может содержать дополнительные параметры и для своих нужд, а также использовать транзитные для базового класса параметры. Например
using System;
namespace Test
{
// Базовый класс
class Base
{
// Все поля закрытые
private int a, b, c;
public Base() // Конструктор по умолчанию
{
a = 1; b = 2; c = 3;
}
public Base(int a, int b, int c) // Конструктор с параметрами
{
this.a = a;
this.b = b;
this.c = c;
}
// Недоступен внешнему коду, но доступен наследнику
protected int SumOfSquares()
{
return a * a + b * b + c * c;
}
}
// Производный класс
class Derived : Base
{
// Собственное поле производного класса
int d;
// Конструктор по умолчанию
public Derived()
{
d = 4;
}
// Конструктор с параметрами
public Derived(int a, int b, int c, int d)
: base(a, b, c) // Передача значений базовому классу
{
this.d = d;
}
public void Show()
{
// Часть работы выполняет код базового класса
Console.WriteLine("Сумма квадратов: {0}",
SumOfSquares() + d * d);
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Доступ к членам в наследовании";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
Derived derived = new Derived(5, 6, 7, 8);
derived.Show();
Console.WriteLine("Контрольная сумма квадратов:
{0}", 25 + 36 + 49 + 64);
Console.ReadLine();
}
}
}
Ключевое слово base со списком аргументов в объявлении конструктора производного класса фактически означает вызов конструктора базового класса с подходящей сигнатурой.
Если конструируется многоуровневая цепочка наследования классов, то последний потомок через свой конструктор должен обеспечить информацией всех своих предков. Например
using System;
namespace Test
{
class A
{
int a; // Закрытое поле
public A() // Конструктор по умолчанию
{
a = 1;
}
public A(int a) // Конструктор с параметрами
{
this.a = a;
}
protected int GetA()
{
return a;
}
}
class B : A
{
int b; // Закрытое поле
public B()
{
b = 2;
}
public B(int a, int b)
: base(a)
{
this.b = b;
}
protected int GetB()
{
return b;
}
}
class C : B
{
int c; // Закрытое поле
public C()
{
c = 3;
}
public C(int a, int b, int c)
: base(a, b)
{
this.c = c;
}
protected int GetC()
{
return c;
}
public void Show()
{
// Доступны все защищенные члены предков
int sumOfSquares =
GetA() * GetA() + GetB() * GetB() + GetC() * GetC();
Console.WriteLine("Сумма квадратов:
{0}", sumOfSquares);
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Доступ к членам в наследовании";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
C obC = new C(4, 5, 6);
obC.Show();
Console.WriteLine("Контрольная сумма
квадратов: {0}", 16 + 25 + 36);
Console.ReadLine();
}
}
}
Диаграмма наследственных связей для этого примера будет такой

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

Находясь внутри производного класса мы можем видеть как-бы два слоя членов: члены производного класса на переднем плане и члены базового класса на заднем плане. Производный и базовый классы могут иметь члены с одинаковыми именам и. Это не является ошибкой, и даже компилятор не выдаст предупреждение, что будет использован член производного класса. Лишь текстовый редактор оболочки напомнит подчеркиванием, что имя члена в производном классе, находящееся в переднем слое, скрывает имя члена базового класса и желательно использовать ключевое слово new, но можно обойтись и без него. Употребление ключевого слова new в данном контексте свидетельствует лишь о том, что программист извещен и знает, что делает. Вот пример
using System;
namespace Test
{
class A
{
protected int x; // Член класса A будет виден в наследниках
public A(int a)
{
x = a;
}
public A()// Обязаны определить
{ }
protected int Get() // Член класса A будет виден в наследниках
{
return x;
}
}
class Empty : A
{
public Empty(int a)
: base(a)
{ }
public Empty() { } // Обязаны определить,
// если определен в базовом классе
}
class B : Empty
{
new int x; // Член класса B, одноименный с унаследованным
public B(int a, int b)
: base(a)
{
x = b; // Значение присваивается члену текущего класса
}
public B(int x)
{
base.x = 10;// Значение присваивается члену базового класса
this.x = x; // Значение присваивается члену текущего класса
}
new int Get() // Член класса B, одноименный с унаследованным
{
return x;
}
public void Show()
{
int sumOfSquares =
base.Get() * base.Get() // Используется унаследованный член
+ Get() * Get(); // Используется член текущего класса
Console.WriteLine("Сумма квадратов:
{0}", sumOfSquares);
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Сокрытие унаследованных имен";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
B ob = new B(1, 2);
ob.Show();
Console.WriteLine("Контрольная сумма
квадратов: {0}", 1 + 4);
Console.WriteLine();
ob = new B(5);// Использование ссылки для другого объекта
ob.Show();
Console.WriteLine("Контрольная сумма квадратов:
{0}", 125);
Console.ReadLine();
}
}
}
Ключевое слово base в данном контексте используется для доступа к члену базового класса, ближайшему в многоуровневой цепочке наследования, содержащему одноименный член. Промежуточный класс Empty здесь как раз и приведен для иллюстрации того, что одноименный член не обязательно может находиться в соседнем базовом классе.
В многоуровневой цепочке наследования производный класс стоит в самом низу иерархической лестницы. Если создается объект этого составного класса, то срабатывание конструкторов выполняется в сторону производного класса, начиная с конструктора базового класса, в порядке их выведения. Деструкторы срабатывают в обратном порядке и только при активизации сборщика мусора ( GC - Garbare Collector ). Вот пример
using System;
namespace Test
{
class A
{
public A()
{
Console.WriteLine("Сработал конструктор A");
}
~A()
{
Console.WriteLine("Сработал деструктор A");
}
}
class B : A
{
public B()
{
Console.WriteLine("Сработал конструктор B");
}
~B()
{
Console.WriteLine("Сработал деструктор B");
}
}
class C : B
{
public C()
{
Console.WriteLine("Сработал конструктор C");
}
~C()
{
Console.WriteLine("Сработал деструктор C");
}
}
class D : C
{
public D()
{
Console.WriteLine("Сработал конструктор D");
}
~D()
{
Console.WriteLine("Сработал деструктор D");
}
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Вызовы конструкторов и деструкторов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
D ob = new D(); // Создаем объект
ob = null; // Теряем объект, чтобы сработали деструкторы
Console.WriteLine();// Новая строка в выводе
GC.Collect();// Принудительно вызываем сборщик мусора
GC.WaitForPendingFinalizers(); // Приостанавливаем текущий поток
// до завершения уборки
Console.ReadLine();
}
}
}
Результат вывода будет таким

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

Конструкторы устанавливают
Все ссылки являются четырехбайтными адресными переменными. Главное их отличие между собой состоит в способности адресовать объекты только определенного типа. Поскольку язык C# поддерживает парадигму строгого
using System;
namespace Test
{
class A
{
}
class B
{
}
// Вызывающий код
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Строгий контроль типов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
// Объявляем ссылки
A obA;
B obB;
// Создаем объекты одного типа,
// а пытаемся адресовать их ссылками другого типа
//obA = new B(); // Ошибка компиляции
//obB = new A(); // Ошибка компиляции
// Создаем объекты и адресуем их ссылками того же типа
obA = new A();// Все нормально! Типы соответствуют
obB = new B();// Все нормально! Типы соответствуют
// Пробуем явно приводить типы
//obA = (A)obB; // Ошибка компиляции
//obB = (B)(new A()); // Ошибка компиляции
Console.ReadLine();
}
}
}
Мы уже говорили, что строгий
Но есть некоторое отступление от строгого

Представим себе цепочку классов, связанных одиночным наследованием, расположенную сверху вниз так, что на вершине ее находится базовый класс. Тогда ссылка, объявленная любым классом этой цепи, может адресоваться как на объект этого класса, так и на любой объект, порожденный любым нижележащим классом. Но все это в рамках своих полномочий, то есть только на члены, определенные классом объявления ссылки. Чтобы тонкая ссылка увеличила свои полномочия, ее нужно явно привести к нижележащему типу, породившему объект.
Обратное утверждение неверно, то есть ссылка
Итак, ссылки базового типа могут адресовать объекты производного типа (тонкие ссылки могут адресовать толстые объекты), а ссылки
using System;
namespace Test
{
class Point
{
protected string name = "Точка: ";
protected int x, y;
public Point()
{
x = y = 0;
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public void Show()
{
Console.WriteLine(name + "x={0}; y={1}", x, y);
}
}
class Circle : Point
{
new protected string name = "Круг: ";
protected int radius;
public Circle()
{
radius = 1;
}
public Circle(int x, int y, int radius)
: base(x, y)
{
this.radius = radius;
}
public double Length()
{
return 2.0 * Math.PI * radius;
}
public double Area()
{
return Math.PI * radius * radius;
}
new public void Show()
{
Console.WriteLine(name + "x={0}; y={1};
r={2}", x, y, radius);
}
}
class Cylinder : Circle
{
new string name = "Цилиндр: ";
int height;// Высота цилиндра
public Cylinder()
{
height = 0;
}
public Cylinder(int x, int y, int radius, int height)
: base(x, y, radius)
{
this.height = height;
}
new public double Area()
{
double area = 2 * base.Area()
+ base.Length() * height;
return area;
}
public double Volume()// Объем
{
return base.Area() * height;
}
new public void Show()
{
Console.WriteLine(name + "x={0}; y={1};
r={2}; h={3}", x, y, radius, height);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их тонкой ссылкой
Point[] point = {
new Point(1, 2),
new Circle(3, 4, 5),
new Cylinder(6, 7, 8, 9)
};
// Распечатываем
string str =
@"Point[] point = {
new Point(1, 2),
new Circle(3, 4, 5),
new Cylinder(6, 7, 8, 9)
};";
Console.WriteLine("Вот такие мы создали
слоеные объекты:");
Console.WriteLine(str);
Console.WriteLine();
Console.WriteLine("Содержимое слоя Point");
for (int i = 0; i < point.Length; i++)
point[i].Show();
Console.WriteLine();
Console.WriteLine("Содержимое слоя Circle");
for (int i = 1; i < point.Length; i++)
((Circle)point[i]).Show();// Увеличиваем полномочия тонкой ссылки
Console.WriteLine();
Console.WriteLine("Содержимое слоя Cylinder");
for (int i = 2; i < point.Length; i++)
((Cylinder)point[i]).Show();// Увеличиваем полномочия тонкой ссылки
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Ссылки родственных типов";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 20;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Результат выполнения будет таким

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

Такие свойства преобразования типов очень часто применяются в функциях. Если в качестве параметра функции используется ссылка на объект базового типа, то в качестве аргумента может быть передан объект любого родственного
Чтобы проиллюстрировать использование ссылок базового типа в параметрах функции, заменим в предыдущем примере класс MyClass следующей модификацией (весь остальной код предыдущего примера остается неизменным)
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
Point point = new Point(1, 2);
Circle circle = new Circle(3, 4, 5);
Cylinder cylinder = new Cylinder(6, 7, 8, 9);
// Последовательно препарируем объекты
Console.WriteLine("Анализируется
объект Point(1, 2)");
PrepareObject(point);
Console.WriteLine("\n\nАнализируется
объект Circle(3, 4, 5)");
PrepareObject(circle);
Console.WriteLine("\n\nАнализируется
объект Cylinder(6, 7, 8, 9)");
PrepareObject(cylinder);
}
void PrepareObject(Point ob)// Функция ожидает базовый тип, поэтому
// ей можно передавать объекты любого производного типа
{
Console.WriteLine("Слой типа Point");
ob.Show(); // Всегда есть в переданном
// Безопасно проверяем, чтобы не
//адресоваться к несуществующему слою
// Проверяем существование слоя Circle одним способом
if (ob is Circle)// Выражение равно true, если слой есть
{
Console.WriteLine("\nСлой типа Circle");
Circle circle = (Circle)ob;// Повышаем полномочия ссылки ob
circle.Show();
Console.WriteLine("Длина окружности: {0}",
circle.Length());
Console.WriteLine("Площадь круга: {0}",
circle.Area());
}
// Проверяем существование слоя Cylinder другим способом
Cylinder cylinder = ob as Cylinder;
// Ссылка нулевая, если слоя нет
if (cylinder != null)
{
Console.WriteLine("\nСлой типа Cylinder");
((Cylinder)ob).Show();
// Повышаем полномочия ссылки ob налету
Console.WriteLine("Площадь всей поверхности:
{0}", cylinder.Area());
Console.WriteLine("Объем: {0}",
cylinder.Volume());
}
}
}
Результат выполнения программы с модифицированным классом MyClass следующий

Чтобы не вызвать исключение времени выполнения при попытке адресации к несуществующему слою переданного объекта, мы внутри функции вначале проверяем наличие этого слоя в адресуемом объекте. Для этого используются равноценные конструкции языка с ключевыми словами as и is.
В пределе тип ссылки в аргументе функции можно объявить как object (или Object ), поскольку класс Object является базовым для всех типов без исключения. Но в этом случае внутри функции ссылку на объект Point также нужно привести к типу Point и начало функции выглядело бы так
void PrepareObject(object ob)// Функция ожидает самый базовый тип, поэтому
// ей можно передавать объекты любого типа
{
Console.WriteLine("Слой типа Point");
((Point)ob).Show(); // Теперь тоже нужно приводить
...........Остальное без изменений...........
}
Таким образом, когда функция ожидает поступления одного из родственных объектов, связанных цепочкой наследования, но не знает, какого именно родственника ей передаст вызывающий код, правильнее всего указать в качестве аргумента тип самого базового родственника, способного представлять любого наследника. А затем уже внутри функции безопасно проверять, к какому типу принадлежит фактически поступивший родственник.
В этом и заключается динамический полиморфизм базовых ссылок, когда одна и та же ссылка может адресовать множество родственных объектов. И все это на этапе выполнения. Нужно только привести заполненную адресом конкретного объекта базовую ссылку к
Статус ссылки на родственные объекты можно не только повышать, но и снижать для адресации к тонким слоям объекта. Вот новая модификация класса MyClass нашего примера
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем многослойный толстый объект
Cylinder cylinder = new Cylinder(6, 7, 8, 9);
// Передаем объект функции для послойной препарации
PrepareObject(cylinder);
// Тонкие объекты передавать нельзя,
// когда функция ожидает толстый объект,
// поскольку функция может потребовать
// от них несуществующих возможностей
// PrepareObject((Circle)cylinder);// Ошибка компиляции!!!
// PrepareObject((Point)cylinder); // Ошибка компиляции!!!
}
void PrepareObject(Cylinder ob)// Функция ожидает толстый тип
{
// Проверяем, что это именно толстый тип
// if (ob is Cylinder)
// {
// ...
// }
// Проверять необязательно, поскольку тоньше объекта,
// чем ожидается, компилятор не пропустит, а если будет
// передан более толстый объект, мы его приведем к исходному
Cylinder cylinder = (Cylinder)ob;
// На случай, если передадут толще
// Заготавливаем адреса разных слоев составного объекта,
// снижая тем самым статус ссылки на исходный объект
Point point = (Point)ob;
Circle circle = (Circle)ob;
// Адресуемся к членам слоя Point
Console.WriteLine("Слой типа Point");
point.Show();
// Адресуемся к членам слоя Circle
Console.WriteLine("\nСлой типа Circle");
circle.Show();
Console.WriteLine("Длина окружности: {0}",
circle.Length());
Console.WriteLine("Площадь круга: {0}",
circle.Area());
// Адресуемся к членам слоя Cylinder
Console.WriteLine("\nСлой типа Cylinder");
cylinder.Show();
Console.WriteLine("Площадь всей поверхности: {0}",
cylinder.Area());
Console.WriteLine("Объем: {0}",
cylinder.Volume());
}
}
Очень важно, что компилятор принципиально не пропускает в функцию объект, который имеет тип тоньше ожидаемого, поскольку для корректной работы внутри функции переданной информации может быть недостаточно. В то же время, внутри функции мы можем адресоваться к более тонким слоям объекта, надеясь, что компилятор следит за тем, чтобы в функцию были переданы все части ожидаемого объекта. А ожидается именно заявленный в параметрах толстый объект, или еще более толстый его наследник.
Результат работы программы будет такой
В поведении базовых ссылок в рассмотренных примерах усматривается один существенный недостаток. Когда базовая ссылка адресует многослойный объект, то именно тип, к которому она принадлежит, определяет ее способность адресоваться к членам соответствующего слоя этого объекта. И для того, чтобы получить доступ к членам расширенных слоев объекта, мы вынуждены приводить ссылку к типу соответствующего слоя, вручную изменяя ее полномочия (статус). Здесь именно тип ссылки определяет ее полномочия в адресации членов многослойного объекта.
А как бы сделать так, чтобы полномочия базовой ссылки автоматически устанавливались типом переданного родственного объекта. Есть такой механизм в объектно-ориентированных языках, который называется виртуальными функциями в наследовании. Его мы сейчас и рассмотрим применительно к C#.
Вернемся к нашему примеру. Посмотрим на члены в цепочке наследования классов Cylinder:Circle:Point, например на функцию Show(), которая есть в каждом слое. Для того, чтобы вызвать эту функцию из нужного слоя
Есть более простой способ выполнить то же самое, если в базовом классе объявить функцию Show() виртуальной с помощью ключевого слова virtual, а в каждом классе-наследнике подтвердить новую версию этой функции с помощью ключевого слова override (переопределенная). После такого объявления компилятором будет создана таблица виртуальных функций. Она реализует механизм виртуальной адресации и позволит нам с помощью ссылки базового типа, которой будет присвоен адрес любого из объектов-родственников, вызывать переопределенные в этом объекте виртуальные функции. При этом явного (вручную) приведения ссылки базового типа к типу фактически адресуемого объекта не потребуется, среда исполнения сама извлечет из нужного слоя объекта подходящий член, определив его по типу самого объекта.
Одно важное условие нужно соблюдать при объявлении и переопределении виртуальных функций:
Чаще всего, когда разрабатывается новый класс как расширение наследуемого (или цепочки наследуемых) класса, программист внутри этого расширения задействует весь потенциал наследуемых классов, вводя свои настройки и сервисы для вызывающего кода. При создании экземпляров этого класса в вызывающем коде программист охотнее использует только добавленное расширение, то есть добавленный слой
Для всей иерархии виртуальных функций автоматически создается и поддерживается таблица виртуальных функций, которая при создании объекта производного класса является присоединенной к нему и заполняется адресами всех переопределений виртуальных функций.
Если в иерархической цепочке наследования в каком-то одном или нескольких производных классах переопределение виртуальной функции пропущено, то для клиента производного класса будет вызвана та ее версия, которая является ближайшей по иерархическим меркам в направлении базового класса.
Объявим в нашем примере функцию Show() виртуальной в базовом классе, а все ее версии в производных классах переопределим. Упрощенный код примера будет таким
using System;
namespace Test
{
class Point
{
string name = "Слой Point";
public void Show0()
{
Console.WriteLine(name);
}
public virtual void Show()
{
Console.WriteLine(name);
}
}
class Circle : Point
{
string name = "Слой Circle";
new public void Show0()
{
Console.WriteLine(name);
}
public override void Show()
{
Console.WriteLine(name);
}
}
class Cylinder : Circle
{
string name = "Слой Cylinder";
new public void Show0()
{
Console.WriteLine(name);
}
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их базовой ссылкой
Point[] point = {
new Point(),
new Circle(),
new Cylinder()
};
// Распечатываем
Console.WriteLine("Адресация к невиртуальной функции");
for (int i = 0; i < point.Length; i++)
{
point[i].Show0();
}
Console.WriteLine("\nАдресация к виртуальной функции");
for (int i = 0; i < point.Length; i++)
point[i].Show();
Console.WriteLine("\nТо же самое, только вручную");
point[0].Show0();
((Circle)point[1]).Show0();
((Cylinder)point[2]).Show0();
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Механизм виртуальных функций";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Обратите внимание, что в каждом производном классе есть переменная с одинаковым именем name. Но ее помечать как новую не нужно, поскольку она невидима в производном классе. При адресации ссылкой базового типа к не виртуальным функциям родственных объектов всегда адресуется только базовый слой. При подобной адресации к виртуальным функциям адресуется член расширения самого объекта или ближайший в глубину наследования. Поиск адресуемого члена ищется послойно, начиная с адресуемого слоя в сторону базового.
Результат, выдаваемый программой, будет таким

Если в крайнем слое адресуемого объекта вызываемая функция непереопределена или отсутствует в нем, ищется ее переопределение в ближайшем слое в сторону слоя базового класса.
Пример
using System;
namespace Test
{
class Point
{
string name = "Слой Point";
public virtual void Show()
{
Console.WriteLine(name);
}
}
class Circle : Point
{
string name = "Слой Circle";
public override void Show()
{
Console.WriteLine(name);
}
}
class Cylinder : Circle
{
string name = "Слой Cylinder";
new public void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их базовой ссылкой
Point[] point = {
new Point(),
new Circle(),
new Cylinder()
};
// Распечатываем
Console.WriteLine("Адресация к ближайшей
виртуальной функции");
for (int i = 0; i < point.Length; i++)
point[i].Show();
Console.WriteLine();
Console.WriteLine("Адресуемся к слою
Cylinder напрямую");
((Cylinder)point[2]).Show();// Адресуемся напрямую
// Извращенный синтаксис использования массива
Console.WriteLine();
Console.WriteLine("Нерациональный
синтаксис использования массива");
for (int i = 0; i < 3; i++)
{
Point point1 = new Point[]{
new Point(),
new Circle(),
new Cylinder()
}[i];
point1.Show();
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Механизм виртуальных функций";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 15;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Результат выполнения будет такой

Мы видим, что при попытке адресации виртуальной функции в объекте Cylinder, которой в расширяющем слое этого объекта нет, вызвался вариант из слоя Circle. Тонкая ссылка при адресации к толстому объекту игнорирует невиртуальные функции в толстых слоях, поскольку они не включены в таблицу виртуальных функций. Если бы и в слое Circle объекта Cylinder этой функции не было, то вызвался бы вариант слоя Point. Если бы вообще этой функции не было ни в одном слое, то была бы сгенерирована ошибка компиляции как попытка вызова несуществующего члена.
Вот еще один отвлеченный пример адресации к ближайшей виртуальной функции
using System;
namespace Test
{
class A
{
string name = "Слой A";
public virtual void Show()
{
Console.WriteLine(name);
}
}
class B : A
{
string name = "Слой B";
}
class C : B
{
string name = "Слой C";
public override void Show()
{
Console.WriteLine(name);
}
}
class D : C
{
string name = "Слой D";
}
class E : D
{
string name = "Слой E";
public override void Show()
{
Console.WriteLine(name);
}
}
class F : E
{
string name = "Слой F";
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех типов
// иерархической цепочки наследования
// и адресуем их базовой ссылкой
A[] a = {
new A(),
new B(),
new C(),
new D(),
new E(),
new F()
};
// Распечатываем
Console.WriteLine("Адресация к ближайшей
виртуальной функции");
Console.WriteLine("В классах B и D виртуальная функция отсутствует");
for (int i = 0; i < a.Length; i++)
{
Console.Write("Объект " +
a[i].GetType().Name + "\t");
a[i].Show();
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Поиск виртуальной функции в
глубину наследования";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
и результат выполнения программы

Мы видим, что при адресации к слою, где функция не переопределена (слой B и D ), поиск по порядку ведется в ближайших от адресуемого слоях в направлении базового слоя.
using System;
namespace Test
{
class A
{
string name = "Слой A";
public virtual string Name
{
get { return name; }
}
public virtual void Show()
{
Console.WriteLine(name);
}
}
class B : A
{
string name = "Слой B";
public override string Name
{
get { return name; }
}
public override void Show()
{
Console.WriteLine(name);
}
}
class C : B
{
string name = "Слой C";
public override string Name
{
get { return name; }
}
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
A[] a = {
new A(),
new B(),
new C()
};
// Распечатываем
Console.WriteLine("Виртуальные методы:");
for (int i = 0; i < a.Length; i++)
{
Console.Write("Объект " +
a[i].GetType().Name + "\t");
a[i].Show();
}
// Распечатываем
Console.WriteLine("\nВиртуальные свойства:");
for (int i = 0; i < a.Length; i++)
{
Console.WriteLine("Объект " +
a[i].GetType().Name + "\t" +
"{0}", a[i].Name);
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Виртуальные методы и свойства";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}

Применение наследования позволяет поэтапно наращивать класс, вводя все новые расширения наследуемого кода, т.е. постепенно продвигаясь от абстрактного к конкретному. Когда программист разрабатывает очередное расширение (описание производного класса), он может планировать такие задачи, для реализации которых в текущем расширении еще нет достаточных условий.
Например, разрабатывая расширение Circle, можно предположить, что в дальнейшем на базе этого кода будет построено новое расширение типа Cylinder. А там может потребоваться функция вычисления объема Volume(). Чтобы не забыть об этом факте, можно вставить в текущее расширение только объявление этой будущей виртуальной функции, без ее реализации, надеясь реализовать ее в следующих расширениях, например в Cylinder. Такие виртуальные функции ( методы с отложенной реализацией ) называются чисто виртуальными или просто абстрактными.
Как и в ряде других случаев, при объявлении абстрактных классов введена некоторая синтаксическая избыточность ( контролирующее обременение ) для усиления контроля компилятора за действиями программиста: если в классе объявлен хоть один
Вот модификация предыдущего примера, иллюстрирующая сказанное
using System;
namespace Test
{
abstract class A
{
string name = "Слой A";
public abstract void Show();// Отложили реализацию
}
// Реализации наследуемого абстрактного метода еще нет,
// значит слой тоже нужно обозначить абстрактным
abstract class B : A
{
string name = "Слой B";
}
// Есть первая реализация, абстрактные слои закончились
class C : B
{
string name = "Слой C";
public override void Show()
{
Console.WriteLine(name);
}
}
class D : C
{
string name = "Слой D";
}
class E : D
{
string name = "Слой E";
public override void Show()
{
Console.WriteLine(name);
}
}
class F : E
{
string name = "Слой F";
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем объекты для всех неабстрактных
// типов иерархической цепочки наследования,
// применяя ссылку абстрактного базового типа
A[] a = {
// new A(), // Ошибка компиляции!!!
// new B(), // Ошибка компиляции!!!
new C(),
new D(),
new E(),
new F()
};
// Распечатываем
Console.WriteLine("Реализация абстрактных методов");
for (int i = 0; i < a.Length; i++)
{
Console.Write("Объект " +
a[i].GetType().Name + "\t");
a[i].Show();
}
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Абстрактные методы и классы";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Результат будет таким

Обратите внимание, что создавать экземпляры абстрактных классов компилятор не дает, но в тоже время объявлять и использовать ссылки абстрактных типов можно (что мы и сделали для базового абстрактного типа A ). Он же следит и затем, чтобы класс, наследующий
Способ 1. Класс с защищенным конструктором не может иметь экземпляров, но может участвовать в наследовании
using System;
namespace Test
{
class A
{
string name = "Класс A";
protected A()
{
}
public virtual void Show()
{
Console.WriteLine(name);
}
}
class B : A
{
string name = "Класс B";
public override void Show()
{
Console.WriteLine(name);
}
}
class C : B
{
string name = "Класс C";
public override void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем и распечатываем объекты
//A a = new A(); a.Show(); // Ошибка компиляции
B b = new B(); b.Show();
C c = new C(); c.Show();
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Защищенный конструктор класса";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Способ 2. Аналогичным образом, класс, объявленный как статический, тоже не может иметь экземпляров. Но статический класс нельзя использовать и в наследовании, поскольку механизм наследования работает только для экземплярных классов.
Способ 3. Класс нужно обязательно объявлять абстрактным, если он содержит или наследует, а сам не реализует, хотя-бы один C#, потому что класс сырой и не готов к производству
объектов. Но любой класс, даже не содержащий абстрактных или виртуальных методов, мы можем объявить абстрактным, если не хотим, чтобы кто-то или мы сами использовали его для создания экземпляров. Обычно это промежуточные вспомогательные классы-заготовки, планируемые для дальнейшего развития с помощью механизма наследования.
Вот простой пример, когда объявление любого класса абстрактным запрещает его реализацию в объекты
using System;
namespace Test
{
// Хоть нет абстрактных методов
// и класс готов к производству объектов,
// захотели объявить абстрактным
abstract class A
{
string name = "Класс A";
public virtual void Show()
{
Console.WriteLine(name);
}
}
// Может производить объекты,
// хоть и наследует абстрактный класс
class B : A
{
string name = "Класс B";
public override void Show()
{
Console.WriteLine(name);
}
}
// Может производить объекты
class C
{
string name = "Класс C";
public void Show()
{
Console.WriteLine(name);
}
}
// Нет виртуальных функций и ничего не наследует,
// все равно можно запретить производить объекты
abstract class D
{
string name = "Класс D";
public void Show()
{
Console.WriteLine(name);
}
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Создаем и распечатываем объекты
// A a = new A(); a.Show(); // Ошибка компиляции!!!
B b = new B(); b.Show();
A b1 = new B(); b1.Show();// Тонкая ссылка - толстый объект
C c = new C(); c.Show();
// D d = new D(); d.Show(); // Ошибка компиляции!!!
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Производить объекты
абстрактных классов запрещено";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Вот результат после закомментирования выделенных строк
Иногда разработанный класс может оказаться настолько совершенным и самодостаточным, что не предполагает никаких дальнейших усовершенствований путем новых расширений. В этом случае его можно объявить с модификатором sealed и компилятор будет пресекать все попытки использовать его в качестве базового для дальнейших расширений. При этом будет безразлично, включен ли такой класс в текущий проект в исходном виде, или существует в откомпилированном виде в подключенной к проекту библиотечной сборке.
Вот пример
using System;
namespace Test
{
// Не может иметь наследников
sealed class A
{
}
/* Чтобы быстро отменить комментарий, поставьте спереди слеш /
// Ошибка компиляции здесь!!! Запрещено наследоваться
class B : A
{
}
//*/
// Может наследоваться
class C
{
}
// Может наследоваться
class D : C
{
}
// Вызывающая сторона
class MyClass
{
public MyClass()
{
// Просто создаем объекты в управляемой куче
A a = new A(); // Создавать объекты можно,
// а наследоваться нельзя
//B b = new B();// Нет такого класса, он закомментирован
C c = new C();
D d = new D();
}
}
// Запуск
class Program
{
static void Main()
{
// Настройка консоли
Console.Title = "Запрет классу иметь наследников";
Console.ForegroundColor = ConsoleColor.White;
Console.CursorVisible = false;
Console.WindowWidth = 60;
Console.WindowHeight = 10;
new MyClass();// Чтобы сработал конструктор
Console.ReadLine();
}
}
}
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.