double arrow

Наследование реализации

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

Основная идея классического наследования заключается в том, что новые классы могут использовать и увеличивать (наращивать) функциональные возможности своих базовых классов. В С# каждый класс может иметь не более одного базового класса. Написав и огладив код базового класса, можно, не изменяя его, только за счет наследования приспособить новый, порожденный класс для работы в новых ситуациях.

Класс в С# может иметь произвольное количество потомков и только одного предка. При создании класса, вместо написания абсолютно новых переменных и методов класса, программист может обозначить факт того, что новый класс должен наследовать данные и методы другого класса. Новый порожденный класс, к которому обычно добавляются уникальные переменные и методы, часто по размеру больше своего базового класса. Именно поэтому порожденный класс более специфичен, нежели его базовый класс. Обычно производный класс содержит данные и поведения, взятые у базового класса, и добавляет к ним новые.

При использовании наследования для создания нового класса из уже существующего новый класс наследует все элементы (переменные и методы) из существующего класса. Новый класс можно настроить под соответствие нужным требованиям путем включения дополнительных переменных и методов членов класса и путем подмены членов базового класса.

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

Или другой пример. Например, прямоугольник - это четырехугольник (впрочем, как и квадраты, параллелограммы или трапеции). Следовательно, можно сказать, что класс Rectangle (прямоугольник) наследуется от класса Quadrilateral (четырехугольник). В данном контексте класс четырехугольника является базовым, а класс прямоугольника – производным. Прямоугольник представляет собой более специфический (конкретный) тип четырехугольника, но некорректно было бы говорить, что любой четырехугольник является прямоугольником. Потому что четырехугольник может быть еще и параллелограммом, трапецией, ромбом или еще чем-то, порожденным от класса четырехугольника. Отсюда следует, что набор объектов, представленный базовым классом, как правило, больше, чем набор объектов, представленных любым из его производных классов. Например, базовый класс "транспортное средство" представляет (описывает) все транспортные средства, включая легковые и грузовые автомобили, корабли, велосипеды и т.д.. Производный же класс "легковые автомобили" представляет собой только лишь небольшое множество транспортных средств.

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


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

Таким образом, наследование представляет собой механизм, когда общие (или наиболее общие) данные и методы берутся от базового класса, и в дополнение к ним добавляются и определяются НОВЫЕ свойства и методы. Поэтому, прежде всего, наследование реализует механизмы расширения базового класса.

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

Наследование применяется для достижения следующих взаимосвязанных целей:

1) исключения из программы повторяющихся фрагментов кода;

2) упрощения модификации программы;

3) упрощения создания новых программ на основе существующих.

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

Определение наследующих классов

Класс в С# может иметь произвольное количество потомков и только одного предка. При описании дочернего класса имя его предка записывается в заголовке класса сразу после двоеточия. Если имя предка не указано, то предком считается базовый класс всей иерархии System.Object:

[ атрибуты ] [ спецификаторы ] class имя класса [ : предки ] тело класса

Например,

//Обьявление базового класса

public class A

{ … };

// Объявление производного класса

public class B : A

{ … };

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

Конструкторы базовых классовне наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов определяется следующими правилами:

  • Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, то автоматически вызывается конструктор базового класса без параметров.
  • Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются его объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. Таким образом, каждый конструктор инициализирует свою часть объекта.
  • Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации. Вызов выполняется с помощью ключевого слова base. При этом, вызывается та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base.

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

Несмотря на то, что private члены базового класса и наследуются, но при наследовании они не доступны напрямую из своих производных классов. Все остальные (не private) члены базового класса сохраняют свой уровень доступа после их наследования производными классами. Например, public члены базового класса становятся public членами производного класса, а protected члены - protected членами производного класса. В результате объекты всех классов, производных от базового класса, можно рассматривать как объекты этого базового класса.

Методы производных классов могут требовать доступа к не закрытым переменным и методам своего базового класса. Элементы базового класса, которые не должны быть доступными для методов производных классов через наследование, объявляются в базовом классе как private. Производные классы могут изменить состояние закрытых данных базового класса, но только с помощью не закрытых, унаследованных методов базового класса. Не унаследованные методы производного класса не могут получить прямого доступа к private членам их базового класса.

using System;

//Определение класса точки неявно наследует все из класса Object

public class Point

{

// Данные:

private int x, y; // Координаты точки

// Методы:

public Point() // конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

x=100; y=100; // если явно не указать значения x и y, то они будут равны 0

}

public Point(int a, int b) // конструктор

{

// здесь происходит неявное обращение к конструктору Object

x=a; y=b;

}

// Открытые функции доступа к закрытым данным

public void SetX(int a){ if(a>=0) x=a; }

public int GetX(){ return x; }

public void SetY(int b){ if(b>=0) y=a; }

public int GetY(){ return y; }

// Функции общего назначения

public void Move(int dx, int dy) { x+=dx; y+=dy; }

}

//Определение класса окружности неявно наследует все из класса Object

public class Circle

{

// Данные:

private int x, y; // координаты центра

private double radius; // радиус окружности

// Методы:

public Circle()// конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

x=100; y=100; // если явно не указать значения x и у, то они будут равны 0

}

public Circle(int a, int b, double r) // конструктор

{

// здесь происходит неявное обращение к конструктору Object

x=a; y=b; radius=r;

}

// Открытые функции доступа к закрытым данным

public void SetRad(diuble rad){ radius=rad; }

public int GetRad(){ return radius; }

public void SetX(int a){ x=a; }

public int GetX(){ return x; }

public void SetY(int a){ y=a; }

public int GetY(){ return y; }

// Функции общего назначения

public void Move(int dx, int dy) { x+=dx; y+=dy; }

public double Area(){ … }

}

После описания класса окружности можно легко заметить, что большая часть его содержимого похожа, если не одинакова, на содержимое класса точки. Например, описание переменных x и y, конструкторы и функции public void SetX, GetX, SetY, GetY и Move. Единственными добавлениями к классу окружности являются переменная радиуса (radius) и функция вычисления занимаемой площади Area. Возникает впечатление, что описание класса точки было просто скопировано и вставлено в класс окружности, после чего класс окружности был расширен включением радиуса и функции вычисления площади. Подобный принцип "копирования и вставки" не защищен от возможных ошибок, требует много времени. И, что еще хуже, результатом его может стать чрезмерное количество физических копий существующего в системе программного кода, обслуживание и поддержка которого превращается в "кошмар" для его разработчиков.

Наследование, представляет собой способ "поглощения" характеристик и поведения одного класса таким образом, чтобы они автоматически становились частью других классов. Покажем это на следующем примере, в котором создадим класс окружности, наследующего переменные x и у и функции SetX, GetX, SetY, GetY и Move из класса точки. По сути дела новый класс окружности становится точной копией класса точки, в которую добавлена переменная радиуса и функция вычисления площади - Area. При этом, производный класс наследует от базового класса всё, что он имеет. Другое дело, что воспользоваться в производном классе можно не всем наследством.

class Circle2: Point

{

// Данные:

private double radius; // радиус окружности

// Методы:

public Circle2()// конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

// Ошибка! Доступ к закрытым данным базового класса запрещен!

//x=100; y=100;

radius =10;

}

public Circle2(int a, int b, double value) // конструктор

{

// здесь имеет место неявное обращение к конструктору класса Point (x=y=0)

// Ошибка! Доступ к закрытым данным базового класса запрещен!

//x=a; y=b;

radius=value;

}

public double Area(){ … }

}

Теперь класс окружности выглядит намного компактнее. Однако, после компиляции этого программного кода компилятор выдает сообщение об ошибке, которые он обнаруживает в теле конструкторов класса Circle2 с аргументами. Объясняется это тем, что в теле конструктора с аргументами делается попытка прямого использования значений x и y, принадлежащихбазовому классу. Дело в том, что производному классу не разрешен доступ к закрытым (private) элементам (x и у) базового класса Point. Эту ошибку легко исправить, если воспользоваться открытыми функциями доступа к закрытым переменным в базовом классе.

class Circle3: Point

{

// Данные:

private double radius; // радиус окружности

// Методы:

public Circle2()// конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

// если явно не указать значения x, у и radius, то они будут равны 0

SetX(100); SetY(100); // Ошибки уже нет!

radius=10;

}

public Circle2(int a, int b, double r) // конструктор

{

// здесь имеет место неявное обращение к конструктору класса Point (x=y=0)

SetX(a); SetY(b); // Ошибки уже нет!

radius=10;

}

public int GetRad(){ return radius; }

public void SetRad(double r){ if(r>0) radius=r; }

public double Area(){ … }

}