Предпосылки появления наследования

Предположим, что в программе требуется создать объект с атрибутами и функцио­нальностью спортивный автомобиль, вседорожник,. Без наследования это можно сделать двумя спосо­бами:

1. Все три типа объектов можно рассматривать как автомобили и сделать все атри­буты общими. Таким образом, все три типа автомобилей будут экземплярами од­ного класса.

2. Можно поступить наоборот — создать три класса.

Оба подхода можно заставить работать, однако у каждого из них есть серьезные не­достатки.

Предпосылки появления наследования - student2.ru Предпосылки появления наследования - student2.ru Рассмотрим первый подход. Графически он представлен на рис.10.2.

       
  Предпосылки появления наследования - student2.ru
   
Дополнительные атрибуты и действия характерные для внедорожника
 

Рассмотрим основные недостатки первого подхода:

1. Трудно найти подходящее имя для класса. Проблемы начинаются с поиска имени. Класс включает не просто усредненные ав­томобили, поэтому имя «Автомобиль»не очень подходит.

2. Необходима переменная экземпляра, которая указывает, автомобиль какого типа представляет объект. Нужен способ узнать, какого типа автомобиль представляет конкретный объект.

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

4. Функции-члены, которые используются только в одном из трех типов автомо­билей, напрасно расходуют память, загромождают код и являются источником ошибок.

5. Функции-члены с одним и тем же именем в трех автомобилях имеют разные реализации, поэтому должны содержать конструкцию if...else для выполнения фрагмента кода в зависимости от того, для какого объекта вызывается метод.

Рассмотрим преимущества, которые возникают при использовании данного подхода.

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

2. Функции-члены всех трех типов автомобилей содержатся в одном и том же месте, поэтому их не нужно копировать и легко модифицировать.

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

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

Предпосылки появления наследования - student2.ru

Рассмотрим основные преимущества и недостатки данного подхода.

Основной недостаток: атрибуты и действия, принадлежащие обобщенному классу автомобилей, дол­жны присутствовать во всех классах. Если программисту требуется изменить или добавить метод присущий свеем автомобилям, то ему придется внести изменения во все классы.

Основное преимущество: уникальные атрибуты и действия содержатся в каждом классе.

Атрибуты и действия, присущие только одному из трех классов встроены в свои классы и не пересекаются с дру­гими.

Обобщив сказанное можно сделать вывод, что два подхода приводят к следующей дилемме: элементы класса, общие для всех трех категорий автомобилей, хорошо обрабатываются при первом подходе, но вызыва­ют проблемы при втором. А элементы, уникальные для каждой категории автомобилей, вызывают проблемы при первом подходе, но естественным образом обрабатываются при втором.

Описанные выше проблемы можно решить с использованием третьего подхода называемого наследованием. Графически этот подход изображен ниже.

Предпосылки появления наследования - student2.ru

Рассмотрим подробно последовательность действий, которые реализуют представленный рисунок. Вначале нужно создать класс «Автомобиль» с общими элементами, которые характерны для автомобилей разных видов. Затем два специализированных класса: «Внедорожник» и «Спортивный»,содержа­щие только характеристики, уникальные для каждого типа автомобилей. Самое главное – создать механизм реализующий такую идею: помимо уникальных элементов каж­дый из специализированных классов должен содержать элементы класса «Автомобиль».Таким образом, требуется создать два новых класса, которые расширяют существующий класс «Автомобиль».При этом два производныхкласса наследуют свойства класса «Автомобиль»и в них добавля­ются уникальные свойства.

Механизмнаследования обеспечивает реализацию описанного третьего подхода. На­следование используется для создания объектов, имеющих как общие, так и особые свой­ства. Когда класс «Спортивный»наследует элементы «Автомобиль», говорят, что класс «Спортивный»— про­изводный от «Автомобиль»,или «Спортивный»— прямой подкласс класса «Автомобиль». Класс «Автомобиль» называется базовым (а также подклассом или родительским классом) класса «Спортивный»,поскольку он служит основой для «Спортивный».

Синтаксис наследования.

Синтаксис для получения производного класса из базового показан в синтаксичес­ком блоке В заголовке производного класса указаны все необязательные специфи­каторы, за которыми следует ключевое слово classи имякласса. Важно при этом двое­точие (:), за которым следует имя базового класса. Его смысл эквивалентен "является производным от" или "наследует от".

Определение_класса_с_необязательным_наследованием::= [<Спецификатор_доступности>] class <Имя_производного_класса> [ : <Имя_базового_класса>]

{

<Элементы_производного_класса>

}

using System;

class Student

{

private string name;

private string surname;

private byte age=0;

public string Name

{

get

{

return name;

}

set

{

name=value;

}

}

public string Surname

{

get

{

return surname;

}

set

{

surname=value;

}

}

public byte Age

{

get

{

return age;

}

set

{

age=value;

}

}

class GraduateStudent:Student

{

private byte min;

public byte GraduateStudentMIN

{

get

{

return min;

}

set

{

min=value;

}

}

}

class Test

{

public static void Main()

{

Student Student1=new Student();

GraduateStudent GraduateStudent1=new GraduateStudent();

Student1.Name="Джон";

Student1.Surname="Матвеев";

Student1.Age=20;

Console.WriteLine("Имя: {0} Фамилия: {1} Возраст: {2}", Student1.Name, Student1.Surname, Student1.Age);

GraduateStudent1.Name="Алексей";

GraduateStudent1.Surname="Лосев";

GraduateStudent1.Age=26;

GraduateStudent1.GraduateStudentMIN=14;

Console.WriteLine("Имя: {0} Фамилия: {1} Возраст: {2} Кандидатский минимум: {3}", GraduateStudent1.Name, GraduateStudent1.Surname, GraduateStudent1.Age, GraduateStudent1.GraduateStudentMIN);

Console.ReadLine();

}

} }

Типы наследования

Сущест­вуют два различных типа наследования: наследование реализации и наследование интерфейса.

Наследование реализации(implementation inheritance) означает, что тип происходит от базового типа, получая от него все поля-члены и функции. При наследовании реализации производный тип адаптирует реализацию каждой функции базового типа, если только в его определении не указано, что реали­зация функции должна быть переопределена. Такой тип наследования более полезен, когда нужно добавить функциональность существующему типу, или же когда несколько связанных типов разделяют существенный объем общей функциональности. Хороший пример — набор классов Windows Forms, наряду с базовым классом System.Windows.Forms.Control, который представляет очень сложную реализацию общего элемента управления Windows, и многочисленными другими классами, такими как System.Windows.Forms.TextBox и System.Windows.Forms.ListBox, унас­ледованными от Control и переопределяющими его функции или опреде­ляющими новые функции для реализации специальных элементов управления.

Наследование интерфейса (interface inheritance) означает, что тип наследует только сигнатуру функций, но не наследует никакой реализации. Этот тип на­следования наиболее полезен, когда нужно специфицировать, что тип обеспе­чивает доступ к определенным средствам. Например, для некоторых типов может быть указано, что они представляют метод очистки ресурсов, называе­мый Dispose(), за счет того, что наследуют интерфейс System.Idisposable. Поскольку способ, которым один тип очищает ресурсы, скорее всего, будет очень отличаться от способа очистки ресурсов, используемого другим типом, нет смысла определять никакой общей реализации, а потому в данном случае подходит наследование интерфейса. Наследование интерфейса часто трактуется как выполнение контракта: наследуя интерфейс, тип берет на себя обязанность представить клиентам определенную функциональность.

Множественное наследование

Некоторые языки, такие как C++, поддерживают то, что известно под названием множественного наследования, когда класс происходит более чем от одного базового класса. Преимущества множественного наследования спорны. С одной стороны, нет сомнений, что можно применять множественное наследование для написания сложного, но при этом компактного кода. С другой стороны, код, использующий множественное наследование, часто сложно понять и сложно отлаживать. Как уже упоминалось, облегчение написания устойчивого кода было одной из ключевой целей проектирования С#. Соответственно, поэтому С# не поддерживает множественного наследования. Однако он позволяет типу наследовать множество интерфейсов. Это значит, что класс С# может наследоваться от другого класса и любого количества интерфейсов. На самом деле можно сказать точнее: благодаря налию System.Object как всеобщего базового типа, каждый класс С# (за исключением Object) имеет исключительно один базовый класс и дополнительно может иметь любое количество базовых интерфейсов.

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

· Структуры всегда наследуются от System.ValueType. Они могут также наследовать любое количество интерфейсов.

· Классы всегда наследуются от одного класса по нашему выбору. Они также могут наследовать любое количество интерфейсов.

Наследование интерфейса

Если необходимо объявить, что класс наследуется от другого класса, необходимо указать следующий синтаксис:

class УнаследованныйКласс : БазовыйКласс

{

//данные и функции-члены

}

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

Если класс (или структура) также наследует интерфейсы, то список базового класса и интерфейсов разделяется запятыми:

public class MyDerivedClass:MyBaseClass, Interfacel, Intexrface2

{

//и так далее.

}

Для структур синтаксис такой:

public struct MyDerivedStruct:Interfacel, Interface2

{

// и так далее.

}

Если при определении класса не указывается базовый класс, то компилятор С# предполагает, что базовым классом является System.Object. Поэтому следующие два фрагмента кода эквивалентны:

class MyClass : Object // наследуется от System.Object

{

//и так далее.

}

class MyClass // наследуется от System. Object

{

// и так далее.

}

Для простоты чаще применяется вторая форма.

Поскольку С# поддерживает ключевое слово object, служащее псевдонимом System.Object, можно также записать так:

class MyClass : object // наследуется от System.Object

{

// и так далее.

}

Если нужно сослаться на класс Object, используйте ключевое слово object, которое распознается интеллектуальными редакторами, такими как Visual Studio .NET. Это облегчит редактирование вашего кода.

Виртуальные методы

Объявляя функцию базового класса как virtual, тем самым вы позволяете ее переопределять в классах-наследниках:

Допускается объявление свойства как virtual. Для виртуального или пере­менного свойства используется такой же синтаксис, что и для невиртуального за исключением ключевого слова virtual, добавляемого к определению.

Для простоты далее речь пойдет в основном о методах, хотя все это касается также свойств. Концепция лежащая в основе виртуальных функций С#, идентична стандартной концепции объектно-ориентированного программирования (ООП). Вы можете переопределить виртуальную функцию в классе-наследнике, и когда этот метод буде-вызван, то запустится его версия, относящаяся к соответствующему типу объекта. С# по умолчанию функции не виртуальные, но (в отличие от конструкторов) могут быть явно объявлены как virtual. Это следует методологии C++: по причинам производительности функции не виртуальные, если это не указано явно. В отличие от этого, в Java все функции виртуальные. С# имеет отличающийся от C++ синтаксис поскольку требует явного объявления, когда функция класса-наследника переопреде­ляет другую функцию, с помощью ключевого слова overridde.

Этот синтаксис переопределения метода исключает потенциальные ошибки вре­мени выполнения, которые могут легко возникать в C++, когда сигнатура метода вклассе-наследнике нечаянно оказывается отличной от базовой версии, в результате чего метод наследника не может переопределить базовый. В С# это всплывает в виде ошибки компиляции, поскольку компилятор легко обнаруживает метод, для которого указан модификатор override, но при этом не имеющий базового метода, который он переопределяет.

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

Синтаксический блок ::=

class <Идентификатор_базового_класса>

{

<Спецификатор_доступности> virtual <Тип__возвр_знач> <Идентификатор_метода> ([<Список_формальных параметров>])

{

< Операторы >

}

}

class <Идентификатор_производного_класса>

{

<Спецификатор_доступности> override <Тип_возвр_знач>

<Идентификатор_метода> ([<Список_формальных_парам>])

{

< Операторы >

}

}

<Спецификатор_доступности>::=

public

protected

internal

protected internal

Примечания.

1. Виртуальный метод не может быть объявлен как private.

2. Если метод в производном классе переопределяет метод базового класса, он дол­жен иметь то же имя и параметры (число и последовательность типов должны со­впадать), что и метод базового класса.

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

4. Кроме нестатических методов, как virtualмогут быть объявлены только нестати­ческие свойства и нестатические индексаторы. Никакие другие функции-члены не мо­гут быть виртуальными, а, значит, не могут быть переопределены.

class class1

{

public int a, b;

public class1()

{

Console.WriteLine("ВВедите числа");

a = Convert.ToInt32(Console.ReadLine());

b = Convert.ToInt32(Console.ReadLine());

}

public virtual void sum(int c, int d)

{

int q = c + d;

Console.WriteLine("Сумма равна" + q);

}

}

class class2 : class1

{

public override void sum(int c, int d)

{

int q = c + 2 * d;

Console.WriteLine("Сумма равна" + q);

}

}

class Program

{

static void Main(string[] args)

{

class1 myClass1 = new class1();

class2 myClass2 = new class2();

myClass1.sum(myClass1.a, myClass1.b);

myClass2.sum(myClass2.a, myClass2.b);

Console.ReadLine();

}

}

Спецификаторы доступности

Спецификаторы доступности особенно важны для наследования. На данный момент мы сталкивались со спецификаторами доступности private и public. Далее рассмотрим несколько новых спецификаторов доступности:

· internal – доступ к элементу возможен только из сборки в которой он объявлен;

· protected – элемент класса доступен из класса, в котором он объявлен и его производных классов.

· protected-internal – элемент класса обладающий этим спецификатором доступен только из того класса, где он объявлен, из производного класса или из сборки в которой он определен.

Согласно принципу инкапсуляции все переменные экземпляра должны быть объявлены как private. Для работы с ними используются свойства, который в базовом классе объявляются как protected.

class class1

{

private int a=0;

protected int A

{

set

{

a = value;

}

get

{

return a;

}

}

}

class class2: class1

{

public void get()

{

A = 20;

Console.WriteLine(A);

}

}

}

class Program

{

static void Main(string[] args)

{

class2 my_class2 = new class2();

my_class2.get();

Console.ReadLine();

}

}

Наши рекомендации