МегаПредмет

ПОЗНАВАТЕЛЬНОЕ

Сила воли ведет к действию, а позитивные действия формируют позитивное отношение


Как определить диапазон голоса - ваш вокал


Игровые автоматы с быстрым выводом


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


Целительная привычка


Как самому избавиться от обидчивости


Противоречивые взгляды на качества, присущие мужчинам


Тренинг уверенности в себе


Вкуснейший "Салат из свеклы с чесноком"


Натюрморт и его изобразительные возможности


Применение, как принимать мумие? Мумие для волос, лица, при переломах, при кровотечении и т.д.


Как научиться брать на себя ответственность


Зачем нужны границы в отношениях с детьми?


Световозвращающие элементы на детской одежде


Как победить свой возраст? Восемь уникальных способов, которые помогут достичь долголетия


Как слышать голос Бога


Классификация ожирения по ИМТ (ВОЗ)


Глава 3. Завет мужчины с женщиной


Оси и плоскости тела человека


Оси и плоскости тела человека - Тело человека состоит из определенных топографических частей и участков, в которых расположены органы, мышцы, сосуды, нервы и т.д.


Отёска стен и прирубка косяков Отёска стен и прирубка косяков - Когда на доме не достаёт окон и дверей, красивое высокое крыльцо ещё только в воображении, приходится подниматься с улицы в дом по трапу.


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

Работа с объектами через интерфейсы. Операции is и as





ИЕРАРХИЯ КЛАССОВ

 

Цель работы: на основе иерархии классов изучить механизм и работу наиболее подходящих средств для целей конкретного проекта.

Наследование классов

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

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

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

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

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

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

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

Описание класса-потомка

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

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

ПРИМЕЧАНИЕ

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

Рассмотрим наследование классов на примере. Ранее был описан класс Monster, моделирующий персонаж компьютерной игры. Допустим, нам требуется ввести в игру еще один тип персонажей, который должен обладать свойствами объекта Monster, а кроме того, уметь думать. Будет логично сделать новый объект потомком объекта Monster (листинг 1).

Листинг 1. Класс Daemon, потомок класса Monster

 

 

 

В классе Daemon введены закрытое поле brain и метод Think, определены собственные конструкторы, а также переопределен метод Passport. Все поля и свойства класса Monstr наследуются в классе Daemon.

Результат работы программы:

 

Экземпляр класса Daemon с одинаковой легкостью использует как собственные (операторы 5-7), так и унаследованные (оператор 8) элементы класса. Рассмотрим общие правила наследования.

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

· Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров.

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

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

Поля, методы и свойства класса наследуются, поэтому при желании заменить элемент базового класса новым элементом следует явным образом указать компилятору свое намерение с помощью ключевого слова new. В листинге 1 таким образом переопределен метод вывода информации об объекте Passport. Метод Passport класса Daemon замещает соответствующий метод базового класса, однако возможность доступа к методу базового класса из метода производного класса сохраняется. Для этого перед вызовом метода указывается все то же волшебное слово base, например:

base.Passport();

Элементы базового класса, определенные как private, в производном классе недоступны. Поэтому в методе Passport для доступа к полям name, health и ammo пришлось использовать соответствующие свойства базового класса. Другое решение заключается в том, чтобы определить эти поля со спецификатором protected, в этом случае они будут доступны методам всех классов, производных от Monster. Оба решения имеют свои достоинства и недостатки.

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

Опишем массив объектов базового класса и занесем туда объекты производного класса. В листинге 2 в массиве типа Monster хранятся два объекта типа Monster и один – типа Daemon.

Листинг 2. Массив объектов разных типов

Результат работы программы:

Результат радует нас только частично: объект типа Daemon действительно можно поместить в массив, состоящий из элементов типа Monster, но для него вызываются только методы и свойства, унаследованные от предка. Это устраивает нас в операторе 2, а в операторах 1 и 3 хотелось бы, чтобы вызывался метод Passport, переопределенный в потомке.

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

Это и понятно: ведь компилятор должен еще до выполнения программы решить, какой метод вызывать, и вставить в код фрагмент, передающий управление на этот метод (этот процесс называется ранним связыванием). При этом компилятор может руководствоваться только типом переменной, для которой вызывается метод или свойство (например, stado[i].Ammo). То, что в этой переменной в разные моменты времени могут находиться ссылки на объекты разных типов, компилятор учесть не может.

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

 

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

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

Следовательно, надо каким-то образом дать знать компилятору, что эти методы будут обрабатываться по-другому. Для этого в C# существует ключевое слово virtual. Оно записывается в заголовке метода базового класса, например:

virtual public void Passport() ...

Объявление метода виртуальным означает, что все ссылки на этот метод будут разрешаться в момент его вызова во время выполнения программы. Этот механизм называется поздним связыванием.

Для его реализации необходимо, чтобы адреса виртуальных методов хранились там, где ими можно будет в любой момент воспользоваться, поэтому компилятор формирует для этих методов таблицу виртуальных методов (Virtual Method Table, VMT). В нее записываются адреса виртуальных методов (в том числе унаследованных) в порядке описания в классе. Для каждого класса создается одна таблица.

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

Если в производном классе требуется переопределить виртуальный метод, используется ключевое слово override, например:

override public void Passport() ...

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

Добавим в листинг 2 два слова – virtual и override – в описания методов Passport соответственно базового и производного классов (листинг 3).

Листинг 3. Виртуальные методы

 

Результат работы программы:

Теперь в циклах 1 и 3 вызывается метод Passport, соответствующий типу объекта, помещенного в массив.

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

С помощью виртуальных методов реализуется один из основных принципов объектно-ориентированного программирования – полиморфизм. Это слово в переводе с греческого означает «много форм», что в данном случае означает «один вызов – много методов».

Виртуальные методы незаменимы и при передаче объектов в методы в качестве параметров. В параметрах метода описывается объект базового типа, а при вызове в нее передается объект производного класса. Виртуальные методы, вызываемые для объекта из метода, будут соответствовать типу аргумента, а не параметра.

 

Абстрактные классы

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

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

Абстрактный класс задает интерфейс для всей иерархии, при этом методам класса может не соответствовать никаких конкретных действий. В этом случае методы имеют пустое тело и объявляются со спецификатором abstract.

Если в классе есть хотя бы один абстрактный метод, весь класс также должен быть описан как абстрактный, например:

abstract class Spirit

{

public abstract void Passport();

}

class Monster : Spirit

{

...

override public void Passport()

{

Console.WriteLine( "Monster {0} \t health = {1} ammo = {2}",

name, health, ammo );

}

...

}

 

class Daemon : Monster

{

...

override public void Passport()

{

Console.WriteLine(

"Daemon {0} \t health = {1} ammo = {2} brain = {3}",

Name, Health, Ammo, brain );

}

}

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

 

Бесплодные классы

В C# есть ключевое слово sealed, позволяющее описать класс, от которого, в противоположность абстрактному, наследовать запрещается:

sealed class Spirit

{

...

}

// class Monster : Spirit { ... } ошибка!

 

Большинство встроенных типов данных описано как sealed. Если необходимо использовать функциональность бесплодного класса, применяется не наследование, а вложение, или включение: в классе описывается поле соответствующего типа.

Вложение классов, когда один класс включает в себя поля, являющиеся классами, является альтернативой наследованию при проектировании. Например, если есть объект «двигатель», а требуется описать объект «самолет», логично сделать двигатель полем этого объекта, а не его предком.

 

Класс object

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

Класс object часто используется и непосредственно при описании типа параметров методов для придания им общности, а также для хранения ссылок на объекты различного типа – таким образом реализуется полиморфизм.

Открытые методы класса System.Object перечислены ниже.

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

public virtual bool Equals(object obj);

· Метод Equals с двумя параметрами возвращает значение true, если оба параметра ссылаются на одну и ту же область памяти. Синтаксис:

public static bool Equals(object ob1, object ob2);

· Метод GetHashCode формирует хэш-код объекта и возвращает число, однозначно идентифицирующее объект. Это число используется в различных структурах и алгоритмах библиотеки. Если переопределяется метод Equals, необходимо перегрузить и метод GetHashCode. Синтаксис:

public virtual int GetHashCode();

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

public Type GetType();

· Метод ReferenceEquals возвращает значение true, если оба параметра ссылаются на одну и ту же область памяти. Синтаксис:

public static bool(object ob1, object ob2);

· Метод ToString по умолчанию возвращает для ссылочных типов полное имя класса в виде строки, а для значимих – значение величины, преобразованное в строку. Этот метод переопределяют для того, чтобы можно было выводить информацию о состоянии объекта. Синтаксис:

public virtual string ToString()

В производных объектах эти методы часто переопределяют. Например, можно переопределить метод Equals для того, чтобы задать собственные критерии сравнения объектов.

Пример применения и переопределения методов класса object для класса Monster приведен в листинге 4.

Листинг 4. Перегрузка методов класса object

Результат работы программы:

Анализируя результат работы программы, можно увидеть, что в операции сравнения на равенство сравниваваются ссылки, а в перегруженном методе Equals – значения. Для концептуального единства можно переопределить и операции отношения.

 

Интерфейсы

Синтаксис интерфейса

Интерфейс является «крайним случаем» абстрактного класса. В нем задается набор абстрактных методов, свойств и индексаторов, которые должны быть реализованы в производных классах. Иными словами, интерфейс определяет поведение, которое поддерживается реализующими этот интерфейс классами. Основная идея использования интерфейса состоит в том, чтобы к объектам таких классов можно было обращаться одинаковым образом.

Каждый класс может определять элементы интерфейса по-своему. Так достигается полиморфизм: объекты разных классов по-разному реагируют на вызовы одного и того же метода.

Синтаксис интерфейса аналогичен синтаксису класса:

[ атрибуты ] [ спецификаторы ] interface имя_интерфейса [ : предки ] тело_интерфейса [ ; ]

Для интерфейса могут быть указаны спецификаторы new, public, protected, internal и private. Спецификатор new применяется для вложенных интерфейсов и имеет такой же смысл, как и соответствующий модификатор метода класса. Остальные спецификаторы управляют видимостью интерфейса. По умолчанию интерфейс доступен только из сборки, в которой он описан (internal).

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

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

interface IAction

{

void Draw();

int Attack(int a);

void Die();

int Power { get; }

}

В интерфейсе IAction заданы заголовки трех методов и шаблон свойства Power, доступного только для чтения. Если бы требовалось обеспечить возможность установки свойства, в шаблоне следовало указать ключевое слово set, например:

int Power { get; set; }

 

Отличия интерфейса от абстрактного класса:

· элементы интерфейса по умолчанию имеют спецификатор доступа public и не могут иметь спецификаторов, заданных явным образом;

· интерфейс не может содержать полей и обычных методов – все элементы интерфейса должны быть абстрактными;

· класс, в списке предков которого задается интерфейс, должен определять все его элементы, в то время как потомок абстрактного класса может не переопределять часть абстрактных методов предка (в этом случае производный класс также будет абстрактным);

· класс может иметь в списке предков несколько интерфейсов, при этом он должен определять все их методы.

 

Реализация интерфейса

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

using System;

namespace ConsoleApplication1

{

interface IAction

{

void Draw();

int Attack( int a );

void Die();

int Power { get; }

}

 

class Monster : IAction

{

public void Draw()

{

Console.WriteLine( "Здесь был " + name );

 

}

public int Attack( int ammo_ )

{

ammo -= ammo_;

if ( ammo > 0 ) Console.WriteLine( "Ба-бах!" );

else ammo = 0;

return ammo;

}

 

public void Die()

{

Console.WriteLine( "Monster " + name + " RIP" );

health = 0;

}

 

public int Power

{

get

{

return ammo * health;

}

}

 

}

Сигнатуры методов в интерфейсе и реализации должны полностью совпадать. Для реализуемых элементов интерфейса в классе следует указывать спецификатор public. К этим элементам можно обращаться как через объект класса, так и через объект типа соответствующего интерфейса:

Monster Vasia = new Monster( 50, 50, "Вася" ); // объект класса Monster

Vasia.Draw(); // результат: Здесь был Вася

IAction Actor = new Monster( 10, 10, "Маша" ); // объект типа интерфейса

Actor.Draw(); // результат: Здесь был Маша

 

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

class Monster : IAction

{

int IAction.Power

{

get

{

return ammo * health;

}

}

 

void IAction.Draw()

{

Console.WriteLine( "Здесь был " + name );

}

...

}

...

IAction Actor = new Monster( 10, 10, "Маша" );

Actor.Draw(); // обращение через объект типа интерфейса

 

// Monster Vasia = new Monster( 50, 50, "Вася" );

// Vasia.Draw(); ошибка!

 

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

 

Работа с объектами через интерфейсы. Операции is и as

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

Результат операции равен true, если объект можно преобразовать к заданному типу, и false в противном случае. Операция обычно используется в следующем контексте:

if ( объект is тип )

{

// выполнить преобразование "объекта" к "типу"

// выполнить действия с преобразованным объектом

}

 

Допустим, мы оформили какие-то действия с объектами в виде метода с параметром типа object. Прежде чем использовать этот параметр внутри метода для обращения к методам, описанным в производных классах, требуется выполнить преобразование к производному классу. Для безопасного преобразования следует проверить, возможно ли оно, например так:

static void Act( object A )

{

if ( A is IAction )

{

IAction Actor = (IAction) A;

Actor.Draw();

}

}

В метод Act можно передавать любые объекты, но на экран будут выведены только те, которые поддерживают интерфейс IAction.

Недостатком использования операции is является то, что преобразование фактически выполняется дважды: при проверке и при собственно преобразовании. Более эффективной является другая операція – as. Она выполняет преобразование к заданному типу, а если это невозможно, формирует результат null, например:

static void Act( object A )

{

IAction Actor = A as IAction;

if ( Actor != null ) Actor.Draw();

}

Обе рассмотренные операции применяются как к интерфейсам, так и к классам.

 

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

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

Как и в обычной иерархии классов, базовые интерфейсы определяют общее поведение, а их потомки конкретизируют и дополняют его. В интерфейсе-потомке можно также указать элементы, переопределяющие унаследованные элементы с такой же сигнатурой. В этом случае перед элементом указывается ключевое слово new, как и в аналогичной ситуации в классах. С помощью этого слова соответствующий элемент базового интерфейса скрывается. Класс, реализующий интерфейс, должен определять все его элементы, в том числе унаследованные.

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

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

interface IBase

{

void A();

}

 

class Base : IBase

{

public void A() { ... }

}

 

class Derived: Base

{

new public void A() { ... }

}

 

...

Derived d = new Derived ();

d.A(); // вызывается Derived.A();

IBase id = d;

id.A(); // вызывается Base.A();

 

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

Существует возможность повторно реализовать интерфейс, указав его имя в списке предков класса наряду с классом-предком, уже реализовавшим этот интерфейс. При этом реализация переопределенных методов базового класса во внимание не принимается:

interface IBase

{

void A();

}

 

class Base : IBase

{

void IBase.A() { ... } // не используется в Derived

}

 

class Derived : Base, IBase

{

public void A() { ... }

}

 

 

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

 





©2015 www.megapredmet.ru Все права принадлежат авторам размещенных материалов.