Наследование является одним из принципов ООП.
Основы классификации и реализация механизмов повторного использования и модификации кода. Базовый класс задает общие признаки и общее поведение для классов-наследников.
Общие (наиболее общие) свойства и методы наследуются от базового класса, в дополнение к которым добавляются и определяются НОВЫЕ свойства и методы.
Таким образом, прежде всего наследование реализует механизмы расширения базового класса.
Реализация принципов наследования на примере:
using System;
namespace Inheritance_1
{
public class A
{
public int val1_A;
public void fun1_A (String str)
{
Console.WriteLine("A's fun1_A:" + str);
}
}
public class B:A
{
public int val1_B;
public void fun1_B (String str)
{
Console.WriteLine("B's fun1_B:" + str);
}
}
class Class1
{
static void Main(string[] args)
{
B b0 = new B();
// От имени объекта b0 вызвана собственная функция fun1_B.
b0.fun1_B("from B");
// От имени объекта b0 вызвана унаследованная от класса A функция fun1_A.
b0.fun1_A("from B");
}
}
}
Производный класс наследует от базового класса ВСЕ, что он имеет. Другое дело, что воспользоваться в производном классе можно не всем наследством.
Добавляем в базовый класс private -члены:
public class A
{
public int val1_A = 0;
public void fun1_A (String str)
{
Console.WriteLine("A's fun1_A:" + str);
this.fun2_A("private function from A:");
}
// При определении переменных
// в C# ничего не происходит без конструктора и оператора new.
// Даже если они и не присутствуют явным образом в коде.
private int val2_A = 0;
private void fun2_A (String str)
{
Console.WriteLine(str + "A's fun2_A:" + val2_A.ToString());
}
}
И объект-представитель класса B в принципе НЕ может получить доступ к private данным — членам и функциям — членам класса A. Косвенное влияние на такие данные-члены и функции — члены – лишь через public -функции класса A.
Следует иметь в виду еще одно важное обстоятельство.
Если упорядочить все (известные) спецификаторы доступа C# по степени их открытости
Public |
... | private |
то наследуемый класс не может иметь более открытый спецификатор доступа, чем его предок.
Используем еще один спецификатор доступа – protected. Этот спецификатор обеспечивает открытый доступ к членам базового класса, но только для производного класса!
public class A
{
:::::::::
protected int val3_A = 0;
}
public class B:A
{
:::::::::
public void fun1_B (String str)
{
:::::::::
this.val3_A = 125;
}
}
static void Main(string[] args)
{
:::::::::
//b0.val3_A = 125; // Это член класса закрыт для внешнего использования!
}
Защищенные члены базового класса доступны для ВСЕХ прямых и косвенных наследников данного класса.
И еще несколько важных замечаний относительно использования спецификаторов доступа:
protected в public ). Но в пространстве имен явным образом можно использовать лишь один спецификатор – спецификатор public, либо не использовать никаких спецификаторов.Продолжаем совершенствовать наши классы A и B. Очередная задача – выяснить способы передачи управления конструктору базового класса при создании объекта — представителя производного класса.
Таким образом, передача управления конструктору базового класса осуществляется посредством конструкции
...(...):base(...){...},
которая располагается в объявлении конструктора класса-наследника между заголовком конструктора и телом. После ключевого слова base в скобках располагается список значений параметров конструктора базового класса. Очевидно, что выбор соответствующего конструктора определяется типом значений в списке (возможно, пустом) параметров:
using System;
/*private*/ class X
{
}
/*public*/ class A
{
public A(){val2_A = 0; val3_A = 0;}
// К этому конструктору также можно обратиться из производного класса.
protected A(int key):this()
{val1_A = key;}
// А вот этот конструктор предназначен исключительно
// для внутреннего использования.
private A(int key1,int key2,int key3)
{val1_A = key1; val2_A = key2; val3_A = key3;}
public int val1_A = 0;
public void fun1_A (String str)
{
Console.WriteLine("A's fun1_A:" + str);
this.fun2_A("private function from A:");
fun3_A();
}
private void fun2_A (String str)
{
Console.WriteLine(str + "A's fun2_A:" + val2_A.ToString());
}
protected int val3_A;
private void fun3_A ()
{
A a = new A(1,2,3);
a.fun2_A("Это наше внутреннее дело!");
}
}
/*public*/ class B:A
{
// Явные обращения к конструкторам базового класса.
public B():base(){val1_B = 0;}
public B(int key):base(key){val1_B = key;}
}
class Class1
{
static void Main(string[] args)
{
B b0 = new B(125);
}
}
Для создания объектов в программе можно применять конструкторы следующих степеней защиты:
public – при создании объектов в рамках данного пространства имен, в методах любого класса — члена данного пространства имен;
protected – при создании объектов в рамках производного класса, в том числе при построении объектов производного класса, а также для внутреннего использования классом — владельцем данного конструктора;
private – применяется исключительно для внутреннего использования классом-владельцем данного конструктора.
Теперь (в рамках того же приложения) построим два новых класса:
class X
{
}
class Y:X
{
}
// Это уже в Main()
Y y = new Y();
Вся работа по созданию объекта – представителя класса Y при явном отсутствии конструкторов по умолчанию возлагается на КОНСТРУКТОРЫ УМОЛЧАНИЯ – те самые, которые самостоятельно строит транслятор.
Особых проблем не будет, если в производном классе явным образом начать объявлять конструкторы:
class X
{
}
class Y:X
{
public Y(int key){}
public Y(){}
}
// Это уже в Main()
Y y0 = new Y();
Y y1 = new Y(125);
С этого момента в производном классе нет больше конструктора умолчания. Теперь все зависит от соответствия оператора определения объекта построенному нами конструктору.
Объявим в производном классе оба варианта конструкторов. И опять все хорошо. Конструктор умолчания базового класса (тот, который строится транслятором) продолжает исправно выполнять свою работу.
Проблемы в производном классе возникнут, если в базовом классе попытаться объявить вариант конструктора с параметрами:
class X
{
public X(int key){}
}
class Y:X
{
public Y(int key){} // Нет конструктора умолчания базового класса!
public Y(){} // Нет конструктора умолчания базового класса!
}
// Это уже в Main()
Y y0 = new Y();
Y y1 = new Y(125);
И здесь транслятор начнет обижаться на конструкторы производного класса, требуя ЯВНОГО объявления конструктора базового класса БЕЗ параметров. Если вспомнить, что при ЛЮБОМ вмешательстве в дело построения конструкторов транслятор снимает с себя всю ответственность, причина негодования транслятора становится очевидной. Возможны два варианта решения проблемы:
class X
{
public X(){}
public X(int key){}
}
class Y:X
{
public Y(int key){}
public Y():base(125){}
}
// Это уже в Main()
Y y0 = new Y();
Y y1 = new Y(125);
При объявлении членов производного класса в C# разрешено использовать те же самые имена, которые применялись при объявлении членов базового класса. Это касается как методов, так и данных — членов объявляемых классов.
В этом случае соответствующие члены базового класса считаются переопределенными.
Следующий простой фрагмент кода содержит объявления базового и производного классов. Особое внимание в этом примере следует уделить ключевому слову new в объявлении одноименных членов в производном классе. В данном контексте слово new выступает в качестве модификатора, который используется для явного обозначения переопределяемых членов базового класса:
using System;
namespace Inheritance_2
{
class X
{
public X(){}
public X(int key){}
public int q0;
public int q;
public void fun0()
{
Console.WriteLine("class X, function fun0()");
q = 125;
}
public void fun1()
{
Console.WriteLine("class X, function fun1()");
}
}
class Y:X
{
public Y(int key){}
public Y():base(125){}
new public int q; // Если опустить модификатор new –
new public void fun1() // появится предупреждение об ожидаемом new...
{
base.fun1(); // Обращение к переопределенным членам базового класса.
base.q = 125;
Console.WriteLine("class Y, function fun1()");
}
static void Main(string[] args)
{
Y y0 = new Y();
Y y1 = new Y(125);
// А извне переопределенные члены базового класса не видны.
y0.fun1();
y0.q = 100;
y0.fun0();
y0.q0 = 125;
}
}
}
При переопределении наследуемого члена его объявление в классе-наследнике должно предваряться модификатором new. Если этот модификатор в объявлении производного класса опустить – ничего страшного не произойдет. Транслятор всего лишь выдаст предупреждение об ожидаемом спецификаторе new в точке объявления переопределяемого члена класса.
Дело в том, что переопределенные члены базового класса при "взгляде" на производный класс "извне" не видны. В производном классе они замещаются переопределенными членами, и непосредственный доступ к этим членам возможен только из функций — членов базового и производного классов. При этом для обращения к переопределенному члену из метода производного класса используется ключевое слово base.
По мнению создателей языка C#, тот факт, что ранее (до момента его переопределения) видимый член (базового) класса стал недоступен и невидим извне (в результате его переопределения в производном классе), требует явного дополнительного подтверждения со стороны программиста.
Объявление класса может содержать вложенное (или внедренное) объявление класса. C# допускает и такие объявления:
class X
{
public class XX
{
}
}
Одновременное включение явного объявления класса или структуры с одним и тем же именем в базовый и производный классы также требует явной спецификации с помощью модификатора new:
class X
{
public class XX
{
}
}
class Y:X
{
new public class XX
{
}
}
Модификатор new используется при переопределении общедоступных объявлений и защищенных объявлений в производном классе для явного указания факта переопределения.
Этот модификатор используется при объявлении класса-наследника в том случае, если надо скрыть факт наследования объявляемого члена класса.
public class MyBaseC
{
public int x;
public void Invoke()
{
:::::
}
}
Объявление члена класса Invoke в наследуемом классе скрывает метод Invoke в базовом классе:
public class MyDerivedC : MyBaseC
{
new public void Invoke()
{
:::::
}
}
А вот данное-член x не было скрыто в производном классе соответствующим членом, следовательно, остается доступным в производном классе.
Сокрытие имени члена базового класса в производном классе возможно в одной из следующих форм:
Одновременное использование в производном классе члена базового класса и его переопределенного потомка является ошибкой.
В этом примере и базовый BC-, и производный DC-классы используют одно и то же имя для обозначения объявляемого в обоих классах члена типа int. New -модификатор подчеркивает факт недоступности члена x базового класса в производном классе. Однако из производного класса все-таки можно обратиться к переопределенному полю базового класса с использованием полного квалифицированного имени:
using System;
public class BC
{
public static int x = 55;
public static int y = 22;
}
public class DC : BC
{
new public static int x = 100; // Переопределили член базового класса
public static void Main()
{
// Доступ к переопределенному члену x:
Console.WriteLine(x);
// Доступ к члену базового класса x:
Console.WriteLine(BC.x);
// Доступ к члену y:
Console.WriteLine(y);
}
}
В производном классе DC переопределяется вложенный класс C.
В рамках производного класса легко можно создать объект — представитель вложенного переопределенного класса C. Для создания аналогичного объекта — представителя базового класса необходимо использовать полное квалифицированное имя:
using System;
public class BC
{
public class C
{
public int x = 200;
public int y;
}
}
public class DC:BC
{
new public class C // Вложенный класс базового класса скрывается
{
public int x = 100;
public int y;
public int z;
}
public static void Main()
{
// Из производного класса виден переопределенный вложенный класс:
C s1 = new C();
// Полное имя используется для доступа к классу, вложенному в базовый:
BC.C s2 = new BC.C();
Console.WriteLine(s1.x);
Console.WriteLine(s2.x);
}
}
Принцип наследования допускает неограниченную глубину иерархии наследования. Производный класс, являющийся наследником базового класса, может в свою очередь сам оказаться в роли базового класса. Однако не всегда продолжение цепочки "предок-потомок" может оказаться целесообразным.
Если при разработке класса возникла ситуация, при которой дальнейшее совершенствование и переопределение возможностей класса в деле решения специфических задач окажется нежелательным (сколько можно заниматься переопределением функций форматирования), класс может быть закрыт для дальнейшего наследования. Закрытие класса обеспечивается спецификатором sealed. При этом закрываться для наследования может как класс целиком, так и отдельные его члены:
sealed class X
{
}
class Y:X // Наследование от класса X невозможно
{
}
А так закрывается для переопределения функция – член класса:
class X
{
sealed public void f0()
{
}
}
class Y:X
{
public void f0(){}
// Наследование (переопределение) f0, объявленной в X, запрещено!
}
При реализации принципа наследования базовый класс воплощает НАИБОЛЕЕ ОБЩИЕ черты разрабатываемого семейства классов. Поэтому на этапе разработки базового класса часто бывает достаточно лишь обозначить множество функций, которые будут определять основные черты поведения объектов — представителей производных классов.
Если базовый класс объявляется, исходя из следующих предпосылок:
return ) в базовом классе лишено всякого смысла.class X
{
public int f0()
{
// Если, в соответствии с замыслом разработчика, этот код ВСЕГДА
будет недоступен,
// то зачем он в принципе нужен?
return 0;
}
}
class Y:X
{
new public int f0()
{
::::::::::
// Здесь размещается код переопределенной функции.
::::::::::
return 0;
}
}
Такой код никому не нужен, и C# позволяет избегать подобных странных конструкций. Вместо переопределяемой "заглушки" можно использовать объявление абстрактной функции.
Синтаксис объявления абстрактной функции предполагает использование ключевого слова abstract и полное отсутствие тела. Объявление абстрактной функции завершается точкой с запятой.
Класс, содержащий вхождения абстрактных (хотя бы одной!) функций, также должен содержать в заголовке объявления спецификатор abstract.
В производном классе соответствующая переопределяемая абстрактная функция обязательно должна включать в заголовок функции спецификатор override. Его назначение – явное указание факта переопределения абстрактной функции.
Абстрактный класс фактически содержит объявления нереализованных функций базового класса. На основе абстрактного класса невозможно определить объекты. Попытка создания соответствующего объекта — представителя абстрактного класса приводит к ошибке, поскольку в классе не определены алгоритмы, определяющие поведение объекта:
abstract class X // Абстрактный класс с одной абстрактной функцией.
{
public abstract int f0();
}
class Y:X
{
// Переопределение абстрактной функции должно
// содержать спецификатор override.
public override int f0()
{
::::::::::
return 0;
}
}
::::::::::
static void Main(string[] args)
{
X x = new X(); // NO!
Y y0 = new Y(125);
// Работает переопределенная абстрактная функция!
y0.f0();
}
Еще пример:
using System;
namespace Interface01
{
// Абстрактный класс.
abstract class aX1
{
public int xVal;
// Его конструкторы могут использоваться при построении
// объектов класcов-наследников.
public aX1(int key)
{
Console.WriteLine("aX1({0})...", key);
xVal = key;
}
public aX1()
{
Console.WriteLine("aX1()...");
xVal = 0;
}
public void aX1F0(int xKey)
{
xVal = xKey;
}
public abstract void aX2F0();
}
class bX1:aX1
{
new public int xVal;
public bX1():base(10)
{
xVal = 125;
Console.WriteLine
("bX1():base(10)... xVal=={0},base.xVal=={1}...", xVal, base.xVal);
}
public bX1(int key):base(key*10)
{
xVal = key;
Console.WriteLine("bX1({0}):base({1})...", xVal, base.xVal);
}
public override void aX2F0()
{
xVal = xVal*5;
base.xVal = base.xVal*100;
}
}
class Class1
{
static void Main(string[] args)
{
// Ни при каких обстоятельствах не может служить основой для
// построения объектов. Даже если не содержит ни одного объявления
// абстрактной функции.
//aX1 x = new aX1();
bX1 x0 = new bX1();
x0.aX1F0(10); // Вызвали неабстрактную функцию базового абстрактного класса.
bX1 x1 = new bX1(5);
x1.aX2F0();//Вызвали переопределенную функцию. В базовом классе это
// абстрактная функция.
}
}
}
Это универсальная ссылка для любого объекта производного типа, наследующего данный базовый класс:
using System;
namespace Inheritance_3
{
class X
{//===================================================
public int q0 = 0;
public int q = 0;
public void fun0()
{
Console.WriteLine("class X, fun0()");
}
public void fun1()
{
Console.WriteLine("class X, fun1()");
}
}//===================================================
class Y:X
{//===================================================
new public int q = 0;
new public void fun1()
{
Console.WriteLine("class Y, fun1()");
}
public void fun00()
{
Console.WriteLine("class Y, fun00()");
}
}//===================================================
class Z:X
{//===================================================
new public int q = 0;
new public void fun1()
{
Console.WriteLine("class Z, fun1()");
}
public void fun00()
{
Console.WriteLine("class Z, fun00()");
}
}//===================================================
class StartClass
{//===================================================
static void Main(string[] args)
{
X x = null; // Просто ссылка!
// Объекты – представители производных классов-наследников.
Y y = new Y(); y.fun0(); y.fun00(); y.fun1();
Z z = new Z(); z.fun0(); z.fun00(); z.fun1();
// Настройка базовой ссылки.
x = y; x.fun0(); x.fun1(); x.q = 100; x.q0 = 125;
x = z; x.fun0(); x.fun1(); x.q = 100; x.q0 = 125;
}
}//===================================================
}
Результат:
class X, fun0() class Y, fun00() class Y, fun1() class X, fun0() class Z, fun00() class Z, fun1() class X, fun0() class X, fun1() class X, fun0() class X, fun1()
Вопросов не будет, если рассмотреть схему объекта — представителя класса Y.

Вот что видно из ссылки на объект класса X, настроенного на объект — представитель производного класса. Схема объекта — представителя класса Z и соответствующий "вид" от ссылки x выглядят аналогичным образом.
При преобразованиях ссылочных типов используются операции преобразования is и as. Вот код, иллюстрирующий особенности применения операций is и as:
using System;
namespace derivation01
{
// Базовый класс...
class X
{//_____________________________.
public int f1(int key)
{
Console.WriteLine("X.f1");
return key;
}
}//_____________________________.
// Производный...
class Y:X
{//_____________________________.
new public int f1(int key)
{
Console.WriteLine("Y.f1");
base.f1(key);
return key;
}
public int yf1(int key)
{
Console.WriteLine("Y.yf1");
return key;
}
}//_____________________________.
// Производный...
class Z:X
{//_____________________________.
public int zf1(int key)
{
Console.WriteLine("Z.zf1");
return key;
}
}//_____________________________.
class Class1
{
static void Main(string[] args)
{
int i;
// Всего лишь ссылки на объекты...
X x;
Y y;
Z z;
Random rnd = new Random();
// И вот такой тестовый пример позволяет выявить особенности применения
// операций is и as.
for (i = 0; i < 10; i++)
{
// Ссылка на объект базового класса случайным образом
// настраивается на объекты производного класса.
if (rnd.Next(0,2) == 1)
x = new Y();
else
x = new Z();
// Вызов метода f1 не вызывает проблем.
// Метод базового класса имеется у каждого объекта.
x.f1(0);
// А вот вызвать метод, объявленный в производном классе
// (с использованием операции явного приведения типа),
// удается не всегда. Метод yf1 был объявлен лишь в классе Y.
// Ниже операция is принимает значение true лишь в том случае,
// если ссылка на объект базового класса была настроена на объект
// класса Y.
if (x is Y)
{
((Y)x).yf1 (0); // И только в этом случае можно вызвать метод,
// объявленный в Y.
}
else
{
((Z)x).zf1 (1); // А в противном случае попытка вызова yf1
// привела бы к катастрофе.
try
{
((Y)x).yf1 (0);
}
catch (Exception ex)
{
Console.WriteLine("–1–" + ex.ToString());
}
}
// А теперь объект, адресуемый ссылкой на базовый класс, надо попытаться
// ПРАВИЛЬНО переадресовать на ссылку соответствующего типа. И это тоже
// удается сделать не всегда. Явное приведение может вызвать исключение.
try
{
z = (Z)x;
}
catch (Exception ex)
{
Console.WriteLine("–2–" + ex.ToString());
}
try
{
y = (Y)x;
}
catch (Exception ex)
{
Console.WriteLine("–3–" + ex.ToString());
}
// Здесь помогает операция as.
// В случае невозможности переадресации соответствующая ссылка оказывается
// установленной в null. А эту проверку выполнить проще...
if (rnd.Next(0,2) == 1)
{
z = x as Z;
if (z != null) z.zf1(2);
else Console.WriteLine("?????");
}
else
{
y = x as Y;
if (y != null) y.yf1(3);
else Console.WriteLine("!!!!!");
}
}
}
}
}
Любой тип .NET строится на основе базового типа (наследует) object. Это означает, что:
object доступны к выполнению любым (производным) типом,object и обратно. Деятельность по приведению объекта к типу object называется Boxing. Обратное преобразование называют Unboxing .Примеры вызова методов базового класса:
int i = 125; i.ToString(); i.Equals(100);
Примеры преобразований:
int i = 125; // Объявление и инициализация переменной типа int. object o = i; // Неявное приведение к типу object в результате присвоения. object q = (object)i; // Явное приведение к типу object. int x = (int)o; // Пример Unboxing'а.
И еще пример кода на ту же тему:
using System;
namespace Boxing02
{
class TestClass
{
public int t;
public string str;
// Конструктор класса принимает параметры в упакованном виде.
public TestClass(object oT, object oStr)
{
t = (int)oT; // unboxing посредством явного преобразования.
str = oStr as string; // unboxing в исполнении операции as.
}
}
class Program
{
static void Main(string[] args)
{
TestClass tc;
// При создании объекта пакуем параметры.
tc = new TestClass((object)0, (object)(((int)0).ToString()));
Console.WriteLine("tc.t == {0}, tc.str == {1}", tc.t, tc.str);
}
}
}
Основа реализации принципа полиморфизма – наследование. Ссылка на объект базового класса, настроенная на объект производного, может обеспечить выполнение методов ПРОИЗВОДНОГО класса, которые НЕ БЫЛИ ОБЪЯВЛЕНЫ В БАЗОВОМ КЛАССЕ. При реализации принципа полиморфизма происходят вещи, которые не укладываются в ранее описанную схему.
Одноименные функции с одинаковой сигнатурой могут объявляться как в базовом, так и в производном классах. Между этими функциями может быть установлено отношение замещения:
функция ПРОИЗВОДНОГО класса ЗАМЕЩАЕТ функцию БАЗОВОГО класса.
Основное условие отношения замещения заключается в том, что замещаемая функция базового класса должна при этом дополнительно специфицироваться спецификатором virtual.
Отношение замещения между функциями базового и производного класса устанавливается, если соответствующая (одноименная функция с соответствующей сигнатурой) функция производного класса специфицируется дополнительным спецификатором override.

Введем следующие обозначения и построим схему наследования.
public void F(){ } |
0 |
![]() |
public virtual void F() { } |
0 |
![]() |
new public virtual void F() { } |
1..N |
![]() |
new public void F() { } |
1..N |
![]() |
public override void F() { } |
1..N |
![]() |
Схема возможных вариантов объявления методов в иерархии наследования трех уровней:

Наконец, замечательный пример:
using System;
namespace Implementing
{
class A
{
public virtual void F() { Console.WriteLine("A");}
}
class B:A
{
public override void F() { Console.WriteLine("B"); }
}
class C:B
{
new public virtual void F() { Console.WriteLine("C"); }
}
class D:C
{
public override void F() { Console.WriteLine("D"); }
}
class Starter
{
static void Main(string[] args)
{
D d = new D();
C c = d;
B b = c;
A a = b;
d.F(); /* D */
c.F(); /* D */
b.F(); /* B */
a.F(); /* B */
}
}
}
в котором становится ВСЕ понятно, если ПОДРОБНО нарисовать схемы классов, структуру объекта — представителя класса D и посмотреть, КАКАЯ ссылка ЗА КАКОЕ место этот самый объект удерживает.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.