русс | укр

Языки программирования

ПаскальСиАссемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование

Все о программировании


Linux Unix Алгоритмические языки Аналоговые и гибридные вычислительные устройства Архитектура микроконтроллеров Введение в разработку распределенных информационных систем Введение в численные методы Дискретная математика Информационное обслуживание пользователей Информация и моделирование в управлении производством Компьютерная графика Математическое и компьютерное моделирование Моделирование Нейрокомпьютеры Проектирование программ диагностики компьютерных систем и сетей Проектирование системных программ Системы счисления Теория статистики Теория оптимизации Уроки AutoCAD 3D Уроки базы данных Access Уроки Orcad Цифровые автоматы Шпаргалки по компьютеру Шпаргалки по программированию Экспертные системы Элементы теории информации

Системы программирования и Объектно-ориентированное программирование


Дата добавления: 2014-04-26; просмотров: 2503; Нарушение авторских прав


ст. преп. каф. 806 Беликов С.В.

 

Москва 2013.

 

 


Содержание

Содержание. 2

Введение. 4

Платформа .NET и язык C#. 5

Платформа .NET. 5

Язык C# 9

Общая информация. 9

Управляющие конструкции. 9

Система типов. 10

Объектно-ориентированное программирование. 12

Сложные системы. 12

Способы декомпозиции. 13

Объектно-ориентированный подход. 14

Классы и объекты. 16

Программа Hello world! 18

Классы и объекты в C#. 19

Конструкторы и деструкторы. 22

Инкапсуляция. 23

Свойства. 24

Перегрузка методов. 27

Способы передачи параметров. 28

Наследование. 29

Наследование. 29

Конструкторы и деструкторы при наследовании. 32

Иерархия классов и иерархия объектов. 34

Переопределение атрибутов и методов. 37

Пример наследования. 39

Полиморфизм. Виртуальные функции. 43

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

Интерфейсы. 48

Варианты полиморфизма. 51

Операторы is и as. 52

Статические атрибуты и методы. 53

Паттерн синглтон (singleton) 54

Класс object 55

Упаковка и распаковка. 56

Строки 57

Вложенные классы. 59

Ссылка this. 61

Пример 62

Структуры. 71

Делегаты. 73

Анонимные делегаты. 75

Паттерн команда (Command) 76

События 78

Обобщенные классы. 80

Коллекции. 82

Интерфейс IComparable. 84

Массивы 86

Обобщенные методы и делегаты. 87

Обнуляемые типы. 87

Методы-расширения (extension methods) 88

Другие механизмы ООП. 90

Обработка ошибок. 91

Исключения (exceptions) 91

Общие принципы обработки ошибок. 94

Некоторые возможности платформы .NET. 95

Работа с файловой системой. 95

Чтение из файла и запись в файл. 96

Работа с изолированным хранилищем. 97

Общие принципы программирования. 98

Стандарты кодирования. 98

Комментарии. 100

Гибкие методологии разработки. 101

Традиционный подход к разработке. 101



Гибкие подходы к разработке. 101

Опасности гибких методологий. 102

Другие применения идей гибких методологий. 103

Рефакторинг 104

Модульные тесты. 106

Разработка через тестирование. 108

Принцип ортогональности. 109

Метапрограммирование. 110

Язык XML. 111

Возникновение XML. 111

XML 111

Таблицы стилей XSL. 113

Программная обработка XML в C#. 116

XPath 119

Схемы XSD.. 119

Сериализация в XML. 121

Теория компиляции.. 124

Введение в компиляцию.. 124

Формальные грамматики. 126

Регулярные выражения. 128

Конечные автоматы. 129

Cвязь между конечными автоматами, регулярными выражениями и регулярными грамматиками. 131

Применение конечных автоматов для создания интерпретаторов. 132

Конечные автоматы и ООП. 135

 


Введение

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

Язык программирования – это всего лишь инструмент. Уметь им пользоваться – это важно, однако гораздо важнее понимать общие принципы создания программных систем, такие как объектно-ориентированное проектирование, а также владеть методами, позволяющими писать хорошие программы на любом языке, такими как рефакторинг и модульное тестирование.

Цель данного курса – научить вас программировать в широком смысле этого слова, т.е. создавать качественное программное обеспечение.


Платформа .NET и язык C#

Платформа .NET

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

Для начала – краткая история развития этой платформы:

2002 год –Появилась .NET Framework v.1.0

2003 год –.NET Framework v.1.1 и среда разработки приложений под эту платформу - Microsoft Visual Studio 2003.

2005 год – .NET Framework v. 2.0 и Microsoft Visual Studio 2005.

2006 год – .NET Framework v.3.0 (добавлены WCF (Windows Communication Foundation), WPF (Windows Presentation Foundation), WWF (Windows Workflow Foundation) и WCS (Windows Card Space).

2008 год -.NET Framework v.3.5 и Microsoft Visual Studio 2008

2010 год -.NET Framework v.4.0 и Microsoft Visual Studio 2010 (существенные изменения).

2012 год - .NET Framework 4.5 и Microsoft Visual Studio 2012

 

Что же такое .NET? Это целый набор технологий, средств, языков и стандартов позволяющий разрабатывать и выполнять приложения в этой среде. Основным отличием от традиционных компиляторов и средств разработки заключается в том, что программа в платформе .NET компилируется не в машинный код для некоторого процессора, а транслируется в программу на специальном промежуточном языке IL (Intermediate Language или, более полно, Microsoft Intermediate Language MSIL). Эта программа затем исполняется средой исполнения .NET CLR (Common Language Runtime). Это очень напоминает язык Java, промежуточный байт-код, в который компилируются программы на этом языке, и Java-машину, которая выполняет этот код.

Для платформы .NET существует множество компиляторов, которые переводят программу на основном языке в программу на языке IL. Существует специальный стандарт, которому должны удовлетворять языки платформы .NET – CLS (Common Language Specification). Благодаря этому стандарту, а также общей системе типов CTS (Common Type System) .NET поддерживает межязыковое взаимодействие. Т.е. в одной программе можно совместно использовать несколько языков программирования (поддерживаемых средой .NET). Всем типам данных используемых в языках .NET соответствуют типы данных в CTS.

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

На рис. 1. показана общая схема работы приложений в среде .NET


C#
VB.NET
Eiffel
Managed C++
IL
+
метаданные
OC
и
внешние
ресурсы
CLR
Сборка
(Assembly)
CLS, CTS

 


Рис. 1. Компиляция и выполнение приложений в среде .NET

Сначала программа на исходном языке (C#, VB.NET и т.п.) транслируется в программу на языке IL и метаданные, которые содержат информацию о версии, описания типов, ссылки на библиотеки и другую информацию, используемую средой исполнения. Типы описываются с помощью языка IDL (Interface Definition Language).

IL и метаданные собираются в так называемую сборку (assembly) – основную программную единицу в среде .NET. Часть сборки, в которой хранятся метаданные, называется манифестом. Также в сборку могут входить различные ресурсы приложения (иконки, картинки, аудиофайлы и т.п.) Физически сборка может представлять собой исполняемый файл (.exe) или библиотеку (.dll). Сборка является самодостаточной, т.е. содержет в себе все, что необходимо для ее исполнения. В отличие от COM-объектов, сборки не требуют регистрации в реестре windows. Для идентификации сборок используется специальная система ключей.

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

В отличие от байт-кода Java, код сборки не интерпретируется средой исполнения. Вместо этого используется так называемая JIT (Just In Time) компиляция – участки кода, к которым происходит обращение в процессе выполнения программы, компилируются и сохраняются в кэше. При повторном обращении к этим же участком кода они повторно не компилируются. Также, поскольку компиляция производится на том же компьютере где и выполнение программы, появляется возможность оптимизировать код под программное и аппаратное обеспечение данного компьютера.

Рассмотрим основные преимущества и недостатки, которые дает такой подход к разработке приложений:


Преимущества:

1. Цельная объектно-ориентированная модель программирования

Частью платформы .NET является богатая библиотека классов. Также сам язык IL является объектно ориентированным.

2. Автоматическое управление ресурсами

В первую очередь – памятью. В .NET подобно Java используется сборщик мусора, который автоматически освобождает динамическую память.

3. Многоплатформенность

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

4. Межъязыковое взаимодействие

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

5. Упрощение развертывания приложений

Самоописываемость сборок позволяет развертывать приложения простым копированием файлов, без регистрации в реестре, прописывании общих библиотек в специально отведенных папках и т.п. действий присущих традиционным приложениям. Единственное, что необходимо – это .NET Framework.

6. Современная модель безопасности

.NET обладает мощной системой безопасности.

7. Средства взаимодействия (interoperability)

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

8. Хорошая поддержка XML

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

9. Единая среда разработки - Visual Studio .NET

Для всех языков .NET существует единая среда разработки, что позволяет легко переключаться с разработки приложения на разных языках.

10. Единая платформа для всего

Зная один из языков платформы .NET (например, C#) вы сможете создавать обычные windows-приложения, распределенные приложения с использованием веб-служб, .NET Remoting или WCF, хранимые процедуры для современных СУБД, веб-приложения (ASP.NET), офисные расширения (add-ins) используя VSTO (Visual Studio Tools For Office).

11. Механизмы рефлексии

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

12. Хорошие возможности разработки пользовательского интерфейса

Для настольных приложений .NET включает WinForms – удобную библиотеку классов для разработки пользовательских интерфейсов со множеством элементов управления. Создать свои элементы управления также очень просто. Для ASP.NET существует WebForms – библиоткека для веб-приложений, а с 2007 года появилась поддержка AJAX. Все это поддерживается средой разработки. В .NET 3.0 появляется WPF (Windows Presentation Foundation), где интерфейс его описывается отделено от кода на языке XAML, благодаря чему можно разделить работу дизайнеров и программистов.

13. Множество сопутствующих технологий

ADO.NET – удобные средства доступа к базам данных, .NET Remoting и пришедший ему на смену WCF (кодовое имя Indigo) – средства для создания распределенных приложений, ASP.NET – разработка веб-сайтов, ClickOnce – система развертывания и обновления приложений и множество других технологий.

 

Недостатки:

1. Производительность

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

2. Защита исходных кодов

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

3. Необходимость установки .NET Framework

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

4. Некоторые ограничения на используемые языки

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


Язык C#. Общая информация

C# (читается как “си шарп”) - мощный объектно-ориентированный язык программирования, разработанный компанией Microsoft, набирающий все большую популярность у разработчиков. Он вобрал в себя лучшее из C++ и Java.

Основные особенности C#:

· Богатые возможности объектно-ориентированного программирования

Особенно в .NET 2.0, где появились обобщенные классы (generics) и другие улучшения, которые будут рассмотрены далее.

· Чисто объектно-ориентированный язык

В отличие от С++, где можно писать обычные процедуры и функции, в C# можно писать только с использованием методов ООП (классов, объектов, интерфейсов и т.д.).

· Автоматическое управление памятью

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

· Хорошая модульность

Как уже отмечалось выше, сборки представляют собой большой шаг вперед в модульности приложений. Внутри сборок мощным средством являются пространства имен (namespace) которые позволяют, в отличие от директивы #include в C++, намного проще и удобнее группировать классы и ссылаться на них (например, отсутствует проблема с циклическим подключением модулей, когда модулю A нужны классы из модуля B, B из С, а C из A).

· Строгая типизация

Контроль типов в C# более строгий, чем C++, что позволяет избежать множества ошибок с неявным приведением типов.

· Разработан специально для платформы .NET

Благодаря этому он может использовать возможности .NET по-максимуму. Хотя в .NET есть C++ (Managed C++), он менее удобен, чем C#.

Управляющие конструкции

Синтаксис управляющих конструкций практически полностью аналогичен C++.

В отличие от C++ переменная счетчик, объявленная в цикле for не видна за пределами цикла.

В конструкции switch можно использовать переменные строкового типа. Также во всех блоках case обязательно должен использоваться break или return.


Система типов

Поскольку язык C# разрабатывался специально для платформы .NET, то его система типов полностью соответствует системе типов платформы .NET (CTS – Common Type System).

Все типы в C# делятся на 4 категории:

1. Типы-значения или значимые типы (value type)

Характеризуются тем, что:

· Хранят свои значения в стеке

· При присваивании, передаче в функции (по значению) и возврате значения из функции происходит копирование значения

· При выходе из области видимости уничтожаются

К значимым типам относятся следующие типы: логический (bool), арифметические типы (int, double и т.д.), структуры (struct) и перечисления (enum).

2. Ссылочные типы (reference type)

Характеризуются тем, что:

· Хранят значения в динамической памяти (куче)

· При присваивании, передаче в функции (по значению) и возврате значения из функции происходит копирование ссылки, а не значения. В результате может получаться несколько ссылок на один и тот же объект.

· Память выделяется с помощью оператора new

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

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

К ссылочным типам относятся: строки (string), массивы, классы, делегаты и интерфейсы

3. Указатели (pointer)

Характеризуются тем, что:

· Хранят значения в динамической памяти (куче)

· При присваивании, передаче в функции (по значению) и возврате значения из функции происходит копирование указателя, а не значения. В результате может получаться несколько указателей на один и тот же объект.

· Память выделяется с помощью оператора new

· Память освобождается с помощью оператора delete

· В .NET могут использоваться только в небезопасных блоках (unsafe). В основном используются при взаимодействии с модулями, использующими указатели (например через dll) и для оптимизации

· Являются адресом объекта. Доступ к объекту происходит через операцию разименования адреса (* или ->). Допускают адресную арифметику.

4. Тип void

Характеризуются тем, что:

· Не имеет значений

· Используется, чтобы создавать функции без возвращаемого значения (процедуры) и безтиповые указатели (void*)

Работа со значимыми типами происходит точно также как и в C++. Они также хранятся в стеке как обычные числовые значения, но есть механизм под названием boxing, позволяющий превращать их в классы. Его мы рассмотрим позже.

Работа с указателями также аналогична C++, однако использовать их придется только для интеграции с системами и использующими модель памяти без сборки мусора и, возможно, в целях оптимизации.

Ссылочные типы и строки будут рассмотрены позднее, после знакомства с основными понятиями ООП.

В завершении приведем таблицу всех типов C# и соответствующих им системных типов .NET.

Тип C# Системный тип Описание Размер/точность Константы
bool System.Boolean Булевый тип, принимает значения истина (true) или ложь (false) 8 бит true; false
sbyte System.SByte Целое число, со знаком 8 бит 1; -2; 3
byte System.Byte Целое число, без знака 8 бит 1; 2; 3
short System.Short Целое число, со знаком 16 бит 1; -100; 77
ushort System.UShort Целое число, без знака 16 бит 1; 100; 77
int System.Int32 Целое число, со знаком 32 бита -5; 7; 0xB1
uint System.UInt32 Целое число, без знака 32 бита 7U; 0x1A4U
long System.Int64 Целое число, со знаком 64 бита 5U; -7U; 0x2F1L
ulong System.UInt64 Целое число, без знака 64 бита 5UL; 0x1AUL
float System.Single Вещественное число с плавающей точкой, одинарной точности 7 цифр 1.2F; 0.5F; 2E10F
double System.Double Вещественное число с плавающей точкой, двойной точности 15 цифр 1.5; -0.5; 2E5; 1.7D
decimal System.Decimal Вещественное число с фиксированной точкой 28 значащих цифр 123.45M
char System.Char Символ Unicode 16 бит ‘Ы’
string System.String Строка из символов Unicode   “строка”
object System.Object Прародитель всех встроенных и пользовательских типов    

 

Суффикс D в константах типа double не обязателен.

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

Обратите внимание, что в символьном и строковом типах используется кодировка Unicode.

Строки являются отдельным типом, а не просто массивом символов, как в C++. Работу со строками мы рассмотрим позже.

Тип object также будет рассмотрен позже.

В коде на C# можно пользоваться как именами типов C#, так и системными именами (это может быть удобно при разработке на нескольких языках).

Все остальные возможности C# связаны с объектно-ориентированным программированием и будут рассмотрены в следующем разделе.


Объектно-ориентированное программирование. Сложные системы

Основной проблемой при разработке программ является сложность программного обеспечения. Эта сложность вызвана четырьмя основными причинами:

1. Сложность реального мира

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

2. Трудность управления процессом разработки

Как правило, разработкой серьезного программного обеспечения занимается команда программистов. А это означает, что их работу нужно координировать, раздавать задачи и контролировать их выполнение. В работе программиста очень много творческих аспектов, что тоже усложняет управление проектом: производительность программистов может различаться в 10-ки раз, сами программисты – творческие люди (особенно самые талантливые) и ими сложно управлять (многие авторы сравнивают управление программистами с управлением бродячими котами). Важную роль в производительности играют психологические аспекты.

3. Необходимость обеспечить достаточную гибкость программы

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

4. Проблема описания поведения больших дискретных систем

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

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

Пять признаков сложной системы:

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

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

2. Выбор, какие компоненты в данной системе считаются элементарными, относительно произволен и в большой степени оставляется на усмотрение исследователя.

В зависимости от решаемой задачи выбирается разный уровень и способ разделения системы на подсистемы.

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

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

4. Иерархические системы обычно состоят из немногих типов подсистем, по-разному скомбинированных и организованных.

5. Любая работающая сложная система является результатом развития более простой системы… Сложная система, спроектированная «с нуля», никогда не заработает. Следует начинать с работающей простой системы.

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

Другой пример сложной системы – компьютер. Он состоит из корпуса, материнской платы, процессора, памяти, видеокарты и жесткого диска. Эти элементы взаимодействуют между собой через стандартные интерфейсы, например, видеокарта и материнская плата взаимодействуют через AGP, PCI или PCI-E. Каждый из этих элементов, в свою очередь, состоит из элементов – печатных плат, транзисторов, резисторов и т.д. Типов радиодеталей не так много, но благодаря их различной комбинации можно получать разные устройства.

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

Способы декомпозиции

Основным способом борьбы со сложностью является декомпозиция – разбиение системы на более простые относительно независимые подсистемы.

Существуют два способа декомпозиции:

1. Алгоритмическая декомпозиция

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

2. Объектно-ориентированная декомпозиция

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

Объектно-ориентированная декомпозиция, ориентирована на объекты (поэтому она так и называется), которые, как правило, описывают объекты реального мира. Объекты могут содержать в себе какие-то данные и обладают поведением – могут реагировать на воздействия извне. Данные и действия над ними объединены в объекте.

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

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


Чайник
Вода
Чай
Чашка
Сахар
скипятить
налить
положить
положить
налить
Налить в чайник воды
Скипятить воду
Положить чай
Положить сахар
Залить кипятком
Рис. 1. Алгоритмическая декомпозиция

Рис. 2. Объектно-ориентированная декомпозиция

Есть очень хорошее выражение, позволяющее понять отличие алгоритмической и объектно-ориентированной декомпозиции: "Если процедуры и функции - глаголы, а данные - существительные, то процедурные программы строятся из глаголов, а объектно-ориентированные - из существительных" [Macintosh MacApp 1.1.1 Programmer's Reference. 1986. Cupertino, CA: Apple Computer, p.2.] Действительно – в первом случае мы сосредоточились на действиях, которые происходят, а во втором – на объектах, которые учавствуют в процессе.

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

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

Объектно-ориентированный подход

Действия при объектно-ориентированной декомпозиции являются операциями над объектами (налить воду в чайник – операция над чайником, скипятить в чайнике воду – операция над водой). Т.е. объекты взаимодействуют между собой, чтобы решить некоторую задачу.

В данном примере хорошо видно, что операции изменяют состояние объекта – вода изменяет состояние чайника (был пустым, стал полным), чайник изменяет состояние воды (температуру). А поведение объекта зависит от состояния – если залить чай и сахар в чашке кипятком, то заварится чай. Это тоже одно из свойств объектов.

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

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

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

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

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

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

Поэтому очень важно понимать различия между процессно-ориентированным и объектно-ориентированным подходами и научиться правильно использовать ООП.

Объектно-ориентированного подход пронизывает все аспекты разработки программного обеспечения от анализа требований до написания кода программы и в нем можно выделить:

Объектно-ориентированный анализ – это методология, при которой требования к системе воспринимаются с точки зрения классов и объектов, выявленных в предметной области.

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

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


Классы и объекты

Объект – некоторая абстракция, которая:

1. обладает некоторым поведением

2. может находиться в одном из состояний

3. идентифицируема

Состояние объекта задается множеством значений атрибутов объекта. Атрибут – это некоторое свойство объекта. Поведение объекта задается множеством методов (функций). Метод – это реакция объекта на внешнее событие. Вызовы методов могут привести к изменению атрибутов объекта или вызову методов других объектов.

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

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

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

Рассмотрим объект реального мира – телевизор. Для обычного пользователя - это ящик, который показывает кино и передачи и управляется пультом. Атрибутами могут быть: цвет, размер диагонали, а методами – включить телевизор, переключить каналы, настроить громкость и т.д. Для телемастера – электронное устройство, с определенными характеристиками, например напряжением, частотой развертки, способом обработки сигнала и т.д. Мастер может разобрать и настроить телевизор. У телевизора добавляются методы – отрегулировать вертикальную развертку и т.п. Для продавца – важна марка телевизора и его цена. Методом может быть метод «продать телевизор». Т.е. в разных системах абстракции для одного и того же объекта реального мира могут быть разными.

Основные особенности объектов:

1. В объектах данные объединяются с методами их обработки

2. Состояние объекта может меняться при вызове методов

3. Поведение объекта зависит от его состояния

Рассмотрим в качестве объекта телевизор с точки зрения пользователя.

Состояние объекта описывается атрибутами:

· включен или выключен телевизор

· какой канал показывает телевизор

· громкость

Поведение объекта задается методами:

· включить

· выключить

· переключить канал

· увеличить громкость

· уменьшить громкость

Другие характеристики объектов, мы отбрасываем, т.к. в данной абстракции они не так важны.

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

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

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

Телевизоры бывают разные, однако они обладают сходной структурой и поведением, поэтому они образуют класс.

Класс – это множество объектов со сходной структурой и поведением. При написании программы достаточно один раз описать класс (структуру и поведение), а потом можно создавать объекты. Объект – экземпляр класса.

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

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

В английском языке различия класса и объекта хорошо видны на примере артиклей. A table – некий абстрактный стол – класс. The table – конкретный стол, который стоит в комнате – объект.

С точки зрения языка программирования класс является типом, а объект – переменной этого типа.


Программа Hello world!

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

using System;

 

namespace Stud.HelloWorld

{

/// <summary>

/// Это специальный XML-комментарий для класса, по которому

/// система умеет генерировать документацию

/// </summary>

class Program

{

/// <summary>

/// Это XML-комментарий для метода, класса, по которому

/// система умеет генерировать документацию

/// </summary>

[STAThread]

static void Main(string[] args)

{

Console.WriteLine("Hello world!");

}

}

}

Рассмотрим эту программу подробно. C# - чисто объектно-ориентированный язык, т.е. в нем нет обычных функций и глобальных переменных – только классы и их атрибуты и методы. Поэтому даже для такой простой программы необходимо описать класс - Medved. Это делается с помощью ключевого слова class. Классы могут содержать описания атрибутов, методов и других классов. Класс в данном примере содержит единственный метод – метод Main.

Работа программы на C# начинается со статического (обозначается ключевым словом static) метода Main, принадлежащего одному из классов. Статические методы можно использовать не создавая объект. Более подробно мы рассмотрим статические атрибуты и методы позже. Аргумент этого метода – массив аргументов командной строки.

Метод Main отмечем атрибутом метода [STAThread] (не путать с атрибутом класса), который указывает, что внутри этого метода (в данном случае во всем приложении) при работе с COM-объектами будет использовать однопоточный STA (Single Threaded Apartment). Это влияет только на работу с COM, в самом приложении можно будет использовать многопоточность. Подобные атрибуты используются в C# для разных целей.

В самом начале мы подключили пространство имен System с помощью директивы using. Пространства имен объединяют в себе описания классов и предназначены для того, чтобы логически структурировать большой набор классов и управлять доступом к этим классам. Например, пространство имен System содержит основные системные классы языка C#. Подключив пространство System мы можем использовать классы из этого пространства имен в описании своего класса. Мы используем класс Console (класс для работы с консольным окном) принадлежащий пространству имен System.

Пространства имен имеют иерархическую структуру. Например, пространство System содержит System.IO (классы ввода-вывода), Sistem.Collections (коллекции – списки, массивы и т.д.) и другие пространства имен. Но эта структура только логическая: при подключении с помощью using System классы пространства имен System, классы System.IO и других дочерних пространств имен остаются недоступными. Их нужно подключать отдельно, с помощью еще одной директивы using.

Любой класс в C# должен принадлежать некоторому пространству имен. Если для класса не указать пространство имен он будет принадлежать так называемому безымянному или глобальному пространству имен. Классы этого пространства имен доступны везде.

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

Console – это класс из пространства имен System, предназначенный для работы с консольным окном. Статический метод WriteLine этого класса выводит в консольное окно строку и переходит на следующую строку.

Классы и объекты в C#

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

Атрибуты:

· Размер магазина

· Количество патронов

· Режим стрельбы (одиночный, очередь)

Методы:

· стрелять

· зарядить

· переключить режим стрельбы

Рассмотрим, как в данном примере проявляются свойства объектов.

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

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

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

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

Реализуем этот класс на языке C#. В примерах мы будем следовать следующему соглашению о наименованиях. Классы будем именовать с большой буквы X (это удобно при автоподстановке в среде разработки, чтобы быстро найти свой класс). Атрибуты будем именовать с _ и маленькой буквы. Это удобно чтобы быстро найти атрибут и чтобы отличать его от локальных переменных и аргументов функций, которые также именуются с маленькой буквы, но без подчеркивания. Методы классы и свойства именуются с большой буквы.


Файл Rifle.cs

using System;

 

namespace Stud.SomeGame

{

/// <summary>

/// Класс, реализующий автомат для некоторой компьютерной игры

/// </summary>

public class XRifle

{

// атрибуты или поля класса

public int _maxAmmo; // Размер магазина

public int _ammoCount; // Количество патронов

public bool _burstModeOn; // Режим стрельбы false - одиночный,

// true - очередь

 

// Методы класса

// Стрелять

public void Fire()

{

// Поведение объекта зависит от его состояния

if (_ammoCount > 0)

{

if (!_burstModeOn)

{

// Console.WriteLine выводит текст в консольном окне

Console.WriteLine("-");

// В этом методе меняется состояние объекта

_ammoCount--;

}

else

{

for (int i = 0; i < 3; i++)

{

Console.WriteLine("-");

_ammoCount--;

if (_ammoCount == 0)

{

Console.WriteLine("");

Console.WriteLine("Out of ammo");

return;

}

}

Console.WriteLine("");

}

}

else

{

Console.WriteLine("Out of ammo");

}

}

// Перезарядить

public void Reload()

{

_ammoCount = _maxAmmo;

}

 

// Переключить режим стрельбы

public void SwitchMode()

{

_burstModeOn = !_burstModeOn;

}

}

}

Теперь опишем класс, который будет использовать класс автомат.

Файл Program.cs

using System;

 

namespace Stud.SomeGame

{

class Program

{

/// <summary>

/// Входная точка приложения

/// </summary>

static void Main(string[] args)

{

// Создаем объект класса XRifle

XRifle ak47 = new XRifle();

 

// Обращаемся к атрибуту объекта – размеру магазина

ak47._maxAmmo = 25;

// Перезаряжаем автомат

ak47.Reload();

 

// Создаем еще один объект

XRifle m16 = new XRifle();

m16._maxAmmo = 30;

m16.Reload();

m16.SwitchMode();

 

// Вызываем метод стрелять для объекта ak47

ak47.Fire();

// В ak47 осталось 24 патрона. В m16 - по прежнему 30

 

// Вызываем метод стрелять для объекта m16

m16.Fire();

// В m16 осталось 27 патронов (т.к. стреляем очередью).

// В ak47 - 24.

 

// Стреляем очередями

m16.SwitchMode();

for (int i = 0; i < 10; i++) m16.Fire();

 

// Патроны кончились. Перезаряжаем

m16.Reload();

 

// Снова можно стрелять

m16.Fire();

 

// Чтобы в конце ждала нажатия enter

Console.ReadLine();

}

}

}

 

Объекты в C# создаются с помощью ключевого слова new в динамической памяти или куче. Работа с объектами происходит через ссылки. В данном примере – ak47 – это ссылка на объект класса XRifle. Через точку мы можем обращаться к атрибутам и методам объекта. Объекты копируются и сравниваются как ссылки (если не определены специальные операции).

[TODO] Написать сюда подробнее но без боксинга пока.

Конструкторы и деструкторы

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

Вызывать конструктор явно – нельзя.

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

public XRifle(int maxAmmo)

{

_maxAmmo = maxAmmo;

_ammoCount = maxAmmo;

_burstModeOn = false;

Console.WriteLine("Rifle created");

}

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

// Создаем автомат с магазином на 30 патронов

XRifle r1 = new XRifle(30);

 

// Создаем автомат с магазином на 20 патронов

XRifle r2 = new XRifle(20);

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

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

~XRifle()

{

Console.WriteLine("Rifle destroyed");

}

В С# деструктор вызывается, когда объект уничтожается сборщиком мусора. Поскольку в C# не нужно явно освобождать динамическую память, как, например, в C++, то деструкторы применяются не так часто. Они используются, когда нужно освободить какие-то ресурсы, которые сборщик мусора автоматически не освобождает – закрыть файлы, сетевые соединения и т.п.

 


Инкапсуляция

С конструктором класс стал лучше, однако, некоторые проблемы все равно остаются. Хотя размер магазина и задается при создании объекта, его по-прежнему можно изменить в любое время. Причем можно даже присвоить ему отрицательное значение. Та же проблема возникает с количеством патронов. Этому атрибуту можно присвоить произвольное значение, например отрицательное или большее чем размер магазина. Количество патронов должно изменяться только при создании объекта, при стрельбе и при перезарядке.

Эти проблемы позволяет решить инкапсуляция.

Инкапсуляция – процесс защиты внутренней структуры объекта от внешнего мира.

Под защитой, в данном случае понимается ограничение доступа, а под внешним миром – другие объекты.

В C# существует 5 уровней доступа к атрибутам, методам и вложенным классам класса:

private – атрибуты и методы, объявленные как private (закрытые) доступны только методам класса.

protected– атрибуты и методы, объявленные как protected (защищенные) доступны методам класса, а также методам дочерних классов (см. Наследование).

public– атрибуты и методы класса, объявленные как public (открытые) доступны любым функциям и методам.

internal– атрибуты и методы классы доступны только внутри сборки[1] (assembly)

protected internal– атрибуты и методы классы доступны только внутри сборки и в дочерних классах (в другой сборке)

Сами классы можно также помечать как internal, чтобы они были доступны только в данной сборке.

Классы по умолчанию – internal, а атрибуты и методы – private.

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

using System;

 

namespace Stud.SomeGame

{

class XRifle

{

// атрибуты

private int _maxAmmo; // Размер магазина

private int _ammoCount; // Количество патронов

private bool _burstModeOn; // Режим стрельбы

}

 

}

 

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

Например, можно было бы не реализовывать отдельные методы Reload и SwitchMode, и не делать атрибуты класса закрытыми, а вместо Reload присваивать _ammoCount = _maxAmmo. Но предположим, что в игре добавляются патроны разных типов, которые тоже будут объектами, и магазин будет представлять собой стек таких объектов. Тогда придется переделать все объекты, которые напрямую работают с атрибутом _ammoCount.

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

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

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

Свойства

Мы закрыли доступ к атрибуту _ammoCount, чтобы защитить его от неправильного использования, однако теперь мы не можем узнать текущее количество патронов.

Чтобы решить эту проблему можно описать метод, возвращающий количество патронов и сделать его открытым (public)

public int GetAmmoCount()

{

return _ammoCount;

}

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

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

public void SetAmmoCount(int newAmmoCount)

{

if ((newAmmoCount >= 0) && (newAmmoCount <= _maxAmmo))

_ammoCount = newAmmoCount;

else

Console.WriteLine("Ошибка! Недопустимое количество патронов");

}

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


В C# есть более удобный механизм, позволяющий сделать то же самое – свойства (property). Свойства описываются следующим образом:

public int AmmoCount

{

get

{

return _ammoCount;

}

set

{

// value – это значение, которое присваивается свойству

if ((value >= 0) && (value <= _maxAmmo))

_ammoCount = value;

else

Console.WriteLine("Ошибка! Недопустимое количество патронов");

}

}

 

При чтении значения свойства, выполняется код в секции get, а при записи значения – код в секции set. При этом работа со свойством выглядит как работа с обычным атрибутом.

ak47.AmmoCount = 100; // выдаст Ошибка! Недопустимое количество патронов

Console.WriteLine(ak47.AmmoCount.ToString());

Оставив только секцию get или только секцию set можно сделать свойство только для чтения или только для записи.

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

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

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

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


В .NET 2.0 можно указывать модификаторы доступа для частей get и set свойств:

public class TestProperty

{

int _test = 0;

 

public int ReadOnlyProperty

{

// Для get или set можно указывать модификаторы доступа строже, чем

// модификатор свойства. Поэтому public указывать нельзя -

// только private или protected

get

{

return _test;

}

private set

{

_test = value;

}

}

}

 

class Program

{

static void Main(string[] args)

{

TestProperty tp = new TestProperty();

tp.ReadOnlyProperty = 5; // ошибка

int z = tp.ReadOnlyProperty; // ok

}

}


Перегрузка методов

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

class XOverloadExample

{

 

public void Print(int x)

{

Console.WriteLine("int x = " + x.ToString());

}

 

public void Print(string x)

{

Console.WriteLine("string x = " + x);

}

 

// С помощью перегрузки можно делать методы с параметрами по умолчанию

public int Sum(int a, int b)

{

return a + b;

}

 

// По умолчанию b = 1

public int Sum(int a)

{

return a + 1;

}

}

 

class Program

{

static void Main(string[] args)

{

XOverloadExample zExample = new XOverloadExample();

zExample.Print(1); // int x = 1

zExample.Print("abc"); // string x = abc

 

int z = zExample.Sum(2, 2); // z = 4

// По умолчанию прибавляет 1

z = zExample.Sum(1); // z = 2

}

}

}

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


Способы передачи параметров

В С# существует несколько способов передачи параметров. По умолчанию используется передача параметров по значению. Т.е. параметры относящиеся к типам-значениям копируются, параметры ссылочного типа копируются как ссылки. Для передачи параметра по ссылке существует два ключевых слова – ref и out. Ref используется для передачи инициализированных параметров, а out для параметров, которые инициализируются внутри метода. Также существует возможность передавать несколько параметров массивом.

// Передача по ссылке с инициализацией

public void ByRef(ref int x)

{

x++;

}

 

// Передача по ссылке с инициализацией внутри метода

public void ByOut(out string s)

{

s = "Hello";

}

 

// Передача параметров

public void ByParams(string s, params int[] x)

{

Console.Write(s);

for (int i = 0; i < x.Length; i++)

{

Console.Write(x[i].ToString());

}

}

int x = 4; // x должно быть обязательно присвоено начальное значение

// Иначе передача через ref невозможна

ByRef(ref x);

Console.Write(x); // 5

 

string s;

// по out можно передавать без инициализации

ByOut(out s);

Console.Write(s);// Hello

 

ByParams("a", 1, 2, 3); // a123

 

 


 

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

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

Наследование – отношение между классами, при котором класс использует структуру и поведение другого (одиночное наследование) или других (множественное наследование) классов.

Класс, от которого наследуются, называется родителем или базовым классом.

Класс-наследник называется дочерним или производным классом.

Рассмотрим, как наследование реализуется в C#.

// Класс родитель

class XBaseClass

{

private int x; // Доступен только в CBaseClass

protected int y; // Будет доступен в дочернем классе, но недоступен извне

public int z; // доступен везде

public static int s = 5; // Статический атрибут

 

public int f()

{

x = 1;

y = 2;

return x + y + z;

}

}

 

// Дочерний класс

class XDerivedClass : XBaseClass

{

public int a;

public void g()

{

// Обратимся к f базового класса

Console.WriteLine(a + f());

// Обратимся к y базового класса. Он доступен, т.к. protected

y = 10;

// x = 5; // Ошибка, т.к. y – private и недоступен

// в дочернем классе

}

}

 

class TestInheritance

{

[STAThread]

static void Main(string[] args)

{

XBaseClass b = new XBaseClass();

// b.x = 1; // Ошибка, x – private и недоступен извне

// b.y = 2; // Ошибка, y - protected и недоступен извне

b.z = 3; // Ок, т.к. z - public

 

XDerivedClass d = new XDerivedClass();

// дочерний класс наследует атрибуты и методы родительского класса,

// поэтому к ним можно обращаться из объекта дочернего класса

// d.x = 1; // Ошибка, x – private и недоступен извне

// d.y = 2; // Ошибка, y - protected и недоступен извне

d.z = 5; // z – public – можно присваивать

d.a = 10; // обращаемся к атрибуту, который появился

// в дочернем классе

Console.WriteLine(d.f()); // 8

d.g(); // 18

 

// К статическому атрибуту можно обращаться, используя

// любое имя класса. Это один и тот же атрибут.

Console.WriteLine(XBaseClass.s); //5

XDerivedClass.s = 7;

Console.WriteLine(XBaseClass.s); // 7

 

Console.ReadLine();

}

}

Обратите внимание, что хотя private атрибуты не видны в дочернем классе, они там присутствуют и используются в процессе вычисления значения функции g, когда мы внутри нее вызываем f.

В C# можно запретить наследование от класса, если написать перед ключевым словом class модификатор sealed.

sealed class XNonInheritableClass

{

}

class XTryToInherit : XNonInheritableClass

{

}

Объявление класса XTryToInherit вызовет ошибку, т.к. нельзя наследоваться от sealed класса.

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

Такое отношение между классами еще называют отношением is (являться). Ёлка является хвойным деревом, а хвойное дерево является деревом.

Объект дочернего класса может играть роль объекта своего родительского класса везде, где это нужно, поскольку он наследует его поведение. Наследование означает, что с объектом дочернего класса можно делать все то же самое что и с объектом родительского классом. Например, деревья можно сажать и рубить. Следовательно, и ёлки можно сажать и рубить, поскольку они являются деревьями. В то же время, Ёлку можно наряжать на новый год, т.е. она расширяет поведение своего базового класса.

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

public void f(XBaseClass b)

{

...

}

вы можете подставить объект дочернего класса

XDerivedClass d = new XDerivedClass();

f(d);

При этом основной код (код метода f) остается без изменений.

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

Рассмотрим основные функции наследования. Наследование позволяет:

1. Повторно использовать уже существующий код

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

2. Сделать поведение объектов единообразным

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

3. Устранить дублирование

Если есть несколько классов, которые обладают общим поведением или структурой, то это дублирование. Чтобы его устранить, нужно выделить общую структуру и поведение в родительский класс.

4. Упростить изменение системы

Часто, чтобы привнести новую функциональность достаточно просто добавить еще один дочерний класс к уже существующим дочерним классам какого-то класса. Например, если есть программа, которая рисует квадраты и круги, которые наследуются от общего класса «Фигура» (в котором, например, уже есть выбор цвета), добавить в такую программу треугольник будет несложно. И он для него уже будет определен выбор цвета. Другая ситуация, когда нужно привнести новую функциональность во все дочерние классы, тогда достаточно изменить логику работы базового класса. Например, чтобы сделать фигуры из предыдущего примера закрашенными, достаточно добавить метод закраски в родительский класс.

5. Организовать несколько уровней инкапсуляции

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


Конструкторы и деструкторы при наследовании

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

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

/// <summary>

/// Базовый класс

/// </summary>

class XBaseClass

{

public int a;

 

public XBaseClass()

{

a = 3;

Console.WriteLine("Работает конструктор XBaseClass");

}

 

~XBaseClass()

{

Console.WriteLine("a = " + a.ToString());

Console.WriteLine("Работает деструктор XBaseClass");

}

}

 

/// <summary>

/// Произсодный класс

/// </summary>

class XDerivedClass : XBaseClass

{

 

public XDerivedClass()

{

// Здесь автоматически вызывается конструктор класса-родителя,

// поэтому инициализировать атрибут a не надо

Console.WriteLine("a = " + a.ToString()); // a уже присвоено 3

Console.WriteLine("Работает конструктор XDerivedClass");

}

 

~XDerivedClass()

{

a = 7; // Это значение попадет в деструктор базового класса

//и там будет напечатано

Console.WriteLine("Работает деструктор XDerivedClass");

// Здесь автоматически вызывается деструктор класса-родителя

// поэтому действия по уничтожению объекта не нужны

}

}

 

class Program

{

static void Main(string[] args)

{

XDerivedClass dc = new XDerivedClass();

// Работает конструктор XBaseClass

// a = 3

// Работает конструктор XDerivedClass

 

dc = null;

// a = 7

// Работает деструктор XDerivedClass

// Работает деструктор XBaseClass

 

}

}

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

Пример:

 

class XBaseClass

{

public int z = 0;

public XBaseClass(int a, int b, int c)

{

z = a + b + c;

}

}

 

class XDerivedClass : XBaseClass

{

public XDerivedClass(int a, int b, int c, int d) : base(a, b, c)

{

// Здесь автоматически вызывается конструктор базового класса

// CBaseClass(a, b, c)

z = z + d;

}

}

 

class Program

{

static void Main(string[] args)

{

XDerivedClass dc = new XDerivedClass(1,2,3,4);

Console.WriteLine(dc.z.ToString()); // 10

}

}

 


Иерархия классов и иерархия объектов

Благодаря наследованию классы выстраиваются в некоторую иерархию, которую можно изобразить в виде дерева (в случае множественного наследования – в виде графа). Например, если у класса A есть дочерние классы B и C, а у класса C есть дочерние классы D и E, то иерархия классов будет выглядеть следующим образом.

A
B
C
E
D

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

Пример:

class C1

{

}

class C2

{

}

class C3

{

C1 a1; // объект класса C1

C2 a2; // объект класса C2

}

class C4

{

C3 a3; // объект класса C3, который включает в себя объекты класса C1 и C2

C2 a4; // объект класса C2

}

C4 Obj; // объявляем объект класса C4

Объекты классов C1, C2, C3 и C4 образуют следующую иерархию:

Obj
a4
a3
a1
a2

Иерархия объектов более гибкая, поскольку она формируется в процессе выполнения. Если атрибут класса является ссылкой на объект некоторого класса, то вы можете оставить эту ссылку пустой (null), подставить вместо нее объект этого класса или объект одного из его дочерних классов. Эти объекты могут содержать ссылки на другие объекты и т.д. Также можно использовать массивы или списки объектов, что делает иерархию объектов еще более динамичной.

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

class XSomeClass

{

public void f()

{

Console.WriteLine("f");

}

}

 

// Первый способ – наследование. Строим иерархию классов

class XDerived : XSomeClass

{

public void g()

{

Console.WriteLine("g");

}

}

 

// Второй способ – обертка (wrapper) или адаптер (adapter).

// Строим иерархию объектов

class XWrapper

{

// Включили объект класса XSomeClass

private XSomeClass _someClass = new XSomeClass();

public void f()

{

_someClass.f();

}

 

public void g()

{

Console.WriteLine("g");

}

}

 

class Program

{

static void Main(string[] args)

{

 

// Работа с оберткой и производным классом похожа

XDerived dc = new XDerived();

dc.f(); // f

 

XWrapper w = new XWrapper();

w.f(); // f

 

}

}

 

 

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

Основным преимуществом наследования является то, что объекты дочерних классов можно использовать там, где требуются объекты родительских классов. Также можно получить доступ к защищенным (protected) атрибутам родительского класса. Но при этом нельзя ограничить доступ к унаследованным public атрибутами и методам (в отличие от C++, в C# нет модификаторов доступа при наследовании). Однако можно переопределить методы.

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

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


Переопределение атрибутов и методов

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

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

Пример:

// Класс родитель

class XBaseClass

{

public int a = 5;

public void f()

{

Console.WriteLine("XBaseClass f()");

}

}

// Класс наследник

class XDerivedClass : XBaseClass

{

// Перекрывает a из CBaseClass

new public int a;

public int b;

// Перекрывает f из BaseClass

new public void f()

{

// Обращаемся к атрибуту из этого класса

Console.Write("a = ");

Console.WriteLine(a.ToString());

// Обращаемся к одноименному атрибуту из базового класса

Console.Write("base.a = " + base.a.ToString());

Console.WriteLine("CDerivedClass f()");

}

}

// Атрибут a и метод f класса-наследника перекрывают

// одноименные атрибут и метод класса-родителя.

 

class Program

{

static void Main(string[] args)

{

XDerivedClass dc = new XDerivedClass();

dc.a = 10; // Присваиваем значение атрибуту дочернего класса

 

dc.f();

// Выдаст:

// a = 10

// base.a = 5

// XDerivedClass f()

 

// Можно обратиться к атрибутам и методам родительского класса

// Приведя объект к объекту базового класса

((XBaseClass)dc).a = 20; // a из базового класса

((XBaseClass)dc).f(); // XBaseClass f()

dc.f();

// Выдаст:

// a = 10

// base.a = 20

// XDerivedClass f()

 

Console.ReadLine();

}

}

 

Если один из атрибутов не виден из-за ограничения доступа (например, a в дочернем классе объявлен как protected, а в базовом, как public), то будет использован тот атрибут, который виден. Т.е. dc.a = 10; в main будет обращаться к атрибуту базового, класса, т.к. одноименный атрибут дочернего класса ему недоступен.

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


Пример наследования

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

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

В данном примере главное преимущество наследования – возможность использовать объект дочернего класса там где требуется родитель не используется. Если мы подставим объект класса XRifle там, где требуется ссылка на XPistol, то нормально работать будет только метод перезарядки. Если же мы попытаемся вызвать Fire, то в соответствии с правилами, описанными в предыдущем разделе, будет вызван Fire пистолета и ничего интересного не получится. Но мы исправим это в следующих вариациях этого примера.

enum FireMode { Single, Burst };

 

/// <summary>

/// Пистолет в компьютерной игре

/// </summary>

class XPistol

{

/// <summary>

/// Размер магазина

/// </summary>

protected int _maxAmmo;

 

/// <summary>

/// Размер магазина

/// </summary>

public int MaxAmmo

{

get

{

return _maxAmmo;

}

}

 

/// <summary>

/// Количество патронов

/// </summary>

protected int _ammoCount = 0;

 

/// <summary>

/// Количество патронов

/// </summary>

public int AmmoCount

{

get

{

return _ammoCount;

}

}

 

 

/// <summary>

/// Конструктор по размеру магазина

/// </summary>

/// <param name="maxAmmo">Размер магазина</param>

public XPistol(int maxAmmo)

{

_maxAmmo = maxAmmo;

Reload();

}

 

/// <summary>

/// Перезарядить

/// </summary>

public void Reload()

{

_ammoCount = MaxAmmo;

}

 

/// <summary>

/// Стрелять

/// </summary>

/// <returns>false, если не получилось</returns>

public bool Fire()

{

if (_ammoCount > 0)

{

_ammoCount--;

Console.WriteLine("-");

return true;

}

 

Console.WriteLine("Кончились патроны");

return false;

}

}

 

 


class XRifle : XPistol

{

FireMode _fireMode = FireMode.Single; // Режим стрельбы

 

/// <summary>

/// Выпускает за очередь

/// </summary>

readonly int _burstCount;

 

/// <summary>

/// Конструктор по кол-ву патронов и размеру очереди

/// </summary>

/// <param name="maxAmmo">размер магазина</param>

/// <param name="_burstCount">выпускает за очередь</param>

public XRifle(int maxAmmo, int burstCount)

: base(maxAmmo)

{

// Достаточно указать параметры очереди

// все остальное инициализировалось в родительском конструкторе

_burstCount = burstCount;

}

 

/// <summary>

/// Стрелять

/// </summary>

/// <returns>false, если не получилось</returns>

public new bool Fire()

{

if (_fireMode == FireMode.Single)

{

return base.Fire();

}

else

{

for (int i = 0; i < _burstCount; i++)

{

if (!base.Fire())

return false;

}

return true;

}

}

 

/// <summary>

/// Переключить режим стрельбы

/// </summary>

public void SwitchMode()

{

switch (_fireMode)

{

case FireMode.Single:

{

_fireMode = FireMode.Burst;

Console.WriteLine("Очередь");

break;

}

case FireMode.Burst:

{

_fireMode = FireMode.Single;

Console.WriteLine("Одиночные");

break;

}

default:

{

// На случай, если добавим еще один режим стрельбы

// и забудем

Console.WriteLine("Ошибка! Неизвестный режим стрельбы");

break;

}

}

}

}

 

 


Полиморфизм. Виртуальные функции.

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

Полиморфизм позволяет воздействовать на разнотипные объекты одинаковым образом - через общий интерфейс, описываемый родительским классом.

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

Виртуальные методы не могут быть статическими.

В С#, полиморфным именем является ссылка на базовый класс.

class XBase

{

// виртуальный метод

public virtual void VPrint()

{

Console.WriteLine("XBase");

}

// обычный метод

public void Print()

{

Console.WriteLine("XBase");

}

 

// Обычный метод, внутри которого

// вызывается виртуальный метод

public void UseVPrint()

{

VPrint();

}

}

 

class XDerived1 : XBase

{

// переопределение виртуального метода

public override void VPrint()

{

Console.WriteLine("XDerived1");

}

// переопределение обычного метода

new public void Print()

{

Console.WriteLine("XDerived1");

}

}

class XDerived2 : XBase

{

public override void VPrint()

{

Console.WriteLine("XDerived2");

}

new public void Print()

{

Console.WriteLine("XDerived2");

}

};

class XDerived3 : XBase

{

// Поскольку мы не написали override, то этот метод будет работать

// как обычный и полиморфизма не будет. То же самое будет, если написать

// вместо override ключевое слово new

public void VPrint()

{

Console.WriteLine("XDerived3");

}

new public void Print()

{

Console.WriteLine("XDerived3");

}

}

 

class Program

{

static void Main(string[] args)

{

XBase bc = new XBase();

XDerived1 dc1 = new XDerived1();

XDerived2 dc2 = new XDerived2();

XDerived3 dc3 = new XDerived3();

XBase zAny;

 

// Здесь ничего особенного, просто вызываем метод базового класса,

// через ссылку типа XBase

zAny = bc;

zAny.Print(); // XBase

zAny.VPrint(); // XBase

zAny.UseVPrint(); // XBase

 

zAny = dc1;

// Не смотря на то, что на самом деле

// pAny ссылается на объект дочернего класса,

// вызывается метод базового, класса, т.к. обычные методы

// вызываются в зависимости от типа ссылки, а не от типа объекта

zAny.Print(); // XBase

 

 

// А здесь работает полиморфизм

// Вызывается метод из дочернего класса, т.к. в базовом классе

// он определен как виртуальный

zAny.VPrint(); // XDerived1

 

// Полиморфизм работает даже в ситуации, когда мы вызываем

// виртуальный метод внутри обычного метода базового класса

zAny.UseVPrint(); // XDerived1

 

// То же самое для другого дочернего класса

zAny = dc2;

zAny.Print(); // XBase

zAny.VPrint(); // XDerived2

zAny.UseVPrint(); // XDerived2

 

// 3-ий дочерний класс будет работать по-другому

zAny = dc3;

zAny.Print(); // XBase

// Здесь полиморфизм не сработает, т.к. мы не написали override

zAny.VPrint(); // XBase

zAny.UseVPrint(); // XBase

 

// Мы можем использовать виртуальные методы

// и напрямую, как обычные методы

dc2.VPrint(); // XDerived2

 

 

Console.ReadLine();

}

}

}

 

Таким образом, ключевое слово override в C# позволяет определять, что делать с виртуальным методом – просто перекрыть его (написав перед одноименным методом дочернего класса new или ничего не написав) или переопределить его, чтобы работал полиморфизм (написав перед одноименным методом дочернего класса override).

Если создать класс - наследник XDerived1, то в нем также можно определить метод VPrint, т.е. вирутальный метод при повторном наследовании остается виртуальным. Но в C# это происходит только при условии, что в XDerived1 этот метод переопределен с помощью override или вообще не переопределен. Если же он перекрыт одноименным методом с помощью new, то в дочерних классах этот метод уже не будет виртуальным. Например, при наследовании от XDerived3 метод VPrint уже нельзя будет переопределить с помощью override.

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

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

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

Особенно полиморфизм актуален, когда исходный код класса недоступен (например, при использовании библиотек классов). В таком случае единственный способ переопределить поведение класса – наследование и полиморфизм.

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

 


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

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

Абстрактный метод – объявленный, но не реализованный метод в абстрактном классе.

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

В C# абстрактные классы и абстрактные метода объявляются с помощью ключевого слова abstract. При этом они не обязательно должны содержать абстрактные методы. Слово abstract перед именем класса обозначает, что нельзя создать объект этого класса.

Классический пример абстрактных классов, виртуальных методов и полиморфизма – класс геометрическая фигура. У фигуры есть цвет и операция - рисовать. Но нарисовать можно только конкретную фигуру – круг, прямоугольник и т.д. Таким образом, класс фигура – абстрактный класс, а «рисовать» - абстрактная операция.

/// <summary>

/// Абстрактный класс - фигура.

/// </summary>

abstract class XShape

{

protected string _сolor;

 

// координаты

public int _x = 0;

public int _y = 0;

 

/// <summary>

/// Обычный метод. Перемещает фигуру в точку x, y

/// </summary>

/// <param name="a_x"></param>

/// <param name="a_y"></param>

public void Move(int x, int y)

{

_x = x;

_y = y;

}

 

/// <summary>

/// Абстрактный метод (ключевое слово abstract уже означает,

/// что он виртуальный, поэтому virtual писать не нужно)

/// </summary>

public abstract void Draw();

 

/// <summary>

/// Не смотря на то, что нельзя создать объект абстрактного класса,

/// абстрактный класс может иметь конструктор. Этот конструктор

/// инициализирует не абстрактные части класса, в данном случае - атрибут

/// _сolor. Этот конструктор используется при создании объектов

/// дочерних классов.

/// </summary>

/// <param name="a_Color">цвет фигуры</param>

public XShape(string color)

{

_сolor = color;

}

}

 

/// <summary>

/// Окружность

/// </summary>

class XCircle : XShape

{

// радиус

public int _r;

 

public XCircle(string color, int r)

: base(color)

{

_r = r;

}

public override void Draw()

{

Console.WriteLine(_сolor + " круг с центром " +

_x.ToString() + "," + _y.ToString() + " и радиусом " +

_r.ToString());

}

}

 

/// <summary>

/// Прямоугольник

/// </summary>

class XRectangle : XShape

{

// размеры сторон

public int _a = 0;

public int _b = 0;

 

public XRectangle(string color, int a, int b)

: base(color)

{

_a = a;

_b = b;

}

public override void Draw()

{

Console.WriteLine(_сolor +

" квадрат с координатоми левого вернего угла " +

_x.ToString() + "," + _y.ToString() + " и сторонами " +

_a.ToString() + "и " + _b.ToString());

}

}

 

class Program

{

static void Main(string[] args)

{

XShape sh;

// zShape = new XShape();// Ошибка, нельзя создать объект

// абстрактного класса

 

 

XCircle c = new XCircle("Красный", 5);

// Вызываем обычный метод, реализованный в XShape

// и унаследованный дочерними классами

c.Move(10, 10);

 

XRectangle s = new XRectangle("Зеленый", 5, 10);

// Вызываем обычный метод, реализованный в XShape

// и унаследованный дочерними классами

c.Move(5, 5);

 

sh = c;

 

// Вызываем абстрактный метод Draw. В каждом из дочерних

// классов он реализован по-своему. Благодаря полиморфизму

// вызывается нужный метод

sh.Draw();

// Красный круг с центром в точке 10, 10 и радиусом 5

 

sh = s;

 

// Вызываем абстрактный метод Draw. В каждом из дочерних

// классов он реализован по-своему. Благодаря полиморфизму

// вызывается нужный метод

sh.Draw();

// Зеленый квадрат с координатоми левого вернего угла 5,5

// и сторонами 5 и 10

 

Console.ReadLine();

}

}

 

 

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

Интерфейсы

Интерфейс - внешний вид класса, объекта или модуля, выделяющий его существенные черты и не показывающий внутреннего устройства и секретов поведения.

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

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

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

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

Например, если рассмотреть объект класса кошка (XCat), наследник класса животное (XAnimal). Объект этого класса может выступать в двух ролях – питомец хозяйки или же пациент ветеринара. В этом случае логично описать два интерфейса – питомец (IPet) и пациент (IPatient). Хозяйка будет взаимодействовать с кошкой, используя интерфейс IPet (играть, кормить), а ветеринар - используя интерфейс (IPatient) – лечить. Если хозяйка – не ветеринар, то попытки лечить домашние животное самостоятельно могут быть опасны, поэтому ей интерфейс IPatient недоступен. А ветеринару недоступен интерфейс IPet. В реальной жизни все несколько сложнее – ведь и хозяйка может быть ветеринаром, но не будем усложнять пример (хотя такую ситуацию тоже можно описать интерфейсами ветеринар (IPetDoctor) и хозяин (IPetMaster) которые реализуют люди). В эту модель можно легко добавить других животных, например, собак. Собаки реализуют методы играть и кормить уже по-другому (играют в другие игры и едят другую еду).

В C# для описания интерфейсов служит специальное ключевое слово interface используемое вместо слова class. Интерфейс не может содержать атрибутов (но может содержать свойства), а все его методы – открытые (public), абстрактные и виртуальные. Множественное наследование в C# работает только для интерфейсов - у класса может быть один родительский класс и множество родительских интерфейсов. В этом случае после имени класса пишется имя родительского класса, а за ним имена интерфейсов через запятую. Пример с домашними животными будет выглядеть так:

// Животное

class XAnimal

{

}

 

// Питомец

interface IPet

{

// Кормить

void Feed();

 

// Играть

void Play();

}

 

// Пациент

interface IPatient

{

// Лечиться

void Heal();

}

 

// Кошка

public class XCat : XAnimal, IPet, IPatient

{

public void Feed()

{

// Кормить едой для кошек

}

 

public void Play()

{

// Играть в мышку

}

 

public void Heal()

{

// Лечить

}

}

 


// Собака

public class XDog : XAnimal, IPet, IPatient

{

public void Feed()

{

// Кормить едой для собак

}

 

public void Play()

{

// Играть в палочку

}

 

public void Heal()

{

// Лечить

}

}

 

// Ветеринар

public class XPetDoctor

{

public void Heal(IPatient patient)

{

patient.Heal();

}

}

 

// Хозяин

public class XPetMaster

{

// Поиграть питомца

public void Play(IPet pet)

{

pet.Play();

}

 

// Покормить питомца

public void Feed(IPet pet)

{

pet.Feed();

}

}

В случае наследования от интерфейса, говорят, что класс реализует (implement) интерфейс. Интерфейсы принято называть с буквы I.

Обратите внимание, что в методах интерфейса нет модификаторов доступа (private и т.д.) и слов virtual или abstract. Они не нужны, поскольку все, что описано в интерфейсе – public abstract. В классах, реализующих интерфейсы нет слов override, т.к. других вариантов нет.

Также интерфейс может содержать свойства, например, так:

int SomeProperty { get;}


Варианты полиморфизма

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



<== предыдущая лекция | следующая лекция ==>
Синхронизация процессов | Реализация интерфейса


Карта сайта Карта сайта укр


Уроки php mysql Программирование

Онлайн система счисления Калькулятор онлайн обычный Инженерный калькулятор онлайн Замена русских букв на английские для вебмастеров Замена русских букв на английские

Аппаратное и программное обеспечение Графика и компьютерная сфера Интегрированная геоинформационная система Интернет Компьютер Комплектующие компьютера Лекции Методы и средства измерений неэлектрических величин Обслуживание компьютерных и периферийных устройств Операционные системы Параллельное программирование Проектирование электронных средств Периферийные устройства Полезные ресурсы для программистов Программы для программистов Статьи для программистов Cтруктура и организация данных


 


Не нашли то, что искали? Google вам в помощь!

 
 

© life-prog.ru При использовании материалов прямая ссылка на сайт обязательна.

Генерация страницы за: 0.575 сек.