Page 1

Серия «Современная прикладная математика и информатика»

В. И. Медведев

Особенности объектно-ориентированного программирования на C++/CLI, C# и Java второе издание Редактор серии — Заслуженный деятель науки и техники PT, доктор технических наук, профессор B.C. Моисеев

Казань - 2 0 1 0


УДК 681.3.06 ББК 32.973.26 - 018 M 42

M 42

Медведев В.И. Особенности объектно-ориентированного программирования на C++/CLI, C# и Java. 2-е изд., испр. и доп. - Казань: РИЦ «Школа», 2 0 1 0 . - 4 4 4 c.: ил. ISBN 978-5-4233-0007-4

Излагаются основные понятия и методика разработки объектноориентированных программ на языках C++/CLI, C# и Java (J#). Изложение сопровождается многочисленными примерами законченных программ. Программы поясняются диаграммами языка UML. Особое внимание акцентировано на наиболее сложных для понимания делегатах, собьггиях, уведомлениях, потоках и их синхронизации. Параллельное изложение схожих основных языковых конструкций позволит лучше выявить не только близость и различие языков C++/CLI, C# и Java, но и лучше понять особенности этих языков. Для студентов и преподавателей вузов по направлению вычислительной техники и информатики. Представляет интерес для всех, знающих язык С и изучающих объектно-ориентированное программирование, а также для тех, кто, овладев языком С++, интересуется особенностями и отличиями программирования на языках C++/CLI, Java(J#) и C#. УДК 681.3.06 ББК 32.973.26 - 018

ISBN 978-5-4233-0007-4

© Медведев В. И., 2010


Предисловие Развитие объектно-ориентированного программирования и Internet способствовало появлению новой технологии программирования - .NET технологии, позволяющей на единой платформе разрабатывать компоненты программ на разных языках программирования и обеспечить их совместное выполнение. В рамках .NET технологии предложен новый язык программирования C#, основанный на языке С++ и перенявший из языка Java черты, обеспечивающие создание безопасных программ. С учётом .NET технологии язык С++ расширен новыми возможностями и получил название C++/CLI, появился также язык J# - язык Java применительно к .NET технологии. Язык C# разработан после языка Java. Он не только наследовал лучшее из языка Java, но модифицировал его, придав стройность и удобство использования, например, таких конструкций как делегаты и события. Но, будучи открытым и легко доступным из сайта фирмы Sun Microsystems в Интернете, язык Java, пожалуй, стал самым популярным языком программирования в мире. Сайт фирмы Sun Microsystems доступен программистам всего мира. Доступность сайта объединила профессиональных программистов, неравнодушных к судьбе языка Java, способствуя продвижению компонентноориентированного программирования на этом языке. Предложенный фирмой Microsoft язык J#, являющийся вариантом языка Java для .NET платформы, может использовать (импортировать - import) как библиотеку .NET Framework, так и библиотеку Java. В данной книге акцентировано внимание на принципиальных понятиях языков программирования C++/CLI, C# и Java(J#), без понимания которых немыслима разработка программ, состоящих из множества объектов с потоками, которые взаимосвязаны не только посредством своих интерфейсов, но и с помощью событий и уведомлений. Используемые при этом конструкции языков C++/CLI, C# и Java по своему своеобразны и для лучшего понимания рассматриваются в книге параллельно, отражая их отличия и сходство, с иллюстрацией примеров программ. Параллельное изложение в книге схожих языков создаёт определённые трудности, как для автора, так и для читателя. Но при этом в выигрыше окажется читатель, так как он не только освоит основы трёх языков и их отличия, но и лучШе поймёт сущность управляемого кода, вникая в идентичные примеры программ, написанных на различных языках. В настоящее время объектно-ориентированный подход при разработке систем различной степени сложности общепризнан. Более того, он применяется не только при разработке, но и при использовании широко распространённых объектно-ориентированных систем. То есть для пользователя систе-


ма представляется в виде совокупности интерфейсных объектов и её управление сводится к управлению этими объектами. Разработка объектно-ориентированной системы заключается в представлении её в виде взаимосвязанных функционирующих объектов. Реализация системы осуществляется с помощью объектно-ориентированного программирования. Этот процесс увлекателен, но, как всё в программировании, требует систематичности и тщательности в разработке и реализации программы и, разумеется, хорошего знания основных принципов и языковых средств - ведь результат труда программиста обычно предназначен сотням, может быть, и тысячам потребителей. В данной книге излагаются основные принципы объектноориентированного программирования, достаточные для написания многопоточных приложений. В качестве сред реализации приводимых примеров программ использованы среды разработки Visual Studio .NRT и Eclipse , языки программирования C++/CLI, C#, Java и J#, а также библиотека .NET Framework и библиотеки (пакеты) языка Java. Книга состоит из 13 разделов. В первом и втором разделах изложены основные сведения об изучаемых языках и базовые понятия объектно-ориентированного программирования. Об управляемом и неуправляемом коде и особенностях его применения на разных языках рассмотрено в третьем разделе. Основные отличия языков C++/CLI, C# и Java от языка С, включая классы, приведены в четвёртом и пятом разделах. Шестой раздел посвящён важнейшим понятиям объектноориентированного программирования - классам и интерфейсам. На примере разработки простой программы ввода-вывода информации о компьютерах седьмого раздела ясно видны отличия программного кода на языках C++/CLI и C#. Восьмой раздел посвящён управляемым данным, отличию их использования в языках C++/CLI и C# и уведомлениям в языке Java. Девятый раздел рассказывает обособенностях создания, использования и синхронизации параллельного выполнения потоков на языках C# и Java. В десятом разделе рассматриваются основные классы библиотеки .NET Framework и библиотек Java, используемые в дальнейшей части книги и достаточные для написания многочисленных программ на C#, управляемом C++/CLI и языке Java. Уделено внимание и o6pa6oi ке исключений, применяющихся при возникновении аварийных ситуаций. Одиннадцатый раздел иллюстрирует использование библиотеки .NET Framework и пакетов языка Java при поэтапной разработке многопоточного приложения на языках C# и J# в среде Visual Studio .NET. В двенадцатом разделе приводятся задачи для самооценки приобретённых знаний и умений.


Последний тринадцатый раздел поясняет и иллюстрирует важнейшие понятия объектно-ориентированного программирования - виртуальные функции и абстрактные классы. Для большей наглядности при разработке ряда программ книги использован графический универсальный язык моделирования UML (Unified Modeling Language), широко распространённый при создании сложных объектноориентированных систем. Книга базируется на лекционном курсе, читаемом автором на факультете технической кибернетики и информатики Национального исследовательского университета имени А.Н.Туполева (бывший КАИ - Казанский авиационный институт). Языкам C++/CLI, C# и Java посвящены многие книги, излагающие их подробно, многогранно и профессионально. Цель данной книги - дать базовые знания об этих языках в объёме, достаточном для разработки консольных и оконных приложений, использующих объекты с потоками, событиями, делегатами, взаимодействующих с интерфейсными элементами. Читатели освоят основные классы библиотек, родных для языков C# и Java, поймут их сходство и различие на рассматриваемых в книге программах, параллельно реализующих одни и те же задачи. Надеюсь, что эта книга стимулирует дальнейшее освоением более полюбившегося языка программирования. Книга требует предварительного знания языка С, имеет явную практическую направленность и предполагает последовательную проработку её разделов с просмотром функционирования, анализом и модификацией приводимых примеров программ. В конце книги в приложениях 1 и 2 приводится полностью программа на языке C# и языке J#, разработанная поэтапно в одиннадцатом разделе. Приложение 3 содержит сведения о средах разработки, достаточные для реализации приведённых в книге примеров программ. Также в конце книги приведён перечень примеров программ и предметный указатель. Во втором издании книги изменены некоторые разделы, добавлены управляющие элементы, базирующиеся на классах UserControl и Panel. В одиннадцатом разделе приводится поэтапная разработка более сложной программы, включающей множество потоковых объектов, активно применяющих события, уведомления, разделяемый ресурс и синхронизацию потоков. Введён раздел с зачётными задачами, которые позволят не только оценить уровень полученных знаний и умений, но ещё дополнительно попрактиковаться, сравнить версии своих программ с приведёнными. Завершает второе издание новый раздел, посвящённый виртуальным функциям и абстрактным классам. В И. Медведев


Оглавление Предисловие

3

1. О языках С++, C++/CLI, Java, J# и C#

10

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

^

2.1. Объекты 2.2. Классы 2.3. Объекты, классы и UML 2.4. Основные свойства объектно-ориентированного программирования

13

20

2.5. Библиотеки классов

25

3. Управляемый и неуправляемый код и данные ЗЛ.Виртуальнаямашинаязыка Java 3.2. Общеязыковая среда выполнения CLR платформы .NET 3.3. Управляемый и неуправляемый код и данные 3.4.Сборкамусора 3.5. Ссылочные типы и типы-значения 3.6. Метаданные 3.7. Библиотеки, пакеты и пространства имён 4. Некоторые особенности языков С++, C++/CLI и C# 4 . 1 . 0 типах данных 4.2. Консольный ввод и вывод 4.3. Объявление переменных 4.4. Операторы динамического распределения памяти new и delete 4.5. Массивы 4.6. Объявления структур и перечислений 4.7. Ссылки 4.8. Перегрузка функций 4.9. Объявление функций и передача аргументов по умолчанию 4.10. Передача аргументов функций в языках C++/CLI и C# 5. Классы

15

18

27 27 27 28 29 29 32 32 34

34 35 39 40 45 53 54 55 56 57 60


5.1.0пределениекласса 5.2. Подставляемые функции и оператор привязки языков С++ и C++/CLI 5.3. Некоторые замечания о классах 5.4. Объекты в объектах 5.5. Специальный вид функций класса - конструкторы и деструкторы 5.6. Статические функции класса 6. Наследование классов и интерфейсы 6.1.0бинтерфейсах 6.2. Наследование неуправляемых классов в C++/CLI 6.3. Наследование управляемых классов в C++/CLI, C# и Java 6.4. Использование конструктора базового класса 6.5. Сокрытые переменные, функции и их использование 6.6. Интерфейсы б.бЛ.Определениеинтерфейса 6.6.2. Базовые интерфейсы 6.7. Упаковка и распаковка типов данных 7. Программа ввода-вывода информации

60 64 65 66 69 72 75 75 75 79 80 84 86 86 89 89 92

7.1. 7.2. 7.3. 7.4. 7.5.

Постановказадачи Диаграмма классов Диаграммапоследовательности Диаграмма видов деятельности Поэтапная разработка Inf-приложения на языке C++/CLI

92 94 96 97 98

7.6.

Реализация Inf-приложения на языке C#

Ю7

8. Управляемые данные и их использование 8.1. Свойства в языках C++/CLI, C# nJava 8.2. Делегаты языков C++/CLI и C# 8.3. События языков C++/CLI и C# 8.4. Уведомления и события в Java 8.4.1.Уведомленияв1ауа 8.4.2. Собьггия в Java 9. Потоки и синхронизация их выполнения

*10 110 114 122

'42 142 ^ 160

9.1. Процессы 9.2. Потоки в C# и C++/CLI

160

9.3.noTOKHBJava

160

9.4. Потоковый объект, выдающий событие

'67 172


9.5. Поток получил событие из объекта 9.6. Синхронизация выполнения потоков 9.6.1. Операторы lock и synchronized 9.6.2.Связимеждупотоками 9.6.2.1. Связи между потоками в Java 9.6.2.2.СвязимеждупотокамивС# 9.6.2.3. Связи между потоками в C++/CLI 10. Библиотека .NET Framework и библиотеки языка Java 10.1. Приложение 10.2. Классы, объекты и элементы приложения 10.3. Простейшие приложения 10.4. Сообщения Windows, собьггия и делегаты 10.4.1. Обработка событий мыши на языке C# 10.4.2. Обработка собьггий мыши на языке C++/CLI 10.4.3. Обработка событий мыши на языке Java с использованием предопределённого обработчика mouseDown 10.4.4. Связь собьггия с несколькими объектами на языке C# 10.5. Ещё раз о событиях и уведомлении в языке Java 10.5.1. Обработка собьггий элементов интерфейса пользователя в языке Java 10.5.2. Обработка событий мыши на языке Java, используя интерфейс MouseListener 10.5.3. Обработка событий мыши на языке Java, используя классы адаптеров 10.5.4. Обработка событий мыши на языке Java, используя внутренние классы адаптеров и анонимные классы 10.5.5. Обработка события закрытия окна 10.6. Графика 10.6.1.Графическиеобъекты

180 188 188 195 195 198 203 206 206 206 207 211 212 215 216 217 219 220 221 224 226 228 230 231

1 0 . 7 . Co6biTHePaintneroo6pa6oT4HK

240

10.8. Управляющиеэлементы 10.9. Дочерниеокна 10.10. Исключения

246 262 268

11. Разработка программы LorryEndWarehouse 11.1. Программа с точки зрения пользователя 11.2. Объектное представление программы 11.3. События, потоки и их синхронизация 11.4. Поэтапная разработка программы 11.4.1. Первый этап. Разработка класса Warehouse 11.4.2. Второй этап. Разработка классов ContrlRegion и Lorry 11.4.3. Третийэтап. Разработкаприложения csLorryAndWarehouse

277 278 279 280 281 281 297 313


11.4.4. Четвертый этап. Удаление ресурса

330

12. Зачетные задачи

337

13. Ещё о важном в объектно-ориентированном программировании на С++, C# и J# (Java)

367

13.1. Виртуальные функции 13.1.1.Преобразованиетипов 13.1.2.Виртуальныефункции 13.1.3. Виртуальные функции, используемые в книге 13.2. Абстрактные классы и функции 13.2.1.Абстрактныефункции 13.2.2. Абстрактные классы 13.3. Нововведения в языке C# 13,3..1.Делегатыисобытия 13.3.2.. Статические классы

367 368 373 380 380 381 382 386 386 388

Заключение

390

Список литературы

391

Приложение 1. cjLorryAndWarehouse -приложение на языке Java

392

Приложение 2. csLorryAndWarehouse -приложение на языке C#

404

Приложение 3. Использование сред разработки программ

415

Перечень примеров

426

Предметный указатель

431


1. О языках С++, C++/CLI, Java, J# и C# В настоящее время языки С++, Java и C# являются наиболее потребными языками объектно-ориентированного программирования. Язык С++ создан в 1979 году Бьёрном Страуструпом (Bjarnc Straustrup) как расширение широко популярного языка программирования С. Первоначальное название нового языка "С с классами" впоследствии было изменёно на укоренившееся "С++". Обогатив широко используемый программистами язык С конструкциями, позволяющими реализовать принципы объектноориентированного подхода при разработке программ, создатель не решился существенно изменить полюбившийся язык С, оставив использусмые типы, операторы, указатели и др. Так в язык С++ перешли конструкции, благоприятствующие появлению ненадёжных программ в руках недостаточно квалифицированных программистов. Из этого языка, обладающего богатыми возможностями, были в дальнейшем задействованы основные конструкции для языка Java, а потом - C#. При этом языки Java и C# были реализованы, позволяя как простое создание и использование компонентов, так существенное повышение надёжности и защищённости многокомпонентных программ, правда проигрывая языку С++ в эффективности их выполнения. Меньшая эффективность обусловлена применением языками Java и C# специальных сред выполнения программного кода. Неуправляемый программный код языка С++ превратился в управляемый программный код в языках Java и C#. Но управляемый код, обладающий дополнительной информацией, внёс новое качество в компоненты и компонентно-ориентированное программирование, существенно расширив возможности разработчиков. В 1991 году Джеймс Гослинг (James Gosling), Патрик Ноутон (Patric Naughton), Крис Вирт (Chris Warth), Эд Франк (Ed Frank) и Майк Шеридан (Mike Sheridan) предложили язык Java для разработки программного обеспечения контроллеров. Поскольку разные контролеры реализуют различные машинные языки, пришлось ввести так называемую виртуальную машину JVM (JVM - Java Virtual Machine). Для каждого типа контроллера создавалась своя виртуальная машина, которая выполняла программу, представленную на едином для всех компиляторов языка Java промежуточном языке. Располагая компилятором языка Java, достаточно для внедрения Java программы на некотором процессоре установить соответствующую этому процессору виртуальную машину JVM. Итак, JVM машина решила проблему переносимости (мобильности) программного кода. Но не только это. Наличие особой среды выполнения управляемого кода повысило защищённость и надёжность программ и позволило через управляемый код обеспечить передачу между программными


компонентами информации, отражающей содержащиеся в них данные. Отражение (reflection), раскрывающее скрьггое нутро программного компонента, облегчило взаимосвязь удалённых объектов компонентов, размещенных на сервере с их представителями (прокси - proxy) на клиенте. Язык C#, созданный под руководствомАндерса Хейльсберга, появился в середине 2000 года как основной язык .NET платформы фирмы Microsoft. Программный компонент, написанный на любом языке .NET платформы (C#, J#, Visual Basic и др.), компилируется на промежуточный язык MSIL (MSIL Microsoft Intermediate Language) и затем выполняется в среде CLR (CLR Common Language Runtime). Теперь многокомпонентную программу можно составить из разноязычных компонентов, обеспечив их взаимосвязь, как с помощью интерфейсов, так и посредством отражений. Базирующиеся на языках С и С++, языки Java и C# во многом схожи, но во многом и отличны. Но один и тот же подход при их реализации применительно к создаваемому ими переносимому коду и применению делегирования делает эти языки интересными для изучения и сравнения. Программирование на Java и C#, действительно, доставляет удовольствие. Среда разработки Visual Studio всегда содержала компилятор языка С++. Этот язык, имевший название Visual С++, изменялся, меняя свои версии. Широко известны библиотеки MFC и ATL, применявщиеся с этим языком. Вполне естественно, что появившаяся новая среда разработки Visual Studio.NET сохранила этот знаменитый язык, модифицировав его применительно к .NET платформе. Создатели языка C++.NET платформы .NET, переименованного потом в язык C++/CLI, изменили язык С++, добавив в него средства создания и выполнения как неуправляемого, так и управляемого кода. Будучи родным для платформы .NET, язык C++/CLI облегчил разработку программ, включающих как управляемый, так и неуправляемый коды, позволяя их объединить. Язык C++/CLI образует мост между неуправляемым и управляемым кодом и поэтому представляет интерес для профессиональных программистов, разрабатывающих программные системы, некоторые компоненты которых реализуются на эффективном неуправляемом коде. Заметим, что каждый из языков Java и C# по-своему специфичен и содержит свои особенности, с которыми можно познакомиться по многочисленной литературе. Но эти языки, как наследники языка С++, содержат единое нутро, которое их объединяет, которое обозримо и позволяет создавать программы и которое также позволяет рассмотреть эти языки параллельно, чтобы понять то, что их объединяет и то, что их отличает. Этому и посвящена данная книга. Рассмотрение языка C++/CLI позволит не только понять отличие неуправляемого и управляемого кода, но и написать программы, использующие только управляемый или неуправляемый код. Разработанные одной и той же фирмой, языки C++/CLI и C# содержат настолько схожие средства работы с управляемыми данными, что внешне их программы выглядят схоже и просто преобразуются одна в другую. Поэтому параллельное изучение этих языков


обоснованно и представляет большой интерес. Что же касается языка Java, то этот язык очень схож с языком C#. Реализация языка Java для .NET платформы (язык J#), позволяет воспользоваться не только библиотекой .NET Framework, используемой тремя языками, но и родными пакетами языка Java. Это позволяет в среде Visual Studio.NET параллельно изучить не только три языка с библиотекой .NET Framework, но и язык J# с родными библиотеками Java. В книге также использована и родная среда Eclipse для языка Java.


2. Основные понятия объектноориентированного программирования 2.1. Объекты Объектно-ориентированное программирование резко изменило технологию разработки программ, которая теперь реализует модель, состоящую из множества взаимосвязанных объектов. Выполнение программы сводится к взаимодействию объектов, каждый из которых динамически может создать новые объекты, управляя ими в дальнейшем, или уничтожить эти объекты, включая самого себя. Каждый объект модели, да и сама модель, представляет некоторую сущность, характеризуемую своим состоянием и поведением. При функционировании модели состояние составляющих её объектов, да и самой модели, меняется в зависимости от поведения объектов. По их состоянию и поведению объекты разделяются на типы. Однотипные объекты имеют схожее состояние и поведение. В языках программирования С++, Java и C# однотипные объекты описываются схоже со структурой языка С, элементами которой теперь являются не только объявления данных, представляющих состояние объекта, но и объявления (или описания) функций (методов), определяющих его поведение. Этот вновь введённый тип, описывающий объекты и отсутствующий в языке С, но появившийся в языках С++, Java и C# называется классом (class). Из класса создаются переменные типа class, называемые объектами. Объект - это ограниченная сущность, характеризующаяся своим состоянием и поведением. Объект является экземпляром класса. Класс описывает всё множество объектов определённого типа, то есть объектов с таким же состоянием и поведением. При создании объект инициализируется, то есть его данным, определяющим состояние объекта, присваиваются исходные значения. Исходные состояния объектов, созданных из одного и того же класса, обычно отличаются, но могут и совпадать. Во время функционирования объекта его состояние будет меняться. Изменение состояния объекта осуществляется функциями класса, из которого создан объект. Функции вызываются применительно к объекту; они могут быть вызваны извне, либо внутри этого объекта. Применение функций к объекту заставляет его функционировать. Функционирование объекта состоит в последовательном изменении его состояния во времени.


Функции объекта могут обеспечить не только функционирование этого объекта. Они, используя другие классы, могут создать объекты этих классов и взаимодействовать с этими объектами посредством функций этих объектов. Перед написанием объектно-ориентированной программы необходимо проанализировать решаемую проблему, выделить (осуществить декомпозицию) основные сущности - будущие объекты, объединить эти сущности во множества, каждое из которых представляется своим состоянием и поведением. Теперь можно представить каждое множество сущностей в видс класса, объявив в нём общие данные и функции, и обеспечить статическую взаимосвязь этих классов. Функции классов должны обеспечить динамическое создание, использование и уничтожение объектов взаимосвязанных классов. В конечном счёте, объектно-ориентированная программа выполняется как организованная совокупность взаимодействующих объектов. Объектами можно представить всё, что требуется в программе, например, прикладное окно, кнопки и меню в этом окне, таблицы, списки, стеки, графы, изображения и т.д. и т.п. Допустим, необходимо написать объектно-ориентированную программу моделирования полётов самолётов между аэропортами. Здесь в виде объектов можно представить такие сущности как самолёты и аэропорты. Для создания объектов самолётов можно разработать, например, класс с именем Plane, в котором объявить данные (состояние): координаты траектории полёта, вес, скорость и др. Функции этого класса должны установить исходные значения этих данных и осуществить полёт, изменяя координаты в соответствии с маршрутом во времени. Класс, связанный с аэропортом, назовём Airport. Данные класса Airport содержат информацию об аэропорте, например: количество взлётнопосадочных полос, координаты местоположения аэропорта и др. Функции этого класса управляют аэропортом, разрешая или запрещая, допустим, взлёт и посадку самолётов, и др. В начале своего выполнения программа должна создать необходимое количество объектов аэропортов из класса Airport с заданным числом взлётно-посадочных полос, затем создать и разместить на них объекты самолётов класса Plane. Потом к созданным объектам применяются их функции - аэропорты и самолёты начинают функционировать. В зависимости от целей разработки программы можно описать классы по-разному, ограничив различно представленные классами сущности. Если, например, ставится задача исследовать расход топлива при полётах самолётов, то класс самолёта должен включать данные, влияющие на расход топлива. Здесь придётся учесть не только вес самолёта, но и вес пассажиров с их багажом. Объектом всегда представляется ограниченная сущность так, как она описана классом этого объекта.


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

2.2. Классы Тип class (класс) объектно-ориентированных языков программирования C++/CLI, C# и Java описывает множество однотипных объектов с одними и теми же данными и функциями, то есть обладающих общим состоянием и поведением. Те данные и функции класса, которые предназначены только для собственного использования внутри объектов данного класса, помечаются доступом private (закрытый). Они не доступны извне объекта. Часть данных и функций класса, которые дозволяется использовать снаружи объекта этого класса, помечаются доступом public (открытый). Используя открытые данные и функции, можно воздействовать на объект извне, управляя его функционированием. Кстати, эти открьггые функции, будучи функциями класса объекта, могут использовать его закрытые функции и данные. Обычно большинство данных, функций и операций скрыто Q>rivate) внутри класса и не доступно вне объекта и лишь только те из них, без которых невозможно управление объектом извне, открыты Q?ublic). В идеале, желательно описать в классе открытыми только функции, достаточные и необходимые для управления поведением его объектов и обращения к требуемым данным, а всё остальное - скрыть. Эти открытые функции образуют интерфейс объектов, создаваемых из класса. В дальнейшем объекты взаимодействуют только через их интерфейсы, поскольку только интерфейсные функции доступны вне объекта. В примере 2.2.1 приведено описание простого класса CBall, которым представлена такая сущность как летающий шар, ограниченная только координатами полёта и простейшим перемещением: каждый раз при вызове функции Move() координата x увеличивается на 5, а коордИната у - на 10 пикселей. Класс CBall описан на языках C++/CLI, C# и Java. Пример 2.2.1. Описание класса объекта шара на языках C++/CLI, C# и Java. Класс CBall содержит закрытые переменные x и у и открьггые функции Move() перемещения, Set() установки исходных значений координат и Show() выдачи на консоль текущих значений координат объекта шара. //////////////////// // C+ + / C L I class CBall

// О п и с а н и е к л а с с а CBall н а я з ы к е C + + / C L I

private:

// Закрыть данные


int public:

x, у;

// Закрытые координаты x и у шара // Открыть функции

void Move ( ) // Описание открытой функции перемещения шара {x= x+5; y= y+10;} void Set (int vX, int vY) // Описание открытой функции установки {x= vX; y= vY;> // координат шара

>;

void Show ( ) // Описание открытой функции выдачи на консоль {System::Console::WriteLine ("x= " + x + " y= " + у);}

//////////////////// //c# class CBall

{

private int

// О п и с а н и е класса CBall на я з ы к е C# x, у;

public void Move ( ) {x= x+5; y= y+10;}

// Закрытые координаты x и у шара // Описание открытой функции перемещения шара

public void Set (int vX, int vY) {x= vX; y= vY;}

>

// Описание открытой функции // установки координат шара

public void Show ( ) // Описание открытой функции выдачи на консоль {System.Console.WriteLine ("x= " + x + " y= " + у);>

//////////////////// // Java и J# class CBall

{

private int

// О п и с а н и е класса CBall на я з ы к е Java x, у;

// Закрытые координаты x и у шара

public void Move ( ) // Описание открытой функции перемещения шара {x= x+5; y= y+10; > public vold Set (int vX, int vY) // Описание открытой функции установки <x= vX; y= vY; > // координат шара

>

public void Show ( ) // Описание открытой функции выдачи на консоль {System,out.prlntln ("x= " + x + " y= " + у);>

На первый взгляд описания класса CBall на языках C++.NET, C# и Java схожи - сказывается общее родство от языков С и С++. Но видны и различия. C++/CLI. В языке C++/CLI используются метки, состоящие из ключевого слова public или private с последующим двоеточием и определяющие доступ


нижеследующих данных и функций. Метка private: закрывает переменные x и у. Метка public: открывает функции Move(), Set() и Show() класса для применения извне к объекту класса CBaIl. Язык C++/CLI в примере 2.2.1 использует для вывода значений текущих данных функцию WriteLine() класса Console пространства имён System, о которых будет рассказано позднее. Аргумент функции WriteLine() определяет выводимую на консоль строку, составленную из четырёх объединённых с помощи операции "+" составляющих строк: строки "x= ", строкового значения x, строки "у = " и строкового значения у. Обратите внимание - описание класса завершено закрывающей фигурной скобкой и точкой с запятой. Java и C#. Ключевые слова public и private пишутся непосредственно перед объявлением переменных и заголовком описания функций. Ключевое слово private спрятало координаты x и у внутри класса, закрыв к ним доступ извне и сделав их доступными только через открьггые функции Move(), Set() и Show(). Ключевые слова public открывают функции Move(), Set() и Show() для их применения извне к объектам класса CBall. Вывод на консоль на языке Java осуществляется функцией println() класса out, а на языке C# аналогичен выводу на языке C++/CLI, поскольку используется та же функция WriteLine() того же класса Console пространства имен System той же библиотеки .NET Framework. Обратите внимание, что вместо двух разделяющих двоеточий языка C++/CLI здесь применены точки. Описание класса на языках C# и Java завершено закрывающей фигурной скобкой без точки с запятой. Ещё раз обратите внимание на различие в описании класса CBalI на разных языках. На языке C++/CLI доступ private (закрьггь) к данным x и у распространяется вниз до следующего доступа public (открьггь), действующего уже на нижеследующие функции. После доступов private и public стоит двоеточие. На языках же Java и C# доступ необходимо указывать перед объявлением каждой переменной и перед описанием каждой функции, и здесь двоеточия отсутствуют. Для вывода на консоль в языке Java используется функция printf() класса out, а в языках C# и C++/CLI - функция WriteLine() класса Console. Кроме этих мелких отличий в написании программ на этих языках вы познакомитесь в дальнейшем и с более существенными. С целью сделать программу общепонятной, принято на языках C++/CLI и C# имена переменных (и, в частности, имена ссылок и указателей на объекты) писать с маленькой буквы, а имена функций и классов с большой. При написании же программ на языке Java эти рекомендации не соблюдаются, но принято имена классов писать с большой буквы. Фрагменты программ примера 2.2.2 показывают разницу в создании и использовании объектов класса CBall на различных языках программирования.


Пример 2.2.2. Создание и использование объектов. //////////////////// // C++/CLI CBall ball; ball.Set (7, 11); ball.Move ( ) ; ball.Show ( ); CBall pBall pBall pBall

// Создан объект ball класса CBall // Установить исходное состояние объекта ball // Переместить объект ball // Выдать текущие координаты объекта ball

*pBall= new CBall ( ) ; -> Set (17, 21); -> Move ( ); -> Show ( );

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

//////////////////// / / J a v a и J# CBall ball= new CBall ( ); // Создать объект в управляемой куче I ball.Set (7, 11); // Установить исходное состояние объекта ball.Move ( ) ; // Переместить объект ball.Show ( ) ; // Выдать текущие координаты объекта

//////////////////// //C# CBall ball= new CBall ( ); // Создать объект в управляемой куче ! ball.Set (7, 11); // Установить исходное состояние объекта ball.Move(); //Переместитьобъект ball.Show ( ); // Выдать текущие координаты объекта

C++/CLI. Объект может быть создан двояко - либо объявлением переменной ball типа класса CBall, либо путём объявления указателя pBall типа класса CBall и создания объекта в куче (о куче будет рассказано в дальнейшем подробнее) посредством оператора new с автоматическим присвоением адреса этого объекта указателю. В первом случае для доступа к открытым данным и функциям используется оператор "." (точка), а во втором случае - " - > " (стрелка). После создания объектов функция Set() устанавливает исходные значения их данных. Затем функция Move() перемещает объекты, и новые значения их данных выдаются функцией Show() на консоль. C# и Java. В языках Java и C# запрещены указатели. Вместо них используются ссылочные переменные, значения которых управляются системой. Ссылочные переменные ссылаются к данным, размещённым в управляемой куче. Оператор new создаёт здесь объект в управляемой куче. Только оператор "." (точка) применяется для ссылки к открытым данным или открытым функциям объекта.


2.3. Объекты, классы и UML .NET технология позволяет разрабатывать программу, интегрировав её из компонентов, написанных на разных языках программирования, например.на C++/CLI, C# и J#. Приступая к созданию какой-либо системы, разработчикам желательно воспользоваться языком более высокого уровня, чем языки программирования, чтобы обсудить структуру как всей системы в целом, так и её частей и взаимосвязь этих частей в пространстве (статически) и во времени (динамически) безотносительно к языку программирования. Желательно, чтобы этот язык оперировал такими понятиями объектно-ориентированных систем как класс, объект, устанавливал бы связи между ними статически и динамически. При обсуждении разрабатываемой системы в нотациях этого языка хотелось бы уточнять данные и функции каждого класса, при необходимости разделять сложные классы на более простые или объединять некоторые простенькие классы, проанализировать динамику создания и уничтожения объектов во времени, их взаимосвязь, и так далее. И только после активного обсуждения проекта системы выбрать языки программирования для реализации её частей и приступить к созданию программного кода, придерживаясь принятых совместных решений. Для графического представления объектно-ориентированных систем в настоящее время используется язык UML. Унифицированный язык моделирования UML {Unified Modeling Language) предназначен для описания объектно-ориентированных систем в виде совокупности диаграмм, тем самым раскрывая статическую и динамическую суть системы как модели, состоящей из взаимодействующих объектов. Перед применением языка UML необходимо проанализировать проектируемую систему, выявить основные сущности и понятия, образовать набор классов, из которых будет создаваться всё множество объектов системы. Затем приступают к разработке диаграмм языка UML: диаграмм вариантов использования, представляющих систему с точки зрения пользователя, диаграмм классов, представляющих классы и их статическую взаимосвязь, диаграмм последовательности, изображающих динамическое функционирование объектов во времени, и др. На рис. 2.3.1 изображен класс CBall из примера 2.2.1 на языке UML. Класс представляется в виде прямоугольника, разделённого на три части, в которых размещается имя класса, перечень атрибутов и, наконец, перечень функций класса. Перед именами данных и функций ставятся символы " - " и "+", указывающие на доступ private и public соответственно.


Рис. 2.3.1. Класс CBall Из рис. 2.3.1 видно, что на языке UML представлен класс, имеющий имя CBall, закрытые данные x и у и открьггые функции Set(), Move() и Show(). Важно, что это представление класса не зависит от языков программирования и, в частности, от C++/CLI, Java и C#. ball:CBall x: int

у: int

: CBall к int v: int

ball2 : CBall

: CBall

Рис. 2.3.2. Объекты класса CBall На рис. 2.3.2 изображены объекты класса CBall на языке UML. В прямоугольнике помещается имя объекта, отделённое от класса этого объекта двоеточием. Если объект безымянный, то ставится только двоеточие перед именем класса. Имя объекта и класса подчёркиваются. В дальнейшем в книге диаграммы языка UML будут использоваться при разработке некоторых программ с единственной целью - познакомить читателя с нотацией этого языка. Язык UML довольно-таки богат, и при необходимости освоить его по многочисленной литературе. В задачи книги не входит подробное изложение языка UML. Познакомиться с нотацией этого языка можно по книге Рамбо Дж., Якобсона А. и Буча Г. "UML: специальный справочник".

2.4. Основные свойства объектно-ориентированного программирования Три основные свойства характеризуют объектно-ориентированное программирование: - инкапсуляция, - наследование, - полиморфизм. Инкапсуляция (encapsulatwn-сокрытв) - сокрытие данных и функций. Данные, определяющие состояние объекта, замкнуты в этом объекте. Можно связать данные объекта с этим объектом по-разному, приписав им доступ private или public.


Данные с доступом private используются только внутри объекта, с ними оперируют только функции этого объекта. Данные же с доступом public можно использовать и вне объекта. Аналогично, функции объекта с доступом private можно использовать только внутри объекта, то есть к ним можно обращаться только из функций объекта. В то время как к функциям объекта с доступом public можно обращаться и извне объекта. Хороший стиль объектно-ориентированного программирования требует объявить наибольшее число данных и функций как private, обеспечив обращение к объекту, в идеале, только через интерфейс, то есть через открытые функции объекта, имеющие доступ public. Доступ private закрывает доступ к данным и функциям объекта извне. В классе CBaIl примера 2.2.1 функция Set() объявлена как public, поэтому, используя её, допустимо обращение извне к объекту ball класса CBall: ball.Set (7, 11);// C#

Желательно придерживаться правила: скрывать в объектах всё, что возможно, исключив тем самым нежелательные изменения данных извне. Пусть эти изменения данных делаются в границах объекта только его родными функциями. Этим обеспечивается не только надёжность объектов, но и системы, состоящей из надёжных объектов. Взглянем ещё раз на написание класса CBall на языках C++/CLI, C# и Java в примере 2.2.1. Грамматика сокрытия данных и функций у них разная. В языках C# и Java используются ключевые слова private и public, которые предшествуют объявлениям данных и описаниям функций (по умолчанию принят доступ private в языке C# и доступ public в языке Java). В языке же C++/CLI используются метки private: и pubIic:. Данные и функции после (ниже) метки private: считаются закрытыми, а после метки public: - открытыми. Наследование (derivation) - это механизм, позволяющий строить иерархию классов. Некоторый класс объявляется как базовый, и из него порождается новый производный (или порождённый) класс. Производный класс, обладая состоянием и поведением базового класса, дополняет их новыми, определёнными в нём самом, данными и функциями. Обычно при разработке объектно-ориентированных программ выявляют общие данные и функции у разнотипных объектов, создают базовый класс с этими общими данными и функциями и из него уже порождают классы разнотипных объектов, включая в каждый из них требуемые новые данные и функции. Применительно к неуправляемым данным язык C++/CLI позволяет множественное наследование, при котором класс может быть порождён из нескольких базовых классов. При этом могут возникнуть проблемы, связанные с неоднозначностью наследования. Чтобы избежать неоднозначности языки C# и Java, а также язык C++/CLI применительно к управляемым данным, запре-


щают множественное наследование классов, но разрешают множественное наследование интерфейсов. Породим из класса CBaIl, описанного в примере 2.2.1, класс, который бы детализировал созданные из него объекты-шары, прибавив к ним такое качество как цвет. И пусть цвет представляется целочисленной переменной col, значение которой определяет цвет: 0 -красный, 1 - синий, и так далее. Будучи порождённым из класса CBall, наш новый класс, назовём его CColBall, наследует закрытые координаты и открытые функции класса CBall. В качестве интерфейса пусть класс CColBall имеет те же функции класса CBall, но тела функций Set() и Show() придётся изменить, чтобы они устанавливали значения не только исходных координат объекта-шара, но и переменной col, и выводили значение col на консоль. Описание класса CColBall, порождённого из базового класса CBall, приведеновпримере 2.4.1. Пример 2.4.1. Класс CColBall, наследуемый класс CBall. //////////////////// //C++/CLI class CColBall : p u b l i c CBall

{

private: int col;

// Закрыть данные //Цвет

public:

// Открыть функции

void Set (lnt vCol, lnt vX, int vY)

{

>

>;

>

// Функция 1 Установить данные'

col= vCol; CBall::Set (vX, vY);

void Show ( )

{

// П о р о ж д е н и е и з к л а с с а C B a l l

// Функция 'Показать

данные'

Console::Write ("col= " + col); CBall::Show ( ) ;

//////////////////// //c# class CColBall : CBall

{

private lnt col;

// П о р о ж д е н и е и з к л а с с а C B a l l // Закрытая переменная цвет

public vold Set (int vCol int vX, int vY) {col=vCol; b a s e . S e t ( v X , v Y ) ; }

>;

// Открытая функция // 'Установить данные'

public void Show ( ) // Открытая функция ' Показать {Console.Write ("col= " + col); base.Show ();>

данные'


шшишиипш ИJava и J# public class CColBall extends CBall

{

private int col; public

{

>

>

// Закрытая переменная цвет

void Set (int vCol, int vX, int vY)

col= vCol; super.Set (vX, vY);

public

{

>

// П о р о ж д е н и е и з к л а с с а CBall

void Show ( )

// Открытая функция // 'Установить данные'

// Открытая функция ' Показать данные'

System.out.println ("col= "+ col);

super.Show ( ) ;

Класс CColBall включает дополнительную переменную col и обновлённые функцию Set(), список параметров которой дополнен параметром vCol, и функцию Show(). Функции Set() и Show() производного класса CColBall вправе воспользоваться наследуемыми доступными функциями Set() и Show() базового класса CBall. Но чтобы отличить их от одноимённых функций своего класса, необходимо использовать специальный оператор привязки: в языке C++/CLI - "::" (два двоеточия), а в языках Java и C# - соответственно ключевые слова super и base. Тело функции Set() класса CColBall присваивает значение новой переменной col и посредством функции Set() класса CBall присваивает значения закрытым переменным базового класса CBall (оператор привязки "::" и ключевые слова super и base соотносят функцию Set() к классу CBall). Аналогично функция Show() класса CColBall использует функцию Show() базового класса CBall, чтобы выдать скрытые значения x и у. Функция Move() класса CColBall наследуется из класса CBall , оставшись неизменённой. C++/CLI. При описании порождённого класса после его имени стоит ":" (двоеточие), за которым следует список порождения. В списке порождения через запятые перечисляются все наследуемые базовые классы. В нашем случае список включает только один базовый класс CBall с доступом порождения public. При ссылке в порождённом классе к наследуемым функциям базового класса использован оператор привязки "::" (двадвоеточия). Java. При описании порождённого класса после имени класса помещается ключевое слово extends и затем указывается имя только одного наследуемого базового класса, поскольку в языке Java запрещено множественное наследование. У нас этим классом является класс CBall. При ссылке к функциям базового класса использовано ключевое слово super.


C#. При описании порождённого класса после имени класса ставится двоеточие и затем указывается имя только одного наследуемого базового класса, поскольку, как и в языке Java, в языке C# запрещено множественное наследование. При ссылке к функциям базового класса использовано ключевое слово base. Полиморфизм polymorphism) заключается в обозначении одного и того же действия одним именем, которое используется во всей иерархии порождения классов. Например, базовый и производный классы примера 2.4.1 имеют одноимённые функции установки Set(), хотя реализованы эти функции по-разному. Разумеется, обращаться к этим функциям надо соответствующим образом. В примере 2.4.2 показано использование объектов класса CBall и CColBall. Пример 2.4.2. Использование функций объектов. //////////////////// // C + + / C L I // Создан объект класса CBall // К этому объекту применить // функцию Set() класса CBall *pColBall= new CColBall ( ) ; // Создан объект класса CColBall -> Set (1, 200, 30); // К этому объекту применить // функцию Set() класса CColBall Show ( ); // К объекту применить функцию Show() класса CBall -> Show ( ); // К объекту применить функцию Show() класса CColBall

CBall *pBall= new CBall ( ) ; pBall -> Set (70, 80); CColBall pColBall pBall -> pColBall

//////////////////// // J a v a и J # // Создан объект ball класса CBall // К объекту ball применить его // функцию Set() класса CBall CColBall colBalI= new CColBall ( ) ; // Создан объект colBall класса CColBall colBall.Set (1, 200, 30); // К объекту colBall применить его // функцию Set() класса CColBall ball.Show ( ) ; // К объекту ball применить его функцию Show() класса CBall co!Ball.Show ( ) ; // К объекту colBall применить функцию Show() класса CColBall CBall ball= new CBall ( ); ball.Set (70, 80);

//////////////////// //C# // Создан объект ball класса CBall // К объекту ball применить его // функцию Set() класса CBall CColBall colBalI= new CColBall ( ) ; // Создан объект colBall класса CColBall colBall.Set (1, 200, 30); // К объекту colBall применить его // функцию Set() класса CColBall ball.Show ( ) ; // К объекту ball применить его функцию Show() класса CBall colBall.Show ( ) ; // К объекту colBall применить функцию Show класса CColBall

CBall ball= new CBall ( ) ; ball.Set (70, 80);


На диаграмме классов языка UML (рис.2.4.1) в виде прямоугольников представлены базовый и производный классы со стрелкой, направленной от производного класса CColBall к базовому классу CBall (означает порождение или наследование). Каждый прямоугольник состоит из трех частей, содержащих соответственно имя класса, перечень данных и перечень функций. Символ "-" означает доступ private, а символ "+" - public. Если не требуется подробное представление классов, то можно указать только часть данных и функций или только имена классов. «ГГп IR all -nnl: int +Set() +Show() +Move()

-x: int О -v: int +S>t() +Show() +Move()

Рис. 2.4.1. Класс CColBall наследуется из базового класса CBall Как уже говорилось, на языке UML объект представляется подобно классу, но вместо имени класса указывается имя объекта, отделённое двоеточием от имени класса. Если представляется произвольный объект, то имя объекта опускается. Имена объекта и класса подчёркиваются (рис.2.4.2). ball: CBall

: CBall

colored : CColBall

; РСрМ

Рис. 2.4.2. Различные представления объектов классов CBall и CColBall

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


Программистам известны и широко применяются такие библиотеки классов как STL, MFC, ATL, .NET Framework Class Library, java.lang, java.util,java.io идругие. Библиотека STL {Standard Template Library) - библиотека так называемых шаблонных классов, используемых при работе с данными. Библиотека MFC (Microsoft Foundation Class library) - библиотека базовых классов предназначена для разработки многофункциональных Winrfowj-приложений. Библиотека ATL (Active Template Library) — библиотека активных шаблонных классов ориентирована на профессиональных программистов и предназначена для разработки так называемых ЛП^серверов и компактных ССМобъектов. Библиотека .NET Framework Class Library - библиотека классов для разработки программных компонентов на .NET платформе и их совместного выполнения. Библиотеки java.lang, java.util, java.io, java.awt, java.swing и другие библиотеки классов (пакеты) для разработки программ на языке Java. При разработке программных компонентов дополнительно используются библиотеки java. beans, javax.servlet. Не останавливаясь на особенностях, достоинствах и недостатках вышеперечисленных библиотек, мы уделим внимание последним двум библиотекам, поскольку именно они ориентированы на современную .NET технологию и программирование Java программ. Для более эффективного и простого использования принято библиотеку классов разбивать на части, включающие схожие классы. При разработке программы к ней подключают часть библиотеки с требуемыми классами. Части, например, библиотеки .NET Framework Class Library называют пространствами имён. Наиболее часто используемым пространством имён является System. Это пространство имён включает другие, а те, в свою очередь, могут включать иные пространства имён. Также часто применяется пространство имён, ссылаемое как System.Windows.Forms. Здесь явно указывается вхождение одного пространства имён в другое. Каждое пространство имён содержит множество классов, любой из которых можно применить при создании соответствующих объектов, используемых в программе.


3. Управляемый и неуправляемый код и данные 3.1. Виртуальная машина языка Java Популярнейший среди программистов язык С++, позволяя разрабатывать эффективные и компактные программы, обладал рядом существенных недостатков, обусловивших появление виртуальной машины и нового языка Java, позволивших создать более надёжные и безопасные программы. Язык Java сохранил максимум из любимейшего языка программистов С++, удалив или изменив конструкции языка, способствовавшие появлению явного или неявного возможного ошибочного кода. Появление виртуальной машины JVM (Java Virtual Machine), которая интерпретирует программу на промежуточном языке (байт-коде ~ bytecode), позволило не только выявлять ошибки на этапе выполнения и корректировать выполнение программы программистом, но и исполнять программу на любом компьютере, имеющем также виртуальную машину Java. Решена проблема переносимости программ. Этими основополагающими решениями в языке Java фирмы Sun воспользовалась фирма Microsoft при разработке .NET технологии.

3.2. Общеязыковая среда выполнения CLR платформы .NET Одна из причин появления .NET технологии - упростить взаимодействие компонентов, написанных на разных языках программирования, и обеспечить надёжность и безопасность программного кода. Для этого компиляторы языков .NET платформы транслируют исходные программы на язык MSIL. Промежуточный язык MSIL (Microsoft Intermediate Language) - это язык "виртуальной машины" .NET платформы. Представленные на одном и том же языке MSIL, программные компоненты теряют особенности, присущие разным языкам программирования. Единым образом представлены не только программы и данные, но и дополнительная информация, описывающая классы и интерфейсы для связи с другими компонентами. Общеязыковая среда выполнения CLR (Common Language Run) управляет выполнением машинного кода виртуальной машины, осуществляя также связь между компонентами и контролируя размещение данных в памяти, обеспечив тем самым безопасность работы каждого компонента и, как следствие, программы из компонентов.


3.3. Управляемый и неуправляемый код и данные Для обеспечения безопасности программного кода и данных на .NET и Java платформах используется управляемый код (managed code) и управляемые данные (managed data). На .NET платформе управляемый код может быть использован на любом языке программирования .NET платформы и оттранслирован соответствующим компилятором на промежуточный язык MSIL. C# и J#. Языки C# и J# - родные языки .NET платформы, и их исходные программы и данные без специальных указаний естественно компилируются на язык MSIL. Java. Исходная программа языка Java компилируется на промежуточный язык - байт-код, с которого затем интерпретируется виртуальной машиной Java. Язык Java имеет дело с управляемыми данными и кодом. С++. Язык С++ не является языком .NET платформы и не создаёт управляемого кода и управляемых данных. Все данные являются неуправляемыми и хранятся в стеке или неуправляемой куче. C++.NET и C++/CLI. Язык C++.NET (или Managed С++) специально был разработан для .NET платформы посредством включения в язык С++ специальных ключевых слов, составляющих управляемое расширение (managed extension), позволившее интегрироваться в общеязыковую среду выполнения CLR. Например, классы, помеченные ключевым словом gc, считаются управляемыми и их объекты будут помещены посредством оператора new в память, называемую управляемой кучей (managed heap). Специальный сборщик мусора GC (garbage collector) надзирает за управляемой кучей, удаляя из неё данные, необходимость в которых исчезла. Неуправляемые данные (unmanaged data) находятся в стеке или размещаются с помощью оператора new в неуправляемой куче (unmanaged heap или heap С++), из которой обязаны в дальнейшем удаляться с помощью оператора delete. Достоинством языка C++.NET является то, что программисты на языке C++.NET могут в пределах одного программного кода разделить данные на управляемые и неуправляемые и сделать свою программу более эффективной, поскольку неуправляемые системой данные быстрее обрабатываются и их существование определяется только программистом, а не сборщиком мусора. Для связи управляемых и неуправляемых фрагментов программы используется особый объект прокси ^>roxy). Язык C++/CLI является дальнейшим развитием языка C++.NET, внесшим большую ясность при создании управляемого кода. Модификация коснулась не только языка, но и его лучшей реализации применительно к срсдс CLR. Исчезли введённые в языке C++.NET ключевые слова, начинающиеся с


двух подчёркиваний gc, nogc, delegate, event, value и другие. В языке C++/CLI введены другие слова, связанные с управляемым кодом и убраны в них подчёркивания, так что вместо ключевого слова gc теперь перед управляемым классом ставится ref, вместо двух слов gc new используется одно слово gcnew. Теперь управляемый указатель (дескриптор) начинается с символа Л , а не с символа *, как было в языке C++.NET. Об особенностях языка C++/CLI и используемых в нём операторах и ключевых словах будет изложено в дальнейшем. Читателям, пожелавшим познакомиться подробнее с языком C++.NET (и C# ) и программированием на этом языке, можно порекомендовать книги автора "Программирование на С++, C++.NET/C# и .NET компоненты" и ".NET компоненты, контейнеры и удалённые объекты ".

3.4. Сборщик мусора Сборщик мусора GC (garbage collector) предназначен для управления распределением памяти внутри управляемой кучи. Сборщику мусора известно размещение управляемых данных в куче. Каждый новый управляемый объект помещается в свободном пространстве кучи, и адрес объекта присваивается соответствующему управляемому указателю (managed pointer). Если свободного пространства будет недостаточно, то сборщик мусора удалит уже неиспользуемые данные, уплотнив кучу. При этом будут изменены ссылки в управляемых указателях на перемещённые данные. Сборщик мусора оперирует только с управляемыми типами (managed types). Иногда сборщик мусора может переслать данные из кучи в стек, если данные невелики и имеют малое время жизни. При этом перемещённые данные изменяют свой управляемый тип на тип-значение (value type). Следя за управляемыми указателями и данными, сборщик мусора способствует безопасности программы. Он по мере надобности уплотняет управляемую кучу, не допуская её переполнения, которое возможно было бы в неуправляемой куче по вине программиста.

3.5. Ссылочные типы и типы-значения C# и J#. В языке C# типы-значения размещаются в стеке, а управляемые данные, созданные посредством оператора new, в управляемой куче. Сам компилятор соотносит данные программы к тому или иному типу. Переменные, ссылаемые к управляемым данным, имеют ссылочный тип. Остальные переменные имеют тип-значение. С++. В языке С++ нет управляемых типов. Компилятор помещает данные, созданные оператором new, в неуправляемой куче. Остальные данные про-


граммы помещаются в стеке. К данным кучи осуществляется ссылка с помощью указателей. C++/CLI. В языке C++/CLI, являющемся расширением языка С++, программист по своему желанию может отнести данные как к неуправляемым типам, так и к управляемым различным типам, используя ключевые слова gcnew, delegate, event, property или ключевые слова с пробелами enum class, enum struct, interface class, ref class, ref struct, value class, value struct. В соответствии с его пожеланиями компилятор создаст их надлежащим образом. Данные, не помеченные ключевыми словами, считаются неуправляемыми данными, то есть данными языка С++. В языке C++/CLI неуправляемые указатели, указывающие на неуправляемые данные, размещённые в неуправляемой куче, помечаются символом "*", а управляемые указатели, ссылающиеся на управляемые данные в управляемой куче, помечаются символом " Л ". В дальнейшем мы неуправляемые указатели будут называть указателями, а управляемые указатели будем называть дескрипторами. В отличие от указателей, жестко привязанных к конкретным областям неуправляемой кучи, значения дескрипторов могут меняться. Сборщик мусора, удаляя непотребные данные из управляемой кучи, меняет значения дескрипторов и уплотняет при этом кучу. Итак, если нам необходимы объекты, размещаемые в управляемой куче, то необходимо при описании класса этих объектов применить пару слов ref class, а при создании объектов применить оператор gcnew и дескриптор, помеченный символом " Л ". Управляемые классы могут включать любые управляемые данные. Ссьшки к управляемым данным (объектам) осуществляются только с помощью дескрипторов. При копировании управляемых данных (ссылочных данных), само данное в управляемой куче не перемещается, а создаётся новый дескриптор (ссылка) на него. Данные типа значения размещаются в стеке, При копировании эти данные копируются в новую область стека. Обычно данные типа значения не громоздки, поскольку они размещены не в куче, а в программе, и поэтому увеличивают размер программы. В качестве значений представляют наиболее часто используемые данные, поскольку работа со стеком быстрее, чем с кучей. В качестве таких данных представлены все базовые данные типа int, char, bool, double, float и др. При желании эти данные могут быть размещены и в куче путем применения специальных классов-оболочек, например для размещения в куче символа типа char применяется специальный классоболочка Char. Язык C++/CLI содержит ключевые слова с пробелами value class и value struct для представления данных типа значение. В примере 3.5.1 показано создание объектатипа-значения CBalll в стеке и в неуправляемой куче С++, а объекта типа class CBall2 в управляемой куче.


Пример 3.5.1. Использование стека, неуправляемой и управляемой куч на языке C++/CLI. //////////////////// // C + + / C L I #include "stdafx.h"

value

<

class C B a l l l

// Класс C B a l l l имеет тип-значение

private: int x, у; public: void Set (int vX, int vY) {x= vX; y= vY;} void Show ( ) {

>;

>

System::Console::WriteLine("x= " + x . T o S t r i n g ( ) + "y= " + y.ToString ( ));

ref c l a s s CBall2

{

// Класс CBall2 имеет управляемый тип

private: int x, у; public: void Set (intvX, int vY) {x= vX; y= vY;} void Show ( ) {

>;

>

System::Console::WriteLine("x= " + x . T o S t r i n g ( ) + "y= " + y,ToString ( ));

void main (void) { C B a l l l ball; ball.Set (1, 11); ball.Show ( );

// Создать объект в стеке

CBalll *pBalll= n e w CBalll ( ) ;

// Создать объект в неуправляемой // куче

pBall 1 -> Set (2, 22); pBalll -> Show ( ) ; CBall2 ^pBall2= g c n e w CBall2 ( ) ; pBall2 -> Set (3, 33);

// Создать объект в управляемой куче


pBall2

-> Show ( );

> /* Result: x= 1 y= 11 x= 2 y= 22 x= 3 y= 33 */

Компилятор языка C++/CLI контролирует правильность использования ключевых слов, чем значительно облегчает освоение .NET технологии. Сочетание обработки данных в стеке и неуправляемой куче с обработкой данных в управляемой куче в пределах одного программного кода делает язык C++/CLI уникальным, позволяющим разработать эффективные программы.

3.6. Метаданные Для связи между собой программные компоненты используют интерфейсы и типы, для чего среды выполнения должны располагать не только скомпилированными на промежуточный язык кодами, но и информацией об этих интерфейсах и типах. В действительности среды выполнения имеет дело с переносимыми исполняемыми файлами, в которых вместе с кодами помещаются метаданные, описывающие интерфейсы и типы. Метаданные генерируются компиляторами в соответствии с типами данных исходной программы. В языке C++/CLI, например, метаданные создаются в соответствии с указаниями ключевых слов управляемого расширения. Специальные средства отражения позволяют на языках C#, Java и C++/CLI одним компонентам программы получать информацию о других компонентах через их метаданные.

3.7. Библиотеки, пакеты и пространства имён Языки программирования используют библиотеки и пакеты, объединяющие классы и интерфейсы для реализации определённых действий в программе, например, ввода и вывода данных, рисования или формирования окон с различными интерфейсными элементами. Для упрощения ссылки к группе классов, эти классы в пределах библиотеки дополнительно объединяют в так называемое пространство гшеи. Пространство имён может включать другие пространства имён C++/CLI. Пространство имён, охватывающее требуемые классы и интерфейсы, объявляется в программном компоненте с помощью ключевого слова namespace и указания названия этого пространства. Для использования этого пространства имён другой компонент, написанный также на языке C++/CLI, должен воспользоваться оператором using namespace, назвав пространство имён.


При использовании пространств имён библиотеки .NET Framework Class Library на языке C++/CLI необходимо применить ряд директив, например, #using <mscorlib.lib>, #using <System.DLL>, #using <System.Windows.Forms.DLL>,

которые делают классы и интерфейсы этой библиотеки доступными. Посредством оператора using namespace можно упростить ссылку к пространствам имён, опуская в ссылке имена пространств, охватывающих указанное имя. Например, using namespace System;

требует ссылаться к классу Form, входящему в пространство имён Forms, которое в свою очередь входит в пространство имён Windows, как Windows::Forms::Form ^form=gcnew

Windows::Forms::Form();

Если же в начале программы указать пространство имён using namespace System::Wlndows::Forms;

то ссылка к этому классу упростится и будет следующей: Form ^form= gcnew

Form ();

C#. Пространство имён объявляется с помощью ключевого слова namespace и используется в других программных компонентах, написанных также на языке C#, посредством указания названия пространства в операторе using. Директива #using не используется. В отличие от языка C++/CLI в языке C# названия пространств имён отделяются не двумя двоеточиями, а точкой. Например: using System. Windows.Forms; Windows.Forms.Form form= new Windows.Forms.Form ( ) ;

Java и J#. В данной книге специально, чтобы использовать библиотеку, отличную от библиотеки .NET Framework, применительно к языку Java (J#) применяются библиотеки (пакеты) языка Java, для подключения которых употребляется оператор import. import java.lang.*; import java.awt,*; import java.util.*;

Этот же оператор применяется и при импортировании классов библиотеки .NET Framework в среде Visual Studio .NET для языка J#, например: Import java.System.*;


4. Некоторые особенности языков C++/CLI, C# и Java(J#), 4.1. О типах данных Типы данных языков C++/CLI, C# и Java содержат базовые типы языка С, такие как char, short, int, long, float и double. Эти типы применяются часто и поэтому данные этих типов хранятся в стеке, работа с которым наиболее эффективна. В языке Java введёны новые типы byte и boolean, а в языке C# логический тип bool. Данные типа bool и booIean принимают значения true (истина) и false (ложь). В языках C++/CLI, C# и Java кроме базовых типов, называемых типами по значению (value types), применяются ссылочные типы (reference types). В языке C++/CLI, как и в языке С, применяются указатели pointers), а также дескрипторы (descriptors). Указатели и дескрипторы используются для ссылки к данным, размещённым в куче - области памяти, расположенной вне программы. Если каждое данное, находящееся в стеке увеличивает программный код, то данные кучи не меняет размер программы. Поэтому большинство данных, особенно громоздких, желательно помещать в кучу. Язык C++/CLI использует неуправляемую кучу (unmanaged heap или С++ heap) и управляемую кучу (managed heap). Неуправляемая куча не управляется системой - размещение данных в неуправляемой куче полностью определяется программистом. Для размещения данных в неуправляемой куче в языке С++ применяются операторы new и delete, о которых будет рассказано в разделе 4.4.

Опыт программирования показал, что использование указателей приводит к массе ошибок, даже у опытных программистов. Например, при обращении к данным в куче неправильное приращение значения указателя приводит к обращению к другим данным кучи или за границы кучи. Часто также позабывают удалять уже ненужные данные, вызывая переполнение кучи. Учитывая только что сказанное, создатели языков Java и C# решили отказаться от неуправляемой кучи и заменили её управляемой кучей (managed heap). Данные в управляемой куче контролируются системой, которая также выявляет наличие уже непотребных данных и периодически их удаляет. В языках Java и C# в управляемую кучу помещаются только объекты. Если появляется необходимость заслать в управляемую кучу данное базового типа, то предварительно оно должно быть преобразовано в объект, для чего, например, в языке Java применяются специальные классы-оболочки Boolean,


Byte, Character, Short, Integer, Long, Float и Double, а в языках C# и C++/CLI - боксирование (boxing). Размещение данных в неуправляемой и управляемой кучах рассмотрено в разделе 4.4, а о применении классов-оболочек и боксировании (boxing) в разделе 6.6, Надо сказать, что язык C++/CLI платформы .NET включает специальные ключевые слова, позволяющие наряду с неуправляемыми данными создавать и применять управляемые данные. Как было сказано выше, язык C++/CLI содержит две кучи - неуправляемую и управляемую. Наличие управляемых кучи и данных делает возможным, применяя .NET технологию, создавать надёжные и безопасные программы. Если же появилась необходимость создать небезопасную, но эффективную программу, то язык C++/CLI позволяет воспользоваться неуправляемым кодом и неуправляемой кучей. При необходимости одна часть C++/CLI программы может состоять только из неуправляемого кода, а другая - из управляемого кода. Для связи этих частей применяют так называемые прокси Q)roxy) - особые объекты специальных классов-оболочек.

4.2. Консольный ввод и вывод C++/CLI и C#. Языки программирования C++/CLI и C# используют библиотеку .NET Framework, содержащую набор классов ввода/вывода. Среди классов пространства имён System есть класс Console, который содержит статические функции ввода/'вывода на консоль. Статическая функция static int Read ();

вводит символ как объект типа int, требуя преобразования его в тип char. Статическая функция static int ReadLine ();

вводит строку символов типа String, пока не будет нажата клавиша Enter. Для вывода используются статические функции: static void Write (String fmt, params object [ ] args); static void WriteLine (String fmt, params object [ ] args);

Первая функция выводит значения аргументов args в соответствии с указаниями строки форматирования fmt. Вторая функция дополнительно завершает вывод переводом строки. Пример 4.2,1 иллюстрирует особенности применения функций консольного ввода-вывода и их различия для языков C++/CLI, Java и C#. В примере 4.2.1 использованы целочисленная переменная и массив (строка) сим-


волов. Обратите внимание, что статическую функцию WtiteLine(), применяя, привязывают к классу через "::" на языке C++/CLI и через "." на языке C#. Как видно из примера, ввод данных на языке Java требует создания дополнительных объектов и обязательной обработки возможных ошибок при вводе некорректных данных. Встречающиеся символы "//" указывают компилятору, что последовательность символов после них на всей строке рассматривается как комментарий. Пример 4.2.1. Консольный ввод и вывод на языках C++/CLI, Java и C#. //////////////////// // C + + / C L I #include "stdafx.h" // Включить файл stdafx.h using namespace System; // Использовать пространство имён System void main (void)

{

String ^st; st= Console::ReadLine ( ); Console::WrlteLine ("s= ", st);

// Строка st // Ввести строку с консоли // Вывести строку на консоль

int x; // Целочисленная переменная x // Ввод целого числа в виде строки и преобразование её в число x x= System::Convert::ToInt32 (ConsoIe::ReadLine ( )); // Вывод преобразованного в строку числа x Console::WriteLine ( " x = " + x.ToString ( )); // Вариант 1 // Console::WriteLine ("x= {0} ", x.ToString ()); // Вариант 2 > /* Result: // Ввели abcde и 34 st= abcde x= 34 */

//////////////////// // C# using System; class Cs4_2_l { static void Main ( ) { String st; // Строка st st= C o n s o l e . R e a d L i n e ( ); C o n s o l e . W r i t e L i n e ( " s t = " + st); lnt x; // Целочисленная переменная x x=System.Convert,ToInt32 (Console.ReadLine ( )); x+= 10; C o n s o l e . W r i t e L i n e ( " x = " + x.ToString());


> /*

>

Result: // Ввели abcde и 34 st= abcde x= 44 */ //////////////////// // Java и J # importjava.io.*; public class Example_4_2_l < public static void main (String[] args) { String st; // Строка st try{ BufferedReader b R = n e w BufferedReader ( n e w I n p u t S t r e a m R e a d e r (System./n)); st= bR.readLine ( ); System.ouf. println ( " s t = " + st);

>

int x; // Целочисленная переменная x st= bR.readLlne ( ); x= I n t e g e r . p a r s e I n t (st); x+= 10; st= I n t e g e r . t o S t r i n g (x); System.ouf. println ("st= " + st);

catch (Exception e){ System.ouf. println ( " и с к л ю ч е н и е ");

>

>

>

/*

Result: // Ввели abcde и 34 st= abcde x= 44 */

Рис. 4.2. Консольный ввод и вывод Java и C#. В отличие от языка С++ (и C++/CLI) создатели языков Java и C# отразили и в языковых конструкциях объектно-ориентированный подход чрезвычайно активно используются как классы, так и объекты классов. Опи-


сание функций вне классов запрещено. Все функции, используемые в Java и C# программах, должны принадлежать каким-либо классам - даже такая особая функция, как Main() или main(). Из примера 4.2.1 видно, что C#компилятор создал для консольного приложения специальный класс Cs4_2_l и включил в этот класс главную функцию Main() программы, объявив её как статическую. Аналогично компилятор языка Java поместил в класс статическую открытую функцию main(). Так что программа на Java и C# представляет собой совокупность описаний классов. Выполнение программы начинается с выполнения главной функции main() или Main(), которая создаст из классов нужные объекты и заставит их функционировать надлежащим образом. При этом, разумеется, активно будут употребляться классы, используемые для ввода-вывода. Сразу скажем, что в Java и C# функция консольного ввода обычно используется редко, поскольку значительно удобнее вводить информацию посредством интерфейсных элементов, таких как, например, текстовый редактор, о котором будет рассказано позднее. Поэтому пусть не огорчает неудобное использование функции read() ввода символа и функции readLine() ввода строки символов на языке Java. Что касается функции print() или Write() вывода строки символов и функции println() или WriteLine() вывода строки с переводом строки на консоль, то они применяются часто, особенно в отладочных выводах. Кстати, их применение, как видно из примера 4.2.1, просто. Поскольку на консоль выводится строка, то обычно в C# и C++/CLI для преобразования аргументов иного типа в строку применяют функцию ToString(). В Java программе для обеспечения большей надёжности при применении функций ввода-вывода компилятор требует обязательного использования специальных try-catch конструкций для обработки возможных ошибочных ситуаций (исключений) при вводе данных. Обработка исключений будет рассмотрена подробнее позднее, а сейчас отметим, что часть программы, связанная с вводом и выводом, помещена в так называемый try блок, после которого размещён catch блок, который должен предпринять какие-либо меры для спасения программы от вынужденного аварийного завершения. В нашем случае catch блок, воспользовавшись функцией println(), перед аварийным завершении программы выдаст на консоль строку "исключение". На языке Java функция read() применяется громоздко - вначале по ссылке bR создаётся с помощью оператора new объект класса BufferedReader в управляемой куче, который, заметим, сам использует объект класса InputStreamReader, размещённый также в управляемой куче. Потом, воспользовавшись ссылкой bR, вызывается функция readLine() класса BufferedReader, возвращаемая которой строка, вводимая с консоли, присваивается строке st. Потом строка st выводится функцией println() класса out на консоль. Затем в виде строки с консоли вводится число как последовательность символов-цифр, которую потом функция parseInt() класса Integer преобразует в целое число. К этому числу прибавляется целое число 10. Прежде чем вывести полученную сумму на консоль, результативное число преобразуется функцией toString() класса Integer в строку.


Реакцию нашей программы на некорректный ввод числа выявить легко. Вместо числа введите любую строку, например ffffff, и на консоль будет выдано слово "исключение". Так наша программа среагировала на ошибку перед аварийным завершением её выполнения. Вместо слова "исключение" в catch блоке можно было бы сообщить об ошибочном вводе числа и попросить о повторном вводе, осуществив его с помощью функции readLine() повторно. Хотя компиляторы языков C# и C++/CLI не настаивают на применении try-catch конструкций, всё же применение их желательно. Обработке исключений в языках Java и C# уделяется особое внимание, поскольку Java и C# технологии ориентированы на разработку надёжных и безопасных программ. Применение try-catch конструкции при консольном вводе на языке C# иллюстрирует также пример 7.5,2. BC++/CLlHC# программахпримера4.2.1дляпростоты try-catch блоки не применены. В дальнейшем обработке исключений мы уделим дополнительное внимание. C# и C++/CLI. При вводе и выводе данных часто появляется необходимость преобразовать данные из одного базового типа в другой базовый тип. Здесь окажется полезным класс Convert, содержащий множество статических функций, преобразующих данные, например, ToBoolean(), ToByte(), ToChar(), ToDecimal(), ToDouble(), ToSingle(), ToString(), ToIntl6(), ToInt32(), ToInt64() и другие. В примере 4.2.1 программа на языке C++/CLI преобразует введённую строку (последовательность символов-цифр) в число посредством статической функции Convert:: ToInt32(). Java и J#. Библиотека языка Java содержит классы Boolean, Byte, Character, Double, Float, Integer и Short, включающие функции преобразования типов базовых данных. Например, класс Integer включает функции byteValue(), doub!eValue(), floatValue(), intValue(), longValue(), toString(), parseInt() и др. Иные классы включают аналогичные функции. В программе на языке Java примера 4.2.1 введённая строка преобразуется в число функцией parseInt(), а из числа в строку функцией toString().

4.3. Объявление переменных Так же, как и в языке С, все переменные перед их использованием должны быть объявлены. В языках С++ и C++/CLI можно объявить переменную в любом месте программы перед её применением, где это необходимо. В языках Java и C# переменная может быть объявлена только в классе или в блоке. Переменную можно объявить как в начале блока, так и перед её использованием. Объявление переменной действует до конца блока, внутри которого она была объявлена, как это показано в примере 4.3.1.


Пример 4.3.1. Объявление переменной в середине блока. < // Начало блока for ( i n t i=0; i <= length; i++) // Начало действия объявления переменной /' s[i] = '\0'; > // Конец блока и конец действия объявления переменной /

4. 4.

Операторы динамического распределения памяти new и delete

Вместо функций malloc(), calloc() и free() распределения памяти языка С в языке C++/CLI применяются более интеллектуальные операторы new, gcnew и delete, а в языках Java и C# только оператор new. C++/CLI. Операторы new и gcnew используются для распределения памяти в куче, а оператор delete - для освобождения памяти, распределённой ранее оператором new. Используя операторы new и gcnew, можно распределять память для одного объекта, массива объектов любого типа, в том числе типа, определённого программистом (структура, класс), вне программы в так называемой куче. Неуправляемые данные с помощью оператора new помещаются в неуправляемую кучу, а управляемые данные с помощью оператора gcnew - в управляемую кучу. В случае неуправляемых данных оператор new возвращает адрес начала отведённой памяти или 0 в случае неудачи. Этот адрес присваивается неуправляемому указателю. Значение, возвращаемое оператором new, не требует явного приведения к типу указателя, которому присваивается результативный адрес. Память в неуправляемой куче, полученная по new, остаётся распределённой, пока она явно не будет освобождена оператором delete. Распределение памяти, то есть управление неуправляемой кучей, полностью лежит на совести программиста. Эта процедура ответственна - чтобы сделать программу эффективной, программист должен сам отслеживать время жизни каадого объекта и удалять объекты из кучи по мере прекращения их функционирования, применив оператор delete. В случае неуправляемых данных размещение данных (объектов) в управляемой куче осуществляется специальной программой сборщиком мусора. Объекты помещаются в кучу при выполнении оператора gcnew, а удаляются сборщиком мусора. Ссылка к распределённым данным осуществляется с помощью управляемых указателей, называемых дескрипторами. Объявление дескриптора отличается от объявления (неуправляемого) указателя: неуправляемый-тип *указатель; // указатель на данные в неупр. куче


управляемый-тип

л

дескриптор; // дескриптор на данные в упр. куче

Использование указателя позволяет изменения его значения при доступе к указанным данным неуправляемой кучи, что может привести к выходам за дозволенные границы и, как результат, к аварийному завершению программы. Применение же дескриптора запрещает изменения его значения, что обеспечивает защиту данных в управляемой куче. То есть, как в языках Java и C#, опасный указатель применительно к управляемым данным заменён неизменяемой ссылкой. Java и C#. Язык Java и C# используют оператор new при создании переменных ссылочного типа. Эти переменные-ссылки объявляются, как и обычные переменные, но они содержат ссылки на области управляемой кучи, выделяемые сборщиком мусора с помощью оператора new. Данные (объекты), размещённые в управляемой куче, становятся управляемыми данными, и они контролируются сборщиком мусора. Сборщик мусора периодически просматривает объекты, размещённые в управляемой куче, и удаляет из неё все ненужные объекты, то есть объекты, ссылки на которые обнулены. Таким образом, в языках Java и C# нет необходимости в операторе delete. Оператор delete в языках Java и C# отсутствует. Наличие сборщика мусора решило болезненную проблему, связанную с переполнением кучи - программисты часто забывали удалять размещённые в куче объекты. Пример 4.4.1 иллюстрирует выделение и освобождение памяти для переменных и объектов для языка C++/CLI, Пример 4.4.1. Использование операторов ncw, gcnew и delete в языке C++/CLI. //////////////////// // C + + / C L I #include "stdafx.h" class CFI

{

float fl;

public:

void Set

>;

// Описание неуправляемого класса // Закрытая (по умолчанию) переменная fl // Открыть функции (float vFI) {fl- vFI;}// Установить значение переменной fl

float Get (){return fl;>

ref class CBool { bool b;

// Получить значение закрытой переменной fl

// Описание управляемого класса // Закрытая (по умолчанию) переменная b

public: // Открыть функции void Set (bool vB) {b= vB;} // Установить значение переменной b bool Get ( ) {return b; >

// Получить значение закрытой переменной b


>; void main ( ) < // В ы д е л и т ь п а м я т ь д л я ц е л о ч и с л е н н о г о д а н н о г о в стеке int x; // Выделить память в стеке x= 5; Ц Установить значение System::Console::WriteLine ("x= {0}", x,ToStrlng()); // Вывести // значение переменной x на консоль // В ы д е л и т ь п а м я т ь д л я о б ъ е к т а н е у п р а в л я е м о г о класса CFI в // стеке CFI cFts; // Выделить память в стеке cFls.Set (0.01F); // Установить значение переменной fl System: :Console: :WrlteLine ("cFls.fl= {0>", cFls.Get().ToStrlngO); // Вы// вести значение переменной fl на консоль // В ы д е л и т ь п а м я т ь д л я ц е л о ч и с л е н н о г о д а н н о г о в куче int *pX= new lnt; // Выделить память в неуправляемой куче *pX= 55; // Установить значение System::Console::WrlteLlne ("*pX= {0>", (*pX).ToString()); // Вы// вести значение на консоль // В ы д е л и т ь п а м я т ь д л я о б ъ е к т а н е у п р а в л я е м о г о класса CFI в куче CFI *pCFI= new CFI; // Выделить память в неуправляемой куче pCFI -> Set (0,02F); // Установить значение System::Console::WrlteLine ("pCFI -> f l = <0>", (pCFI -> Get()).ToStrlng()); // Удалить данные из неуправляемой кучи delete pX; // Удалить целочисленное данное из неуправляемой кучи delete pCFI; // Удалить объект класса CFI из неуправляемой кучи

>

// В ы д е л и т ь п а м я т ь д л я о б ъ е к т а у п р а в л я е м о г о к л а с с а CBool в куче CBool ^pCBool= gcnew CBool ( ) ; // Выделить память в управляемой куче pCBool -> Set (true); // Установить значение System: :Console: :WrlteLine ("pCBool -> b= {0>", (pCBool -> Get()).ToString()); delete pCBool; // Нет ошибки

/*

Result: x= 5 cFls.fl= 0,01 *pX= 55 pCFI -> f l = 0,02 pCBool -> b= true */

Программа примера 4.4.1 начинается с оператора включения файла stdafx.h, содержащего наиболее часто используемые константы и функции. Затем помещено описание простого неуправляемого класса CF1, содержащего переменную fl с плавающей точкой и две открьггые функции Set() и Get(). Интерфейсная функция Set() устанавливает значение закрытой переменной fl, а интерфейсная функция Get() получает её значение. Затем описан управляемый класс CBool, содержащий булевскую переменную b и функции уста-


новки и получения её значения. Обратим внимание - при описании управляемого класса перед словом class стоит ключевое слово ref. Главная функция main() программы вначале выделяет в стеке память под целочисленную переменную x и под объект cFls класса CF1. Если в C++/CLI возможно размещение объекта в стеке, то в языках Java и C# это запрещено. В языках Java и C# объекты размещаются только в управляемой куче. Дальше функция main() примера 4.4.1 размещает целое числа и объект класса CF1 в неуправляемой куче. Обращение к этим данным осуществляется через их указатели pX и pCFl, которым оператор new присвоил адреса соответствующих областей кучи. Указатель pX применён при присвоении значения целому числу в куче и при получении этого значения в функции WriteLine(). Указатель же pCFl применяется при вызове интерфейсных функций Set() и Get(). Перед завершением своего выполнения функция main() удаляет данные из кучи, воспользовавшись оператором delete. Перед завершением выполнения функция main() создаёт объект класса CBool в управляемой куче. Выделение области в управляемой куче и создание в ней объекта осуществляет оператор gcnew. Та же программа без класса CBool на языках Java и C# приведена в примере 4.4.2. В языках Java и C# все классы являются управляемыми, оставим в примере только один управляемый класс CF1. Пример 4.4.2. Использование оператора new в языках Java и C#. //////////////////// // Java и J # import java,io.*; class CFI

// Описание класса с переменной и двумя функциями

{

private

floatfl;

// Закрытая переменная fl

publicvoid Set (float vFI) {fl= vFI;} // Установить значение переменной fl

>

public float Get ( ) {return fl;>

// Получить значение переменной fl

public class Exampie_4_4_2 // Описание класса с функцией main() { public static void main ( ) { // В ы д е л и т ь п а м я т ь д л я ц е л о ч и с л е н н о г о д а н н о г о в стеке int x; // Выделить память в стеке для переменной x= 5; // Установить значение System.out. println ("x= " + x); // В ы д е л и т ь п а м я т ь д л я ц е л о ч и с л е н н о г о д а н н о г о в куче int sX= new Integer (x); || Выделить память для объекта // в управляемой куче. sX= S5; // Установить значение


System.ouf. prlntln ("sX= " + sX);

> /*

>

// В ы д е л и т ь п а м я т ь д л я о б ъ е к т а к л а с с а CFI в к у ч е CFI sCFI= new CFI (); // Выделить память // в управляемой куче. sCFI.Set (0.01F); // Установить значение System.out. println ("sCFI.fl= " + sCFI.Get());

Result: x= 5 sX= 55 sCFI.fl= 0.01 */

//////////////////// / / c #

using System; class CFI

{

// Описание класса с переменной и двумя функциями

float fl; // Закрытая переменная fl publicvold Set (float vFI) {fl= vFI;} // Установить значение переменной fl

>

public float Get ( ) {return fl;>

// Получить значение переменной fl

class Example_4_4_2 // Описание класса с функцией Maln() { static void Main ( ) { // В ы д е л и т ь п а м я т ь д л я и е л о ч и с л е и н о г о д а н н о г о в с т е к е int x; // Выделить память в стеке для переменной x= 5; // Установить значение Console.WriteLine ("x= " + x); // В ы д е л и т ь п а м я т ь д л я ц е л о ч и с л е н н о г о д а н н о г о в к у ч е int sX= new int(); // Выделить память // в управляемой куче. sX= 55; // Установить значение Console.WriteLine ("sX= " +sX);

>>

>

/* Result:

// В ы д е л и т ь п а м я т ь д л я о б ъ е к т а к л а с с а CFI в куче CFI sCFI= new CFI ( ); // Выделить память П в управляемой куче. sCFI.Set (0.01F); // Установить значение Console.WriteLine ("sCFI.fl= " + sCFI.Get ( ));


x= 5 sX= 55 sCFI.fl= 0,01 */

Java. Программа примера 4.4.2 на языке Java вначале импортирует riaKeTjava.io, содержащий классы ввода-вывода. Использование звёздочки после точки означает о доступности всех классов пакета в данной программе. Затем следует определение класса CF1, в котором в отличии от аналогичного класса С++ программы вместо метки доступа public: перед объявлениями интерфейсных функций помещены ключевые слова public и определение класса не завершается точкой с запятой. Функция main(), как принято в языке Java, размещена в классе. В начале функции main() объявляется переменная x. Для неё выделяется память в стеке. Функция println() выдает на консоль присвоенное этой переменной значение 5. Затем объявляется ссылка sX типа int, которой присваивается значение оператором new, возвратившим адрес созданного объекта класса-оболочки Integer в управляемой куче. Объекту присваивается значение 55, которое выводит на консоль функция println(), используя ссылку sX. В конце тела функции main() создаётся объект класса CF1. По ссылке sCFl вызывается функция Set() этого объекта для установки значения закрытой переменной fl. Затем эта же ссылка sCFl применяется к функции Get() для получения значения закрытой переменной fl, используемого функцией println() при выдаче на консоль. C#. В начале программы примера 4.4.2 на языке C# указывается используемое пространство имён System библиотеки .NET Framework, включающее наиболее употребительные класса и, в частности, класс Console, содержащий функцию WriteLine() вывода на консоль. Затем определяется класс CF1, который, как видим, полностью совпадает с классом CF1 для языка Java. Как и в случае языка Java, главная функция Main() находится в классе Example_4_4_2. С небольшими отличиями в функции Main() выполняются те же действия, что и в функции main() программы на языке Java. Язык C# позволяет создать объект базового типа, используя непосредственно этот тип. При создании целочисленного объекта был использован тип int. Вывод значений данных на консоль осуществила функция WriteLine() класса Console.

4.5. Массивы Если в языке C++/CLI массивы в зависимости от типа данных могут быть размещены по желанию программиста в стеке, неуправляемой или в управляемой кучах, то в языках Java и C# массивы всегда размещаются посредством оператора new только в управляемой куче. В языках Java и C#


массив является объектом, который указывает на множество других объектов или базовых типов данных. Рассмотрим создание одномерных массивов на языках C++/CLI, Java и C#. C++/CLI. В языке C++/CLI массив размещается в стеке, если он объявлен как тип

имя_массива

[ размер ] ;

Ссылка к такому массиву осуществляется с помощью индексированной переменной имя_массива

[ индекс J,

Допускается инициализация массива при его описании: тип

имя_массива

[ размер ]= < список инициализации >;

где список инициализации состоит из значений элементов массива, отделённых друг от друга запятыми. Для размещения массива в неуправляемой куче используется оператор new и указатель, которому оператор new присваивает адрес этого размещённого массива: тип *указатель-массива = new

тип

[ размер ] ;

При ссылке к элементу массива по указателю применяется звёздочка и индекс этого элемента в массиве: *( указатель + индекс ).

Но дозволяется применить ссылку с индексированной переменной имя_массива

[индекс].

Массив неуправляемых данных, созданный с помощью оператора new, удаляется из кучи оператором delete. Описание массива управляемых объектов необычно. Для размещения массива в управляемой куче используется ключевое слово array, оператор gcnew и дескриптор, которому оператор gcnew присваивает ссылку на этот массив: аггау<тип [, разм]>

А

дескриптор-массива =

дескриптор-массива; gcnew

аггау<тип[, разм]> ^лина [, длина] .,.);

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


дескриптор-массива - управляемая ссылка (дескриптор) на массив.

При ссылке к элементу массива применяется дескриптор и индексы этого элемента в массиве: дескриптор-массива [индекс [ , индекс] ...].

Описание одномерного управляемого массива выглядит так: аггау<тип> А дескриптор-массива; дескриптор-массива = gcnew аггау<тип> (длина);

Заметим, что количество элементов массива (длина) указывается в круглых скобках. После создания управляемый массив надлежит инициализировать, например одномерный массив так array<lnt>^ ints= gcnew array<lnt>(2); ints[0]=50; ints[l]=60;

или при создании array<int>^

intsl= gcnew array<int>(2){50, 60};

А двумерный массив таким образом array<float,2>^ floats= gcnew array<float,2> (2,3); floats[0,0]=l; floats[0,l]=2; floats[0,2]=3; floats[l,0]=4; floats[l,l]=5; floats[l,2]=6; или при создании array<float.2>^ floats 1; floatsl= gcnew array<float,2> (2,3){{l,2,3},{4,5,6}}; Следует обратить внимание на то, что при создании массива объектов в куче оператор gcnew выделяет память под массив ссылок. Поэтому требуется ещё инициализировать каждую из этих ссылок путем создания самих объектов в куче с помощью соответствующих операторов gcnew. Java и C#. Переменная массива в языках Java и C# является переменной ссылочного типа. Объявление массива отличается от объявления в языке C++/CLI: тип [ ] ссылочная_переменная =

new

тип

[ размер ];


Заметим, что квадратные скобки размещены между типом тип элементов массива и ссылочной переменной ссылочная переменная. А количество размер элементов массива помещается в квадратные скобки после оператора new и типа. Ссылка к такому массиву осуществляется посредством ссылочной переменной с индексом: ссылочная_переменная

[ индекс ].

Массив - это управляемое данное (объект) языков Java и C#. Когда массив создаётся с помощью оператора new, он размещается сборщиком мусора в управляемой куче и удаляется при ненадобности сборщиком мусора. В языке C# любой массив наследуется из базового класса Array и поэтому при использовании массивов можно воспользоваться удобными свойствами и функциями класса Array, например, свойством Length, определяющим длину массива (количество элементов в массиве). В языке Java каждый объект массива содержит переменную length, определяющую длину массива. Языки Java и C# допускают инициализацию массива при его создании: тип [ ] ссылочная_переменная=

< список_инициализации >;

Язык Java разрешает ещё такую инициализацию массива при его создании: тип [ ] ссылочная_переменная= new тип [ ] { список_инициализации >;

Пример 4.5.1. Описание одномерных неуправляемых массивов на языке C++/CLI. /////////////// // C + + / C L I #include "stdafx.h" void main ( ) < float masl[3]= { l . l F , 1.2F, 1.3F>; // М а с с и в в стеке for (int i= 0; i < 3; i++) System: :Console::Write(" m a s l [" + i.ToString() + "]= " + masl[i].ToString()); System::Console::WriteLine (); float *pMas2= new float [3]; // М а с с и в в н е у п р а в л я е м о й к у ч е *pMas2= 2.1, *(pMas2+l)= 2.2; *(pMas2+2)= 2.3; for (int i= 0; I < 3; I++) System::Console::Write(" pMas2 [" + i.ToString() + "]= " + pMas2[i] ,ToString()); delete pMas2; > /* Result: masl[0]= 1,1

m a s l [ l ] = 1,2

masl[2]= 1,3


pMas2[0]= 2,1 pMas2[l]= 2,2 pMas2[2]= 2,3 */

C++/CLI. В примере 4.5.1 созданы два одномерных массива с плавающей точкой на языке C++/CLI в соответствии с правилами, приведёнными в начале этого раздела, с инициализацией и без инициализации членов массивов. Один массив размещён в стеке, другой - в неуправляемой куче. Пример 4.5.2 иллюстрирует создание и инициализацию массива в языках Java и C#. Пример 4.5.2. Описание одномерных массивов на изыках Java и C#. //////////////////// // C# using System;

class Example_4_5_2

{

static vold Main ( ) { float [ ] masl= new float [3]; masl[0]= l , l F ; m a s l [ l ] = 1.2F; masl[2] = 1.3F; for (lnt 1= 0; I < 3; I++) Console.Write (" masl[" + i.ToString() + »] = " + masl[i].ToString ()); Console.WriteLine (); float [ ] mas2= new float [3] {2.1F, 2.2F, 2.3F>; for (int 1= 0; i < 3; i++) Console.Write (" mas2[" + i.ToString() + "J= " + mas2[i].ToString ()); Console.WriteLine ( ) ; float [ ] mas3= { 3.1F, 3.2F, 3.3F>; for (int i= 0; i < mas3.Length; i++) Console.Write (" mas3[" + i.ToString() + "]= - + mas3[i].ToString ( ));

} /*

>

Result*

masl[0]= 1.1 m a s l [ l ] = 1.2 masl[2]= 1.3 mas2[0]= 2.1 mas2[l]= 2.2 mas2[2]= 2.3 mas3[0]= 3.1 mas3[l]= 3.2 mas3[2]= 3.3 */ //////////////////// // Java и J# importjava.io.*; public class Example_4_5_2 < public static void main ( ) { float [ ] masl= new float [3]; masl[0]= l . l F ; m a s l [ l ] = 1.2F;

masl[2]= 1.3F;


for (int i= 0; i < 3; i++) System.out. print (" masl["+ i + "]= " + masl[l]); System.out. println ( );

> /*

>

float [ ] mas3= { 3.1F, 3.2F, 3.3F>; for (int 1= 0; I < mas3.length; i++) System.ouf. print (" mas3["+ i + "]= " + mas3[i]);

Result: masl[0]= 1.1 m a s l [ l ] = 1,2 masl[2]= 1.3 mas3[0]= 3.1 mas3[lj= 3,2 mas3[2]=3.3 */

Java. В примере 4.5.2 созданы два одномерных массива с плавающей точкой на языке Java. Если в первом массиве masl присваивается значения поэлементно, то второй массив mas3 инициализируется списком. При выдаче значений элементов массива masl на консоль в операторе for переменная цикла i сравнивается с числом 3, равным длине массива. В операторе же for для массива mas3 вместо числа используется переменная length массива. В аргументе функции print() вывода строки на консоль формируется выводимая строка путём объединения её частей, соединённых операцией "+". C#. Программа на языке C# включает три массива, два из которых инициализированы так же, как и массивы программы на языке Java. Массив mas2 инициализирован иначе. Оператор for, выводящий элементы массива mas3, использует свойство Length, определяющее длину массива. Это свойство является свойством класса Array, из которого наследуется любой массив на языке C#. Из программы видно, что в отличие от функции print() языка Java функция Write() класса Console требует преобразования не строковых значений аргумента в строку с помощью специальной функции ToString(). В примере 4.5.3 показано создание массивов из объектов на языке C++/CLI. Пример 4.5.3. Создание массивов объектов на языке C++CLI. /////////////// // C + + / C L I #include "stdafx.h" class CFI

// Н е у п р а в л я е м ы й к л а с с

float fl; public:

// Закрытая переменная с плавающей точкой

{

vold Set (float vFI) {fl= vFI;} // Установить значение переменной fl >;

float Get ( ) {return fl;>

ref class CBool

// Возвратить значение переменной fl

// У п р а в л я е м ы й класс


bool b; public:

// Закрытая булевская переменная b

void Set (bool vB) {b= vB;} // Установить значение переменной b

>;

bool Get ( ) {return b;}

// Возвратить значение переменной b

void main ( ) { // размещение массива в стеке CFI m l [2 ]; // Массив объектов в стеке m l [0].Set ( l . l F ) ; m l [l].Set (1.2F); System::Console: :Write ( " m l [0].fl= " + ( m l [0].Get( )).ToString ( )); System: :Console: :WriteLine (" m l [l].fl= {0>", ( m l [l].Get ( )).ToString ( )); // размещение массивов в неуправляемой

куче

CFI *m2 = new CFI [2];; // Массив объектов в неуправляемой куче m2[0].Set (2.1F); m2[l].Set (2.2F); System::Console: :Write ("m2[0].fl= {0> ", (m2[0].Get ( )).ToString()); System: :Console: :WriteLine ("m2[l].fl= " + (m2[l].Get()).ToString()); CFI *m3 [2]; // Массив указателей на объекты в неуправляемой куче m3[0]= new CFI (); m3[l]= new CFi (); // Создать объекты в куче m3[0] -> Set (3.1F); m3[l] -> Set (3.2F); System::Console::Wrlte ("m3[0]->fl= " + (m3[0] -> Get ()).ToString()); System: :Console::WriteLine ("m3[l]->fl= " + (m3[l]->Get()).ToStrlng()); // размещение массива в

>

управляемойкуче

array<CBool^> ^m4 = gcnew array<CBool^> (2); // Массив // управляемых указателей на объекты в управляемой куче //Присвоение управляемым указателям(дескрипторам) ссылок на объекты m4[0]= gcnew CBool(); m4[l]= gcnew CBool ( ); // Создать объекты m4[0] -> Set (true); m4[l] -> Set (false); System: :Console::Write ("m4[0]->b= " + (m4[0] -> Get ( )).ToStrlng()); System::Console::WriteLine ("m4[l]->b= " + (m4[l] -> Get 0).ToString());

/*

Result: ml[0].fl= 1,1 m2[0].fl= 2,1 m3[0]->fl= 3,1 m4[0]->b= True */

ml[l].fl= 1,2 m2[l].fl= 2,2 m3[l]->fl= 3,2 m4[i]->b= False

В программе примера 4.5.3 созданы массив ml из двух объектов типа CF1 в стеке, массивы m2 и m3, каждый из двух объектов типа CF1, в неуправляемой куче и массив m4 из двух объектов типа CBool в управляемой куче. По-


сле выделения памяти присваиваются значения закрытым переменным fl и b с помощью интерфейсной функции Set() соответствующих объектов. А при выводе значений в функциях Write() или WriteLine() к объекту массива применяется интерфейсная функция Get(), возвращающая значение закрытой переменной fl или b. Как видно из примера 4.5.3, применение управляемых массивов, оперирующих с дескрипторами, отличается от применения неуправляемых массивов. На языках C# и Java , имеющих дело только с управляемыми объектами, размещаемыми в управляемой куче, использование массивов подчинено единым правилам и, конечно, проще, чем на языке C++/CLI. Это иллюстрирует пример 4.5.4. Пример 4.5.4. Создание массивов объектов на языке Java. //////////////////// // Java и J# import java.io.*; class CFI {

>

private

float fl;

// Закрытая переменная с плавающей точкой

public

void Set (float vFI) {fl= vFI;}

public

float Get ( ) {return fl;>

public class Example_4_5_4 < public static void main ( ) { // Создать м а с с и в о б ъ е к т о в в у п р а в л я е м о й куче CFI [] m l ; // Создать массив (управляемых) ссылок на объекты m l = new CFI [2]; // Создать объекты в (управляемой) куче ml[0]= new CFI ( ) ; m l [ l ] = new CFI ( ) ; // Установить значения переменной fl объектов массива m l [0].Set ( l . l F ) ; m l [l].Set (1.2F); // Вывести значения переменной fl объектов массива System.oot.prlnt ( " m l [0].fl= " + m l (0].Get ()); System.out,prlntln (" m l [l].fl= " + m l [l].Get ()); // Создать м а с с и в о б ъ е к т о в с его и н и ц и а л и з а ц и е й // Создать первый объект в (управляемой) куче CFI m 2 _ l = new CFI ( ); m2,_l.Set (2.1F); // Создать второй объект в (управляемой) куче CFI m2_2= new CFI ( ); m2_2.Set (2.2F); // Создать и инициализировать массив объектов CFI [] m2= {m2_l, m2_2}; // Вывести значения переменной fl объектов массива System.out.print ("m2 [0].fl= " + m2 [0].Get ( ));


> /*

System.out.print (" m2 [l].fl= " + m2 [l].Get ());

>

Result: m l [0].fl= l . l

m l [l].fl= 1.2

m2 [0].fl= 2.1 m2 [l].fl= 2.2 */ В примере 4.5.4 вначале создаётся массив ml ссылок к двум объектам типа CF1, с каждой из которых затем связывается отдельно созданный с помощью оператора new объект, размещённый в куче. Затем переменной каждого объекта функция Set() присваивает значение, а функция Get() выводит присвоенное значение на консоль. Массив m2 создан иначе. Вначале создаются объекты m2_l и m2_l типа CFI в куче, а потом они инициализируются в массив m2.

4.6. Объявления структур и перечислений В отличие от языка С при объявлении переменных типа структура и перечисление в языках C++/CL1 и C# не надо использовать ключевые слова struct и enum. В языке Java структуры отсутствуют. Пример 4.6.1. Описание структур в языках C++/CLI и C#. //////////////////// // C++/CLI struct studentl

// Описание н е у п р а в л я е м о й структуры studentl

{

>;

char name [20]; int birth;

studentl ourStudentl;

// Объявление переменной типа studentl (в стеке).

value

// Описание структуры student2 т и п а з н а ч е н и я

{

>;

struct student2

array<char> ^name; lnt birth;

student2 ourStudent2;

// Объявление переменной типа student2 (в стеке).

ref struct student3 {

// Описание управляемой структуры studer.t3

};

array<char> ^name; lnt blrth;

student3 ^ourStudent; // Объявление переменной типа student3 (в куче).


ourStudent= gcnew student3;

//////////////////// //C# struct student

<

>

// Описание структуры

student

public char [ ] name; public int birth;

student ourStudent;

// Объявление переменной типа student на языке C#

4.7, Ссылки Ссылки - новый тип неуправляемых данных в языке C++/CLI, отсутствующий в языке С. Ссылка позволяет определить альтернативное имя переменной. Объявление ссылки: тип & идентификатор2

=

идентификатор1;

Это объявление назначает переменной идентификатор1 второе имя идентификатор2. Следует отличать это понятие ссылки языка C++/CLI от понятия ссылки на объект и интерфейсной ссыпки в языках C# и Java. Пример 4.7.1. Использование ссылочной переменной. //////////////////// // C + + / C L I #include "stdafx.h" void main ( ) < int a = l ; int &m=a; // m - с с ы л о ч н а я п е р е м е н н а я System: :Console: :WrlteLine ("a= <0> rn= {1>", a.ToString(), m.ToString());

а++;

System::Console::WriteLine ("a= {0> m= <1>", a.ToString(), m.ToStrlng());

m++;

System: :Console::WriteLine ("a= {0> m= {1>", a.ToString(), m.ToString()); > /* Result: a=l m=l a=2 m=2 a=3 m=3 */


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

4.8. Перегрузка функций Языки С++, C++/CLI и C# позволяют определять в программе несколько функций с одним и тем же именем. Такие функции удобны и широко применяются в объектно-ориентированном программировании, когда надо выполнить аналогичные операции над данными разных типов. Подобные функции называются перегруженньши функциями. Имея одно и то же имя, перегруженные функции отличаются количеством или типом параметров. Пример 4.8.1. Перегруженные функции swap (int &, int &) и swap (bool &, bool &).

Illlllllllllllllllll // C+ + /CLI

#include "stdafx.h" using namespace System; // Использовать пространство имён System void swap (lnt &a, int &b) {

}

int c=a; a=b; b=c;

void swap (bool &a, bool &b)

{

>

// Целочисленные параметры

// Булевские параметры

bool c=a; a=b; b=c;

void main ( ) { int x = l , y=2; System::Console::WriteLine ("x= " + x.ToString()+ " y= " + y.ToString()); swap (x, у); System;:Console: :WrlteLine ("x= " + x.ToString()+ " y= " + y.ToString()); bool m=true, n=false;

>

System: :Console::WriteLine ("m= " + m + " n= " + n); swap (m, n); System: :Console: :WriteLine ("m= " + m + " n= " + n);


/* Result:

x=ly=2 X=2y=l m=True n=False m=False n=True */

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

4.9. Объявление функций и передача аргументов по умолчанию C++/CLI. В отличие от языка С, в котором отсутствуют перегруженные функции, в языках С++ и C++/CLI все функции перед использованием должны быть объявлены с полным списком типов формальных параметров и указанием типа возвращаемого значения, например: void swap (int &, int &); void swap (char 8t, char &);

В объявлении или описании функции её параметры могут быть заданными по умолчанию. При этом параметры, заданные по умолчанию, должны быть последними в списке. Пример 4.9.1. Задание значений параметров функции по умолчанию. //////////////////// // C + + / C L I #include "stdafx.h" int sum ( int а, int b= 10, int c=15 ) {

>

return а + b + с ;

void main ( ) { System: :Console::WriteLine ("sum (1, 2, 3)= " + sum (1, 2, 3)); System: :Console::WriteLine ("sum (1, 2)= " + sum (1, 2)); System::Console::WriteLine ("sum (1)= " + sum (1)); //System::Console::WriteLine ("sum ( ) = " + sum ()); // Error: 'sum': // function does not take 0 arguments


> /* Result: sum (l, 2, 3)= 6 sum (1, 2)= 18 sum (1)= 26

*/

4.10. Передача аргументов функций в языках C++/CLI и C# В языке С аргументы функций передаются только rio значению. Имеются два способа передачи: - передаётся непосредственное значение данного, - передаётся значение адреса данного Указатель). Аргументы в функцию передаются через стек. В язык С++ЛМЕТ введен новый тип данных ссылка (см. раздел 4.7), а в языке C# - модификатор ref параметра функции, которые позволяют передавать аргументы и получать возвращаемое ими значения в виде ссылки. Пример 4.10.1 иллюстрирует различные способы передачи и получения данных функциями F1(), F2() и F3(). В качестве передаваемых данных взята структура, включающая целочисленную и булевскую переменные. Пример 4.10.1. Различные способы передачи и получения данных. /////////////////// // C + + / C L I #include "stdafx.h" // Описание структуры struct ST {

>;

int x; bool с;

// Открытая целочисленная переменная // Открытая булевская переменная

// Описание функций void F l (ST st) // Передаются н е п о с р е д с т в е н н ы е з н а ч е н и я д а н н ы х

{

>

st.x= 3; st.c= true;

void F2 (ST &st)

{

>

st.x= 3; st.c= true;

void F3 (ST *pSt)

{

>

// Данные передаются через с с ы л к и

// Передаются значения указателей д а н н ы х

pSt->x= 5; pSt->c= false;


void main ( )

{

ST s t l = {1, false>; System::Console::WriteLine ("stl.x= " + (stl.x).ToString() " stl.c= " + stl.c); F l (stl); // Передать данные по значению System::Console::WriteLine ("stl.x= " + (stl.x).ToString() " stl.c= " + stl.c); F2 (stl); // Передать данные по ссылке System::Console::WriteLine ("stl.x= " + (stl.x),ToString() " stl.c= " + stl.c); F3 (&stl); // Передать данные по указателю System::Console: :WriteLine ("stl.x= " + (stl.x).ToString() " stl.c= " + stl.c);

+

+

+

+

> /* Result: stl.x= 1 stl.x= 1 stl.x= 3 stl.x= 5 */

stl.c= stl.c= stl.c= stl.c=

False False True False

//////////////////// //C# using System; struct ST {

>

public int x; public String с;

class C4_10_l { static void F l (ST st)

{

>

// Передаются н е п о с р е д с т в е н н ы е з н а ч е н и я

st.x= 3; st.c= "b";

static void F2 fref ST st) // Данные передаются через с с ы л к у

< >

st.x= 3; st.c= "b";

static void Main ( ) { ST s t l = new ST(); stl.x= 1; stl.c= "а"; Console.WriteLine ("stl.x= {0} stl.c= <1>", stl.x, stl.c); F l (stl); Console.WriteLine ("stl.x= {0} stl.c= {1>", s t l . x , stl.c);


F2 (ref stl); Console.WriteLine ("stl,x= {0> stl.c= {1}", stl.x, stl.c);

> /*

>

Result:

stl.x= 1 stl.c= а stl.x= 1 stl.c= а stl.x= 3 stl.c= b V C++/CLI. В языке C++/GLI функции должны быть описаны или объявлены до их использования. Поэтому в C++/CLI-nporpaMMe примера 4.10.1 функции F1(), F2() и F3() описаны до главной функции main(), в которой они вызываются. Ссылочные параметры помечаются символом "&". Из результата работы программы видно, что выполнение функции F1() не меняет значения переменных структуры stl, поскольку эти значения не передаются из тела функции через аргумент. Вызов же функции F2(() с ссылочным параметром и функции F3() с параметром-указателем изменил значения переменным stl.x и stl.c - осуществилась передача значений из тела функций через аргументы по ссылке и по указателю. C#, В языке C# все функции должны принадлежать классам. Поэтому функции и описаны в классе C4_10_1, в котором, кстати, описана и главная функция Main(). Описание функций F1() и F2() как статических (глобальных) в классе C4_10_l позволяет вызывать их непосредственно в статической функции Main(), а не через класс C4_10_1. Ссылочный параметр использует модификатор ref. Функция F2(), имеющая ссылочный параметр, в отличие от функции F1() изменила значения переменных структуры stl.


5. Классы 5.1. Определение класса Класс (class) - новый тип данных, определяемый программистом, появившийся в языке С++ и отсутствующий в языке С. Посредством class программист описывает совместно данные и действия над этими данными (функции). Из класса создаются объекты (переменные типа class). При этом данные, принадлежащие объекту некоторого класса, обусловливают состояние этого объекта, а набор функций - его поведение. В языке С++, используя метки private и public, можно по-разному инкапсулировать (скрывать) данные и функции в классе, запрещая или разрешая доступ к ним извне объекта. Метка private объявляет данные и функции, описанные после неё, закрьггыми. Эти данные и функции могут использоваться только функциями данного класса. Метка public объявляет данные и функции класса, описанные после неё, доступными (открытыми) вне объекта внутри блока, в котором объявлен или создан объект этого класса. В языках же C# и Java сокрытие данных и функций класса определяется не метками, а ключевыми словами private и public. Если при описании класса не применены private и public, то в классе языков С++, C++/CLI и C# по умолчанию данные и функции закрыты (то есть имеют доступ private), а в языке Java (J#) - открыты (то есть имеют доступ public). Правила описания класса в языках C++/CLI , C# и Java отличаются. Сравните описание класса А и его использование, реализованные на разных языках в примере5.1.1. Пример 5.1.1. Описание класса и создание его объектов. //////////////////// // C+ + / C L I #include "stdafx.h" using namespace System; class А

// Описание класса А

{

private: int x; public: char у;

// Закрытая переменная x | | Открытая переменная у


vold Set (int m, char n){x=m; y=n;}

// Открытая функция

vold Show ( )

// Открытая функция

{

>;

>

System::Console::Wrlte ("x= <0> y= ", x.ToString()); System: :Console: :WriteLine(gcnew Char(y));

vold main ( )

// Главная функция программы

{

А objA; objA.Set (1, 'z'); objA. Show ( ); А *pObjA=new А; pObjA -> Set (2,'w'); pObjA -> Show ( ) ; pObjA -> y='b'; pObjA -> Show ( ); delete pObjA;

>

// Объект objA класса А размещён в стеке // Установить значения данных объекта objA // Выдать значения данных объекта objA // Объект pObjA класса А размещён в неуправл. куче // Установить данные объекта с указателем pObjA // Выдать данные объекта с указателем pObjA | | Присвоить значение открытой переменной у // Удалить объект класса А с указателем pObjA

/* Result: x = l y=z x=2 y=w x=2 y=b */ //////////////////// //C# using System;

// Использовать пространство имён System

class А

// Описание класса А

{

int x; // Закрытая (по умолчанию) переменная x public char у; // Открытая переменная у p u b l i c void Set (int m, char n) {x= m; y=n;> // Открытая функция p u b l i c void Show ( )

{

>

// Открытая функция

Console.Write ("x={0}", x.ToString ( )); Console,Write (" у="); Console.WriteLine (у);

class C5_1_1 { static vold Maln ( ) {

// Главная функция

А sObjA= new А ( ) ; sObjA.Set (2, 'w'); sObjA,Show ( ); sObjA.y= 'b'; sObjA.Show ( ) ;

// Объект класса А в управляемой

куче


> /*

>

Result: 2 y= w x= 2 y= b */

X=

//////////////////// // J a v a и J # class A

<

// Описание класса А

private

lnt x; // Закрытая переменная x chary; //Открытаяпеременнаяу vold Set (int m, char n) {x= m; y=n;> Ц Открытая функция void Show ( )

{

>

>

// Открытая функция

System.out.print ("x= " + x); System.out.println (" y= " + у);

class C5_1_1 { public static vold ma!n ( ) {

> /*

>

А sObjA= new А ( ) ; sObjA.Set (2, 'w'); sObjA.Show ( ); sObjA.y= 'b'; sObjA.Show ( );

// Главная функция Ц Объект класса А в управляемой куче

Result: x= 2 y= w x= 2 y= b */

Поясним эту простую программу. C++/CLI. В начале тела главной функции main() описан класс А, содержащий целочисленную закрьггую переменную x и открытые символьную переменную у и функции Set() и Show(). Переменная x доступна функциям Set() и Show() только внутри объекта, а переменная у, открьггые функции Set() и Show() могут использоваться и вне объекта. Объявление А objA размещает объект objA в стеке. Затем функция objA.Set(l,'z') устанавливает значениех, равным 1, и значение у, равным 'z', а функция objA.Show() выводит эти значения на экран. В строке А *pObjA =new А; оператор new создаёт объект класса А в неуправляемой куче и присваивает его адрес указателю pObjA типа А. Затем


pObjA->Set(2,'w'); устанавливает значение x, равным 2, и значение у, равным 'w', а pObjA->Show(); выводит эти значения на консоль. Строка pObjA~ >y=-'b'; присваивает открьггой переменной у значение 'b'. Оператор delete pObjA; удаляет память, выделенную ранее объекту оператором new в неуправляемой куче. Присвоение значения закрытой переменной x посредством objA.x=l; и pObjA->x=2; было бы ошибочно, так как переменная x недоступна вне объекта objA и объекта, созданного оператором new. При определении класса на языке C++/CLI через ключевое слово class его данные и функции по умолчанию считаются закрьггыми (private). Но в языке C++/CLI можно также описать классы с помощью ключевых слов struct и union. Если класс описан с помощью ключевого слова struct, то его данные и функции по умолчанию считаются открьгтыми (public), и их можно закрыть с помощью private. При использовании union данные и функции класса могут быть только открытыми. C#. Как видно из примера 5.1.1, структура С#-программы отличается от структуры С++-программы. Программа на C# представляет пространство имён, включающее описание классов и структур. В нашем примере за ненадобностью пространство имён опущено. Компилятор всегда вставляет пространство имён, но мы для простоты программы удалили его и в дальнейшем будем оставлять только при необходимости. В нашем примере в опущенном пространстве имён описаны класс А и класс C5_1_1, содержащий главную функцию Main() программы. Ранее уже говорилось, что язык C# не разрешает описывать в программе функции вне класса как в языке C++/CLI. То есть пространство имён языка C# не содержит описания функций. Описание класса не завершается точкой с запятой ";" как в языке C++/CLI. Взглянем на тело функции Main(). Поскольку в языке C# тип class относится к управляемым типам, то объекты класса должны размещаться в управляемой куче с помощью оператора new. Вместо запрещённых указателей язык C# использует переменные ссылочного типа, значения которых инициализируются оператором new и используются сборщиком мусора. У нас это переменная sObjA. Здесь это не указатель, как в C++/CLI! Переменная ссылочного типа использует оператор "." (точка) при ссылке к открытым данным или функциям класса, а не оператор " - > " (стрелка). Исчезла необходимость в применении оператора delete, поскольку удаление объектов в управляемой куче осуществляется сборщиком мусора. По умолчанию данные и функции в классе закрыты и для их открытия надлежит воспользоваться доступом public. В примере 5.1.1 перед объявлением закрытой переменной x не стоит ключевое слово доступа, по умолчанию оно является словом private. Java. Из примера 5.1.1 видно, что Java-nporpaMMa отличается от C# программы незначительно. Отличаются функции вывода на консоль, заголовок


главной функции main() и, обратите внимание, перед объявлением закрытой переменной x появился доступ private. В отличие от языков C++/CLI и C# данные и функции клaccaязыкaJava поумолчаниюоткрыты.

S.2. Подставляемые функции и оператор привязки языка C++/CLI C++/CLI. Функции языка C++/CLI, описанные внутри класса, называются подставляемьши функциями (inline functions). В местах вызова этих функций вставляется их тело. Это обеспечивает эффективное выполнение этих функций. Так как в местах вызова обычных функций компилятор вставляет только операторы вызова, а в случае подставляемых функций - код их тела, то при использовании подставляемых функций тратится значительная память (возрастает размер программы). Подставляемые функции должны быть очень простыми, в них не допускается использование сложных операторов. Описанные в классе А примера 5.1.1 функции Set() и Show() являются подставляемыми функциями. Обычно функции класса (не подставляемые функции) только объявляют внутри класса, а описывают вне класса, привязывая их к классу с помощью специального оператора привязки. Оператор привязки "::" или оператор разрешения области видимости "::" (scope access operator) указывает, какому классу принадлежит функция. Оператор привязки помещается в описании функции между типом возвращаемого значения и именем функции. Перед оператором привязки ставится имя класса. Описание функции класса вне класса приведено в примере 5.2.1. Пример 5.2.1. Описание функций вне класса. //////////////////// // C + + / C L I class А

// Объявление класса А

{ private: lntx; //Закрытаяпеременнаях public: char у; // Открытая переменная / voidSet(intm,charn); vold Show (); >i

// 0 б ъ я в л е н и е ф у н к ц и и 5 е ^ // Объявление функции Show()

void A::Set (int m, char n) {x= m; y= n;} // Описание функции

Set()

В классе А примера 5.2.1 функции Set() и ShowO только объявлены, поэтому их описания должно быть размещены вне класса с использованием оператора привязки А::. В примере дано описание функции Set() вне класса. Чтобы превратить эту функцию в подставляемую функцию, достаточно поместить ключевое слово inline перед void.


C# и Java. В языках C# и Java понятие подставляемых функций отсутствует. Все функции должны описываться внутри класса. 5.3.

Некоторые замечания о классах

C++/CLI. Описание класса: class имя- класса {

>;

члены класса (данные и функции)

Описание класса начинается с заголовка класса, состоящего из ключевого слова c/aw, за которым следует имя класса. Затем помещается тело класса, заключённое в фигурные скобки и заканчивающееся точкой с запятой. В отличие от языков C# и Java в классах языка C++/CLI не разрешается инициализация данных. class А { lnt x = 0 ;

// И н и ц и а л и з и р о в а т ь нельзя !

}; ' В языке C++/CLI инициализация данных класса осуществляется специальной функцией конструктор, которую мы рассмотрим позже. Функции класса имеют область видимости класса, а не файла. То есть они могут быть использованы внутри класса объекта или вне объекта (если они открыты), но применительно к объекту класса, в котором они объявлены. Функция класса, кроме области видимости класса, имеет свою локальную область видимости. Если идентификатор переменной функции совпадает с идентификатором переменной класса, то для ликвидации двусмысленности надо использовать оператор разрешения области видимости (оператор привязки). void А:: Set (lnt x, char у) { А:: x= x; А:: y= у;>

Здесь A::x указывает на закрытую переменную x класса А, а x - на параметр x функции Set(). Нижеследующая программа примера 5.3.1 иллюстрирует использование областей видимости файла, локальных областей видимости и области видимости класса.


Пример 5.3.1. Области видимости. //////////////////// // C + + / C L I #include "stdafx.h" int k = l ; // Переменная к видна в файле, в классе А, в функциях Get() и maln() int GetK ( ) {return k;} // Функция GetK() видна в файле, классе А и в // функции main() class А // Класс А виден в файле и в функции main()

{

lnt n; // Переменная n закрыта и видна только в классе А public: void SetM (int n) {A::n= n+k + GetK ( );>// Функция SetM() открыта и видна // применительно к объекту класса А lnt GetM ( ) {return n;} // Функция GetM() открыта и видна // применительно к объекту класса А

>;

void main ( )

// Функция main видна в файле

{ А objA; objA.SetM (4);

// Объект objA виден внутри тела функции main() // Открытая функция объекта objA применима к нему

System::Console::WriteLine ("GetK ()+k= {0} n= {1}", (GetK ( )+k).ToString ( ), (objA.GetM ()).ToString ()); > /*

Result: GetK ( ) + k= 2 n= 6 */

C# и Java. В языках C# и Java область видимости функции определяется областью видимости класса, которому она принадлежит. То есть функция может использовать данные и функции класса, а также статические функции классов доступных пространств имён. Ссылка к открытым функциям других классов осуществляется посредством ссылок к их объектам или ссылки к классу статических функций. В отличие от языка C++/CLI в языке C# запрещено использование переменных с одним и тем же именем в охватываемом и охватывающем блоках. Имена параметров функции должны отличаться от имён других переменных в теле функции. О статических функциях и их использовании будет изложено в разделе 5.6.

5.4. Объекты в объектах Часто одни объекты содержат явно другие объекты или же создают другие объекты вне себя с помощью операторов new или gcnew. В первом случае объявленный в классе объект создаётся автоматически (instantiate) на языке C++/CLI. Во втором случае объект явно создаётся в куче - с помощью оператора new на языках Java и C# и помощью операторов new или gcnew на языке


C++/CLI. Затем объект используется и, в конце концов, потом удалится оператором delete из неуправляемой кучи или сборщиком мусора из управляемой кучи. Пример 5.4.1. Объект содержит объект другого класса. //////////////////// // C++/CLI #include "stdafx.h" using namespace System; class А

// Описание класса А

{

int x; public:

// Закрытая переменная x

char у;

// Открытая переменная у

void Set (int x, char у) {A:: x= x; А:: y= у;> void Show ( ) {

>;

>

System: :Console::Write ("x= {0} y= ", x.ToString ()); System::Console::WriteLine (gcnew Char (y));

class В

// Описание класса В

{

float f; // Закрытая переменная f А а; // Объявлен объект а класса А внутри объекта класса В public: void Set (float f, int x, char у) { В: :f=f; a.Set (x, у);> // Установка значения f класса В и данных // объекта а класса А через его функцию Set() void Show ( ) {

>;

>

System::Console::Write ("f= {0} ", f.ToString ( )); a.Show (); // Здесь выдаются данные объекта а на консоль // с помощью его функции Show()

void main ( ) { А objA; // Объект objA класса А в стеке objA.Set (1, 'c'); // Установить значения данных объекта objA objA.Show ( ); // Показать значения данных объекта obJA В objB; // Объект objB класса В в стеке objB.Set (10.5, 2, 'd'); // Установить значения данных объекта objB objB.Show ( ); // Показать значения данных объекта objB > /*

Result: x=ly=c


f=10,5 x=2 y=d */

В приведённой программе на языке C++/CLI объект objB содержит объект а. Функция Set() класса В устанавливает значение закрьггой переменной f этого класса и устанавливает значения данных включённого объекта а, используя функцию Set() объекта а. Обратите внимание, что в область видимости функции Set(float f, int x, char у) класса В входят переменная f и объект а, а в область видимости функции Set(int x, char у) класса А входят переменные x и у. К недоступным для неё переменным x и у функция Set(float f, int x, char у) обращается через доступный объект а, конкретнее, посредством его открьтгой функции Set(int x, char у). Взаимосвязь классов программы примера 5.4.1 можно представить в виде диаграммы классов рис 5.4.1. Класс В связан с классом А отношением композиции, указывающей, что объект класса В является владельцем объекта класса А. На так называемой диаграмме последовательности (рис. 5.4.2) показана последовательность выполнения действий в нашей программе. Наверху слева диаграммы находится фиктивный объект :main, пунктирная вертикальная линия, исходящая из него вниз, - это линия жизни этого объекта (время изменяется сверху вниз). Вертикальный прямоугольник на этой линии - выполнение функции main(), которая автоматически создаёт (instantiate) объявленные в ней объекты objA:A и objB:B, затем устанавливает значения объекта objA:A и выдаёт эти значения на экран, используя соответствующие функции объекта objA:A. Потом выполняются аналогичные действия с объектом objB:B. При завершении функции main() объект :main уничтожается, при этом автоматически уничтожаются и находящиеся в нём объекты objA:A и objA:A. Уничтожение объектов показано крестом.

•В -f: float -а: А +Set() +Show()

dh, ЧР

Рис. S.4.1. Диаграмма классов примера 5.4.1

•А -к int v ^ +y. char +Set() +Show()


nfrJA;.6

: main

ob[B : В

; main ^j instantiate() ш

+Setf)

Jj o b j A : A

u ^

ГЕ

4Showf 1 instantiate()

+6et() +ShowQ deetroy()

^

ъ

аШ^В

II

ъ

7L

^

Рис. 5.4.2. Диаграмма последовательности программы примера 5.4.1

5.5. Специальный вид функций класса - конструкторы и деструкторы C++/CLI. Из класса можно создать объекты с различными начальными значениями их данных. Среди данных могут оказаться и указатели, которым при создании объекта присваиваются адреса выделенных областей в куче. После использования объекты уничтожаются, а вся выделенная память освобождается. Действия по инициализации и удалению объектов выполняют специальные функции класса - конструктор и деструктор. Конструктор - это функция класса, основное назначение которой состоит в инициализации значений данных вновь создаваемого объекта. Имя конструктора всегда совпадает с именем класса. Конструктор вызывается всякий раз при объявлении объекта класса или при создании объекта с помощью оператора new или gcnew. Класс может иметь несколько перегруженных конструкторов. При создании объекта применяется тот конструктор, параметры которого обеспечивают установку требуемых начальных значений данных этого объекта. Конструктор не имеет возвращаемого значения. Конструкторы используются при создании объектов как в неуправляемой, так и в управляемой куче. Деструктор - это функция класса, предназначенная для уничтожения ранее созданного объекта этого класса. Имя деструктора всегда совпадает с именем класса с предшествующим символом "~" (тильда). Деструктор авто-


матически вызывается, когда объект, созданный объявлением, выходит из области видимости (например, функция, в которой создан объект, завершила работу) или, когда объект, созданный оператором new,

delete.

В деструкторе с п о м о щ ь ю оператора

delete

удаляется оператором

освобождается память, ра-

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

Пример 5.5.1. Перегруженные конструкторы. //////////////////// // C+ + / C L I #include "stdafx.h" class А { int x; public: А (int x = l ) {A:: x= x;}

// Закрытая переменная x // Параметр - з н а ч е н и е п е р е м е н н о й x

А (int *pX) {x= *pX;}

// Параметр - указатель з н а ч е н и я // переменной x А (А *pObj) {x= pObj -> GetX ();> // Параметр - указатель о б ъ е к т а // класса А int GetX ( ) {return x;}

// Возвратить значение закрытой переменной

vold Set (int x=10) {A::x= x;}

// // Установить переменнойзначение закрытой void Set (А obj) {x= obj.GetX ( );> // Параметр - о б ъ е к т к л а с с а А

>;

void ShowX ( ) // Вывести значение переменной {System::Console::WriteLine ("x= {0>", x.ToString());>

void main ( ) { А objl; objl.ShowX ( );

// Аргумент конструктора п о у м о л ч а н и ю равен 1

А obj2 (4); obj2.ShowX ( );

// Аргумент - н е п о с р е д с т в е н н о е д а н н о е

lnt k=5; А obj3 (&k); obj3.ShowX ( ) ;

// Аргумент - а д р е с д а н н о г о

А obj5 (&obj2); obj5.ShowX ( );

// Аргумент - а д р е с

объекта

obj5.Set (50); obj5.ShowX ( ) ;

// Аргумент - н е п о с р е д с т в е н н о е д а н н о е

obj5.Set (objl);

// Аргумент - о б ъ е к т


> Г

obj5.ShowX ( );

Result: x=l X=4 X=5 x=4 x=50

x=l */

Можно описание конструктора вынести из описания класса, привязав это описание к классу с помощью оператора привязки. Например, A:: А (А *pObj) {x= pObj -> GetX ( );} , // Конструктор не имеет возвращаемого // значения!

оставив в классе его объявление А (А *pObj);

Каждая функция класса неявно содержит указатель this имя-класса

*this;

Значение этого указателя равно значению адреса объекта класса, к которому относится данная функция. Мы будем активно использовать this при программировании оконных приложений. А (А *pObj) <this -> x= pObj -> GetX ( ) ; >

Если в классе не описан конструктор, то при создании объекта будет использован конструктор по умолчанию с пустым списком параметров. Как только в описание класса добавляется свой конструктор, конструктор по умолчанию удаляется. Требуется отдельно переопределять этот конструктор без параметров, если он нужен. Java и C#. Конструктор при создании объекта инициализирует значения данных и, в частности, с помощью оператора new выделяет области в управляемой куче, устанавливая при этом значения соответствующим ссылочным переменным. Как и в C++/CLI, имя конструктора совпадает с именем класса, он открытый (public) и не имеет возвращаемого значения. В языке C# дозволяется деструктор. Аналогично имя деструктора начинается с тильды и совпадает с именем класса. Деструктор используется для выполнения действий непосредственно перед тем, как сборщик мусора удалит из памяти объект. В отличии от деструктора языка C++/CLI, который выполняется, когда объект удаляется оператором delete или выходит за пределы области видимости, в случае языка C# объект может быть удалён позднее сборщиком мусора. Поэтому время выполнения деструктора точно не из-


вестно в отличии от языка C++/CLI. Напомним, что в конструкторе языка C# имена параметров должны отличаться от имён других переменных тела конструктора.

5.6. Статические функции класса Как в языке C++/CLI, так и в языках C# и Java, можно описать в классе статические данные или функции, поставив перед ними ключевое слово static. Статические данные становятся глобальными данными, а статические функции - глобальными функциями. С точки зрения надёжности программ желательно не создавать глобальные данные и функции, и делать это лишь в исключительных случаях. Желательно иметь дело только с интерфейсными функциями объекта, инкапсулировав в объектах всё, что возможно. Но, тем не менее, иногда появляется необходимость в глобальных данных и функциях. Например, глобальные функции Main() и main() программы объявлена как статические, поскольку они должна вызываться операционной системой. Как статические объявлены и функции Write() и WriteLine(), print() и printin() консольного ввода/вывода. Особенностью статических функций является то, что - они не должны использовать нестатические данные и функции, то есть они могут использовать только данные и функции, помеченные словом static; - статическая функция вызывается посредством ссылки через класс, в котором она описана, например: Console:: WriteLlne (" Статическая функция класса Console "); // C++/CLI Console. WrlteLlne (" Статическая функция класса Console "); // C# System.out.prlntln (" Статическая функция класса out"); // Java

- статическая функция может вызывать только статические функции класса, к которому она принадлежит; то есть она не может вызвать обычные функции её класса. Пример 5.6.1. Статические функции. //////////////////// // C+ + / C L I #lndude "stdafx.h" using namespace System;

static

// Использовать пространство имён

void Fm ( ) {Console::WriteLine (" Fm ( )");}

class CA

<

System

// Статическая функция

// Описание класса с функциями

public:

>;

vold F ( ) {Console::WrlteLine (" F ()");} // Обычная функция s t a t i c void Fs ( ) {Console::WriteLine (" Fs ()");> // Статическая ф-я


void main (void) { CA ca; ca.F(); CA::Fs ( ); //Fs ( ); Fm ( );

// Главная статическая функция Main() // Объект ca класса СЛ // Вызвать обычную функцию Ff,) класса СЛ | | Вызвать статическую функцию Fs() класса CA II Вызвать статическую функцию Fs() класса CA // О ш и б к а : Undeclared identifier // Вызвать глобальную статическую функцию Fm()

} /* Result:

F() Fs<) Fm() */

IIIIIIIIIUiHIIIIII //C# using System;

// Использовать пространство имён System

class CA

| | Описание класса с функциями

{ public vold F ( ) {Console.WriteLine (" F ()");} | | Обычная функция public static void Fs ( ) {Console.WriteLine (" Fs ()");} // Статическая ф-я

> class C5_6_1 { s t a t i c void Fm ( ) {Console.WriteLine (" Fm ()");> // Статическая ф-я static void Main (string[] args) {

>

// Главная статическая функция Main()

CA ca= new CA ( ) ; // Создать объект ca класса CA ca.F ( ) ; | | Вызвать обычную функцию F() класса CA C A . F s ( ); // Вызвать статическую функцию Fs() класса CA //Fs ( ) ; // Вызвать статическую функцию Fs() класса CA // О ш и б к а : The name 'Fs' does not exist in the class or namespace C 5 _ 6 _ l . F m ( ); // Вызвать статическую Fm() функцию класса C5_6_1

> /* Result:

F() Fs() Fm() */ //////////////////// // J a v a и J # class CA {

// Описание класса с функциями


public void F ( ) {System.out.println (" F ()");> // Обычная функция public static void Fs ( ) {System.out.prlntln (" Fs ( )");> // Статическая ф-я

> class C5_6_1 { static vold Fm ( ) {System.out.prlntln (" Fm ()");> // Статическая ф-я public static void main(String[] args)// Главная статическая функция Main() <

>

>

CA ca= new CA ( ) ; // Создать объект ca класса CA ca.F ( ); // Вызвать обычную функцию F() класса CA CA.Fs ( ); | | Вызвать статическую функцию Fs() класса CA //Fs ( ) ; // Вызвать статическую функцию Fs() класса CA // О ш и б к а : Cannot find method 'Fs()' in 'c)Example_4_6_l' C 5 _ 6 _ l . F m ( ); // Вызвать статическую Fm() функцию класса // C5_6_1

/*

Result:

F()

Fs() Fm() */

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


6. Наследование классов и интерфейсы 6.1. Об интерфейсах Под интерфейсом объекта понимается совокупность всех его открытых (public) функций, свойств и событий. Составляющие интерфейс функции, свойства и события (о событиях и свойствах поговорим попозже) используются при применении объекта. Языки C++/CLI и C# содержат конструкцию, описывающую интерфейс как совокупность функций, свойств и событий. В языке же Java интерфейс определяется как совокупность функций, свойств и статических переменных. Используя наследование, интерфейсы связывают с классами. Будучи наследованным несколькими разными классами, интерфейс обязывает эти классы реализовать объявленные в нём функции, свойства и события, то есть обязывает их подчиняться установленным этим интерфейсом правилам поведения. Таким образом, объекты, созданные из разных классов, наследующих один и тот же интерфейс, в определённых ситуациях должны вести себя одинаково. Например, объекты языка C#, наследующие интерфейс IDisposable, обязательно имеют функцию Dispose() и событие Disposed, которые применяются при освобождении ресурсов. Класс любого объекта, оперирующего с ресурсами, должен наследовать этот интерфейс. Наследование интерфейса Observer в языке Java добавляет наблюдающим объектам возможность обозревать наблюдаемые объекты, то есть включает объекты в особый механизм их взаимосвязи. В отличие от классов из интерфейсов нельзя создать объекты. Они для этого и не предназначены. Назначение интерфейсов состоит в формировании поведения объектов путём навязывания классу объекта обязательной реализации указанных в интерфейсе функций, свойств и собьггий. Подробнее о свойствах и собьггиях будет изложено в разделе 8.

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


Порождение (наследование) классов - это средство получения новых классов на базе существующих. При этом новый класс наследует данные и функции из базовых классов и интерфейсов. C++/CLI. В языках С++ и C++/CLI объявление порождённого неуправляемого класса начинается с ключевого слова class, за которым указывается имя порождённого класса. Затем за двоеточием следует список порождения, состоящий из отделённых запятыми имен базовых классов или интерфейсов с предшествующими им видами порождения public, private, protected). После этого заголовка следует тело порождённого класса, содержащее объявления данных и функций. c l a s s имя-порожд-класса : { p u b l i c | p r i v a t e | p r o t e c t e d > имя-баз-класса | имя-баз-интерфейса [, { p u b l i c 1 p r i v a t e | p r o t e c t e d > имя-баз-класса | имя-баз-интерфейса] ...

i

объявление данных и функций

>;

Количество базовых классов или интерфейсов в списке порождения может быть любым. Каждый базовый класс (или интерфейс) списка порождения должен быть уже описан или для него должно быть употреблено упреждающее объявление вида: c l a s s имя-класса;

// Упреждающее объявление класса имя-класса

Ключевое слово public (открытый), private (закрьггый) или protected (защищённый) перед именем базового класса в списке порождения объявляет доступ к наследуемым членам базового класса в порождённом классе. По умолчанию вид порождения - private. Для интерфейсов в списке порождения применяется только доступ public. Наследуемый интерфейс может включать только интерфейсные функции. Следующая таблица определяет доступ в порождённом классе к наследуемым членам (данным и функциям) базового класса:

Доступ в базовом

Доступ при порождении (в списке порождения)

классе public

protected

private

public

public

protected

private

protected

protected

protected

private

private

недоступен недоступен

недоступен


Анализ таблицы показывает, что - члены базового класса с доступом private становятся недоступными в порождённом классе. Их использование в. порождённом классе возможно только через функции базового класса, которые доступны в порождённом классе; - доступ при порождении public позволяет "протаскивать" члены базового класса с доступами public и protected по иерархии порождения; при этом члены с доступом public будут доступны и вне объекта, а с доступом protected будут защищены и могут использоваться только внутри объекта; - доступ при порождении private закрывает члены базового класса для дальнейшего использования. Диаграммы классов языка UML (рис. 6.2.1) поясняют графически приведённую выше таблицу, указывая в блоке комментария доступ к наследуемым данным а , e и с базового класса А в порождённом классе В при разных видах наследования. Символ "-" означает доступ private, символ "+" означает доступ public и символ "#" - protected.

Рис. 6.2.1. Доступ к наследуемым данным и функциям базового класса в языке С++ На другой диаграмме языка UML (рис. 6.2.2) показано изменение доступа к данным а, в и с базового класса А в иерархии порождения: класс В наследует класс А с порождением public, класс С наследует класс В с порождением protected и, наконец, класс D наследует класс С с порождением public. •в

!+aflb^

- protected: s|

+C

| #a' #b^

^, j _public: ^ t^

*D

[#a'#b^

Рис. 6.2.2. Изменение доступа к наследуемым членам (данным и функциям) базового класса при порождении в языке С++


Последовательность порождений на рисунке 6.2.3 говорит, что при использовании в иерархии только одного вида порождения public способствует "протаскиванию" данных базового класса с доступом public и protected вдоль всей иерархии наследования неизменяемыми. - ^j public:

+a

#b -с

,,«В

..

. . _>» pr\

P U tt<ihlic UHv.

• с

_^i f*^\

|+aftF)

MU rtnhlir* L/tl U

i B

Ив

Рис. 6.2.3. "Протаскивание" данных базового класса с доступом public и protected вдоль всей иерархии наследования в языке С++ В дальнейшем, при разработке W^Wcwj-приложений мы будем создавать наши классы, порождая их из классов библиотеки .NET Framework, содержащих десятки данных и функций. А наш класс добавит к ним ничтожное количество переменных и функций, обусловленных особенностями решаемой задачи. На основе порождённых классов создаются объекты с необходимым поведением. Взаимосвязь между классами и объектами может осуществляться как на уровне защищённых protected) данных и функций внутри иерархии порождения, так и с помощью специальных функций, способных связать иерархически не связанные объекты. Пример 6.2.1. Программа, использующая порождение public. ////////////////////

// C++/CLI

#include "stdafx.h" using namespace System;

class А

{

lnt x; public: char у; А ( ) {x= 10; y= 'f';> void Set (int x, char у) {A:: x= x; A:: y= у;> void Get (lnt &x, char &y) {x= A:: x; y= A:: у;> vold Show ( ) {

>;

>

System::Console: :Write ("x= {0} y= ", x.ToString ( )); System::Console::WriteLine (gcnew Char (y));

class В: public А

{

int z;


public: B ( ) { A ( ) ; z= 20;> void Sct (int z, int x, char y) {A:: Set (x, у); В:: z= z;} vold Get (lnt &z, int &x, char &y) {z= В:: z; А:: Get (x, у);> void Show ( ) {

>;

>

System::Console::Write ("z= {0> ", z,ToStrlng ( )); A:: Show ();

void main ( ) { B *pObjB= new В; pObjB -> Show ( ); pObjB -> Set (1, 2, 'a'); pObjB -> Show ( );

>

/*

Result: z=20 x=10 y=f z=l x=2 y=a */

6.3. Наследование управляемых классов в C++/CLI, C# и Java C# и Java. В отличие от языка С++, позволяющего множественное наследование классов и интерфейсов, языки C# и Java разрешает наследование порождённым классом только одного базового класса и множественное наследование интерфейсов. C++/CLI. Так же как и в языках C# и Java, в языке C++/CLI применительно к управляемьш классам также разрешается наследование порождённьт классом только одного базового класса и множественное наследование интерфейсов. В языках C# и Java правила наследования упрощены - в списке порождения не указывается допуск порождения, то есть в отличие от языка C++/CLI порождение не управляется и доступ порождения считается равным public. C#. class имя-порожд-класса ; {имя-баз-класса| имя-баз-класса [, имя-интерф] ...| имя-интерф [,имя-интерф ] ... > { объявление данных, функций, свойств и событий

>;


Java. class имя-порожд-класса

{ };

[extends имя-баз-класса] [implements имя-интерф [,имя-интерф

] ... ]

объявление данных и функций

Правила доступа в порождённом классе к членам базового класса в языках C# и Java просты: - члены базового класса с доступом private не доступны в порождённом классе; - члены базового класса с доступом protected доступны в порождённом классе и не доступны вне класса; - члены базового класса с доступом public доступны в порождённом классе и вне класса к объектам этого класса. Рисунок 6.3.1 поясняет доступ к членам базового класса из порождённых классов.

Рис. 6.3.1. Доступ к наследуемым членам базового класса в C# и Java C++/CLI. Класс, определённый с ключевым словом seled (конечный), не допускает дальнейшего порождения из него классов C#. Класс, определённый с ключевым словом seled (конечный), не допускает дальнейшего порождения классов из этого класса. Java. Класс, определённый с ключевым словом final (конечный), не допускает дальнейшего порождения классов из этого класса.

6.4. Использование конструктора базового класса Как наследуемый, так и базовый класс, имеют конструкторы без параметров и с пустым телом по умолчанию, которые могут быть заменены конструкторами, описанными в классе. При создании объекта класса, порождённого из некоторого базового класса, сначала выполняется конструктор базового класса, а затем конструктор порождённого класса. Если базовый и порождённые классы имеют конструкторы с параметрами, то в конструкторе порождённого класса необходимо указать список аргументов, передаваемых в соответствующий перегруженный конструктор базового класса. В этом случае применяется конструктор со списком инициализации:


C++/CLI.

имя-порожд-констр (список-парам) : имя-баз-констр (список-арг) [ , имя-баз-констр (список-apr) ] ... { тело конструктора >

C#.

имя-порожд-констр (список-парам) : base (список-арг) { тело конструктора >

Java и J#.

имя-порожд-констр (список-парам)

{ super

(список-арг); . . . >

Обратим внимание, что список инициализации для C++/CLI состоит из имён конструкторов базовых классов со списком аргументов в круглых скобках, отделённых друг от друга запятыми. В случае управляемых классов применимо только одно имя базового класса, поскольку управляемые классы наследуют единственный базовый класс. Для языка C# список инициализации состоит из ключевого слова base, за которым указан список аргументов в круглых скобках. При создании объектов порождённого класса порядок выполнения конструкторов следующий. Сначала выполняется конструктор базового класса. Затем выполняются конструкторы объектов, создаваемых в теле порождённого конструктора. И, наконец, выполняется конструктор порождённого класса. В языке Java вызов конструктора базового класса осуществляется в начале тела порождённого класса. Конструктор представляется в видс ключевого слова si'per, за которым в круглых скобках указывается список аргументов. Java и J#: имя-порожд-констр (список-парам) < вирег(список-аргументов); тело конструктора >

В программах примера 6.4.1 выполняются разные конструкторы при создании объекта порождённого класса. Пример 6.4.1. Выполнение конструкторов. Illlllllllllllllllll // C + + / C L I #include "stdafx.h" using namespace System; class CA

{

// Класс для объекта, создаваемого конструктором класса CB

int x; // Закрытая переменная x public: CA (int x) {CA::x= x;} // Конструктор класса CA int Get ( ) {return x;} // Получить значение x

>;


class CB

// Базовый класс для класса CC

<

CA *pA; // public: CB (int x) {pA= new CA (x);} // int Get ( ) {return pA -> Get ();> // //

>;

Указатель на объект класса CA Конструктор класса CB Получить значение переменной x объекта класса CA

class CC: public CB // Класс, порождённый из базового класса CB { char у; // Закрытая переменная у public: CC (int x, int у): CB (x) // Конструктор со списком инициализации {CC::y= у;> void Get (int &x, char &y) {x= CB:: Get ( ); y= CC::y;} // Получить x и у

>;

void main ( ) <

>

CC *pCC= new CC (5, 'a'); // Создать объект класса CC в куче lnt x; char у; pCC -> Get (x, у); // Получить значения x и у Console::WriteLine (x); // Вывести значение x Console::WriteLine (gcnew Char(y)); // Вывести значение у

//////////////////// // J a v a и J # class CA

< >

// Класс для объекта, создаваемого конструктором класса CB

private lnt x; public CA (lnt ух) {x= vx;} public int Get ( ) {return x;}

class CB

|| Закрытая переменная x | | Конструктор класса CA || Получить значение x

// Базовый класс для класса CC

{

>

private CA sA; // Ссылка на объект класса CA public CB (int x) {sA= new CA (x);} // Конструктор класса CB public lnt Get ( ) {return sA.Gct ( );> // Получить значение переменной x // объекта класса CA

class Data {

>

public int x; public char у;

class CC extends CB

// Класс, порождённый из базового класса CB


{

>

private char у; // Закрытая переменная у public CC (int x, char my) // Конструктор с инициализацией {super (x); y= my;} public CC (Data d) // Конструктор с инициализацией {super (d.x); y=d.y;> publicvoldGet(Datad){d.x=super.Get(); d.y=y;> //Получить

class C6_4_1 { public static void main ( )

{

>

>

CC sCC= new CC (5, 'a'); // b a t a d= n e w Data ( ); d.x= sCC.Get (d); // System.out.println (d.x); // System.out.println (d.y); //

Создать объект класса CC в куче 0; d.y= 'm'; Получить значения x и у Вывести значение x Вывести значение у

//////////////////// //C# using System;

class CA

// Класс для объекта, создаваемого конструктором класса CB

{ private lnt x; public CA (int vx) {x= vx;} public lnt Get ( ) {return x;}

>

class CB

// Закрытая переменная x // Конструктор класса C4 // Получить значение x

// Базовый класс для класса CC

{

}

private CA sA; // Ссылка на объект класса CA public CB (lnt x) {sA= new CA (x);} // Конструктор класса CB public lnt Get ( ) {return sA.Get ( );> Ц Получить значение переменной x || объекта класса CA

class CC: CB {

// Класс, порождённый из базового класса CB

private char у; // Закрытая переменная у public CC (int x, char my): base (x) // Конструктор со списком инициализации {y= my;> public void Get (ref lnt x, rcf char my) {x= base.Get (); my= у;> // Получить

>

class C6_4_1 { static vold Main ( ) < CC sCC= new CC (5, 'a'); int x= 0;

// Создать объект класса CC в куче


} /*

>

char y= m ; sCC.Get (ref x, ref у); Console.WriteLine (x); Console.WriteLine (у);

// Получить значения x и у // Вывести значение x // Вывести значение у

Result: 5

а */

Java и J#. В языке Java данные стандартных типов передаются в параметрах функции по значению (через стек). Поэтому их изменённые в теле функции значения не возвращаются через аргументы этой функции. Если появилась необходимость возврата каких либо значений через параметры функции, то в качестве параметров необходимо применить только ссьшаемые данные, такие как массив и объекты, которые размещены не в стеке, а в управляемой куче. Обратите внимание, что в Java программе примера 6.4.1 в конструкторе CC() и в функции Get() в качестве параметра применена ссылка на объект типа Data, используемый только для извлечения данных x и у.

6.5. Сокрытые переменные, функции и их использование C#. В C# не разрешается использовать одноимённые переменные и функции в охватываемом и охватывающем блоках, а также в базовом и порождённом классах. Но при необходимости язык C# позволяет скрыть переменную или функцию в охватываемом блоке путём использования ключевого слова new (это не оператор new, применяемый при создании объектов в куче, а ключевое слово newl) перед объявлением переменной или функции в порождённом классе. Если же скрытые переменная или функция понадобятся в порождённом классе, то к ним ссылаются с помощью ключевого слова base. С++ и C++/CLI. В С++ и C++/CLI разрешается использовать одноимённые переменные и функции в охватываемом и охватывающем блоках, а также в базовом и порождённом классах - это разные переменные и функции с одинаковыми именами. Можно использовать их в базовом и порождённом классах, но для разрешения неоднозначности применить оператор привязки. Пример 6.5.1 иллюстрирует применение сокрытых переменных и функций. Пример 6.5.1. Сокрытые переменные и функции. //////////////////// // C + + / C L I #include "stdafx.h" using namespace System; class CA

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


<

public: int x; int F ( ) <return x * x;}

// Открытая переменная x // Возвратить x * x

}; class CB : public CA

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

{

public: int x; // Открытая переменная x int F ( ) { return x + CA: :x + CA::F ();> // Вычислить и возвратить void Set (intx) {CA::x= x; CB::x=10 * x;J // Установить значения

>;

void main ( ) {

>

CB *pCB= new CB; // Создать объект класса CB в куче pCB -> Set (2); // Установить значения x в классах CA и СВ. int m= pCB -> F (); // Получить значениеСВ::х + CA::x + CA::F ( ) Console::WriteLine (m);

llllllllllllllllllll //C#

using System;

class CA

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

{

>;

public lnt x; public int F ( ) {return x * x;}

class CB : CA

<

>;

// Открытая переменная x // Вычислить и возвратить

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

new public int x; // Открытая переменная x new public lnt F ( ) { return x + base.x + base.F ();> // Возвратить public vold Set (int mx) {base.x= mx; x=lO * mx;} // Установить значения

class C6_5_1 { static void Main ( )

{

> /*

>

Result:

26

V

CB sCB= new CB(); // Создать объект класса CB в куче sCB.Set (2); // Установить значения всех x lnt m= sCB.F (); // Получить значение CB::x + CA: :x + CA::F ( ) Console.WriteLine (m);


6.6. Интерфейсы В предыдущем разделе говорилось, что интерфейс обязывает реализовать объявленные в нём функции, свойства и события в классе, наследующем этот интерфейс. Более того, как мы скоро увидим, класс, наследующий некоторый интерфейс, должен реализовать функции, свойства и события интерфейса особым образом, сделав их доступными извне. Таким образом, наследуемый интерфейс влияет на поведение и общение объектов класса с внешним миром. Многие классы библиотеки .NET Framework и библиотек Java наследуют потребные интерфейсы. Интересно, что пользователь объекта может вызвать функции базового интерфейса не только обычным образом, воспользовавшись ссылкой или дескриптором к объекту, но, даже не зная тип (класс) объекта. Для этого достаточно воспользоваться так называемыми интерфейсными ссылками соответствующего базового интерфейса, который наследуется классом. Об интерфейсных ссылках будет рассказано позже. Не существует такого понятия, как объект интерфейса - хотя интерфейс описывается отдельно и схоже с описанием класса, используя, правда, ключевое слово interface или пару слов interface class при описании и включая только объявления функций, свойств и собьггий. Интерфейс - это средство, обязывающее некоторый класс реализовать объявленные в интерфейсе собьггия, свойства и функции. Поскольку функции класса объекта определяют поведение этого объекта, то наследуемый интерфейс обязывает объект уточнить поведение, навязав объявленные в нём функции. Как известно, ссылаясь к объекту по его ссылке, можно воспользоваться всеми открытыми данными, событиями и функциями его класса. Но если сослаться на интерфейсную ссылку объекта, то придётся ограничиться функциями, свойствами и собьггиями, объявленными только в интерфейсе. Это обстоятельство часто используется, когда требуется функциональность не всего объекта, атолько его части, представленной интерфейсом. 6.6.1. Определение интерфейса Интерфейс rtnterface) объекта есть совокупность открытых (public) функций, свойств, собьгтий или статических переменных, необходимых и достаточных для управления его функционированием пользователем объекта. Заметим, что определение интерфейса в языках C++/CLI, C# и Java отличается. C+ + / C L I . interface class имя-интерф [ : public имя-базов-интерф [ , public имя-базов-интерф ] ... ]


{

объявление функций [,

свойств

и событий]

>; C# interface {

имя-интерф [ : имя-базов-интерф [, имя-базов-интерф ] ... ]

объявление функций,

>

Java и J#. i n t e r f a c e имя-интерф

{

свойств и событий

[ e x t e n d s имя-базов-интерф [ , имя-базов-интерф ] ... ]

объявление функций и статических

>;

переменных

Для языков программирования C# и Java объявленные в интерфейсе функции, свойства и события считаются открытыми (public) по умолчанию и должны быть реализованы в классе, который наследует этот интерфейс. При отсутствии реализации наследуемого интерфейса компилятор сообщит об ошибке. Заметим, что при наследовании классом интерфейса в языке Java применяется ключевое слово implements, а при наследовании интерфейсом интерфейса - слово extends. Пример 6.6.1 иллюстрирует наследование (управляемого) интерфейса на языках C# и C++/CLI. Пример 6.6.1. Наследование интерфейса в С # и C++/CLI. /////////////// //C# File using System; interface Ipe {

>

// Интерфейс, объявляющий свойство P // и функцию Hallo

i n t P {set; get;} void Hallo ( );

class А: Ipe

{

//Объявитьсвойство // Объявить функцию

// Класс, наследующий интерфейс Ipe

private int x;

// Переменная x, связанная со свойством P

public void Hallo ( )

// Реализовать функцию Hallo интерфейса Ipe

{

>

Console.WriteLine ("ПРИВЕТ");

public int P

{

// Реализовать свойство P интерфейса Ipe


set {x=value;} get {returnx;}

//Установитьзначение //Получить значение

свойстваР свойстваР

class Test { static void Main ( ) { А sA= new А ( ); // sA.P= 5; // Console.WriteLine ("P= " sA. Hallo ( ); //

Создать объект Установить свойство P + sA.P); // Выдать значение свойства P Приветствовать

/////////////// // C + + / C L I #include "stdafx.h" using namespace System; interface class Ipe { property lnt P; vold Hallo ( );

// Интерфейс, объявляющий свойство P // и функцию Hallo. // Объявить свойство P // Объявить функцию

>; ref class А: public Ipe {

// Класс, наследующий интерфейс Ipe

private: lnt x;

// Переменная x, связанная со свойством P

public: virtual vold Hallo ( )

// Реализовать функцию Hallo интерфейса Ipe

{

Console::WriteLine ("ПРИВЕТ");

property lnt P

{

>

// Реализовать свойство P

virtual vold set (int param) {x= param;} virtual lnt get ( ) {return x;}

void main ( ) {

>

А ^pA= gcnew А ( ); // Создать объект класса А pA -> P= 5; // Установить свойство P Console::WriteLine ("P= {0>", pA -> P.ToString ()); pA -> Hallo ( ); // Приветствовать


/* Result: P= 5 ПРИВЕТ */

Программы примера 6.6.1 включают интерфейс Ipe, содержащий объявления функции Hallo() и свойство Р. О свойствах будет рассказано позднее. Функция Main() или main() создаёт объект класса А и устанавливает его свойство P, равным 5. Затем значение свойства P выдаётся на консоль и вызывается функция Hallo(). Функция Hallo() и свойство P, объявленные в интерфейсе Ipe, обязательно должны быть реализованы в классе А, наследующим этот интерфейс. Попытка не реализовать хотя бы один элемент интерфейса приведёт к сообщению компилятора об ошибке. Интерфейс представляет свод правил, которым обязан подчиниться объект, наследуемый данный интерфейс. Если объект наследует несколько интерфейсов, то его класс обязан реализовать их все.

6.6.2. Базовые интерфейсы Часто разнотипные объекты (то есть объекты, созданные из разных классов) обязаны включать в свой интерфейс среди функций, свойств и событий одну и ту же совокупность функций, свойств и собьггий. Эти дополнительные и обязательные функции, свойства и события обычно образуются из так называемого базового интерфейса, который наследовал класс каждого из этих объектов. Базовые интерфейсы обязывают разнотипные объекты соблюдать одни и те же правила поведения. Базовые интерфейсы широко применяются при разработке компонентов, поскольку именно базовые интерфейсы обязывают компонент соблюдать рекомендации компонентно-ориентированного программирования, включать обязательные функции, свойства и события. Среди функций базового интерфейса есть, например, функция Dispose(), обязывающая объект компонента освободить используемые им ресурсы перед его уничтожением.

6.7. Упаковка и распаковка типов данных C#. В языке C# любой класс и все типы данных, включая обычные типы (как int, float и др.), наследуются из класса Object пространства имён System. Поэтому любой класс может воспользоваться открытыми функциями класса Object, в частности, функцией ToString(). При желании эта функция может быть переопределена (override) в классе, отображая нужное содержание объекта класса. Для обычного типа данных эта функция представляет его значение в виде строки, что, например, используется функциями Write() и WriteLine(). Наличие общего базового класса System.Object позволяет использовать ссы-


лочную переменную типа Object при ссылке на объект любого другого класса, включая массив, поскольку массивы реализованы в C# как классы. С++ /CLI. В языке C++/CLI только классы библиотеки .NET Framework и управляемые классы наследуются из класса System::Object. Также из этого класса наследуются классы, полученные путём упаковки из типов значений. Упаковка состоит в применении к типу-значение специального управляемого класса-оболочки, который обволакивает тип значения, превращая его в управляемый объект. Этот класс наследует класс System::Object, а объект этого класса размещается в управляемой куче. К объекту можно применить любую открытую функцию класса System::Object и, в частности, ToString(). Распаковка состоит в преобразовании упакованного типа, хранимого в управляемой куче, в неупакованное значение, хранимое в стеке. При распаковке осуществляется разыменование дескриптора с использованием "*". В примере 6.7.1 приведено использование класса object (краткое имя для System::Object) для упаковки и распаковки в C#, класса-оболочки Int32 для C++/CLI и класса-оболочки Integer для Java. Пример 6.7.1. Упаковка и распаковка типов значений. //////////////////// // C+ + / C L I #include "stdafx.h" using namespace System; void main ( ) { int x= 20; Console::WriteLine (x);

// Целочисленное значение в стеке

Int32 ^pX= gcnew Int32 (x); // Упаковка значения в управляемую кучу int y= *pX; // Распаковка (разыменование) в стек Console::WriteLine (у); > /* Result:

20 20 */

//////////////////// // J a v a и J # class C6_7_1 { public static void main ( ) < int x= 20; // Целочисленное значение в стеке System.out.println (x); I n t e g e r o b j I = n e w I n t e g e r ( x ) ; // Упаковка в управляемую кучу int y = objI.intValue(); // Распаковка в стек


System.out.println (у);

>

boolean b= true; // Булевское значение в стеке System.out.println (b); B o o l e a n o b j B = n e w B o o l e a n ( b ) ; // Упаковка в управляемую кучу b o o l e a n c = o b j B . b o o l e a n V a l u e ( ) ; // Распаковка в стек System.out.println (с);

> /* Result:

20 20

true true */

iiiiiiimiiimiiii //c# using System; class C6_7_1 { static void Main ( ) { int x= 20; // Целочисленное значение в стеке Console.WriteLine (x); abject obj; Ц Объект класса System. Object obj= x; // Упаковка в управляемую кучу x= 500; lnt y= (lnt) obj; // Распаковка в стек Console.WriteLine (у);

>

System.Object objS; // Объект класса System.Object objS= x; // Упаковка в управляемую кучу int yS= (lnt) objS; // Распаковка в стек Console.WriteLine (yS);

> /* Result: 20

20

500 */

C#. Обратите внимание, после упаковки целочисленного значения x, равного 20, в управляемую кучу, значение x изменено на 500. Но распакованное значение равно 20. Таким образом, переменная x и ссылка obj указывают на разные места хранения данных (стек и управляемая куча). Целочисленное данное, действительно, боксировалось. Java. В языке Java для обёртки базовых типов данных применяются специальные классы Boolean, Character, Integer, Long, Float, Double и функции booleanValue(), charValue(), intValue(), longValue(), floatValue(), doubleValue().


7. Программа ввода/вывода информации В предыдущих разделах уже были рассмотрены программы, написанные на языках C++/CLI, C# и Java. Получив определённый опыт, перейдём к разработке программы, включающей большее количество классов и поэтому способной вызвать затруднения при её создании и поиске ошибок. Объём книги не позволяет рассмотреть разработку сложной программной системы, поэтому поэтапный подход к созданию простой программы этого раздела желательно использовать читателями этой книги в дальнейшем, поскольку этот подход оправдал себя при разработке сложных программ, существенно уменьшив время разработки и увеличив их надёжность. Программу разработаем на языке C++/CLI, в конце раздела приводится её версия на языке C#. Для большей ясности разработка программы будет иллюстрироваться диаграммами языка UML. В конце книги приведена поэтапная разработка на языках C++/CLI, C# и Java более сложной программы, включающей множество объектов, а также события, делегаты, потоки, интерфейсные элементы и дочернее окно, о которых будет рассказано в следующих разделах.

7.1. Постановка задачи Давайте поэтапно разработаем программу, формирующую список взаимосвязанных элементов - объектов, каждый из которых включал бы сведения о компьютере и о магазине, в котором он был куплен. Пользователь этой программы может пополнять список, вводя информацию о вновь купленном компьютере и о магазине, его продавшем. При желании можно выдать на экран всю введённую информацию или уничтожить её. Вводимая информация о компьютере: - имя производителя, - год производства. Вводимая информация о магазине: - адрес магазина, - стоимость компьютера. Для простоты, не будем проверять соответствие строк с именами реальных производителей и адресами реальных магазинов. Пусть год и стоимость являются целыми числами, диапазон которых определяется диапазоном значений типа int. Разработаем для пользователя два варианта этой программы: - 1пАС++ЛХ1-приложение, написанное на языке C++/CLI,


- 1п£С#-приложение, написанное на языке C#.

Диаграмма вариантов использования Диаграмма вариантов использования (use cases diagram) языка UML представляет разрабатываемую программу как систему (модель) в виде совокупности вариантов её использования пользователем. Этой диаграммой должна быть показана внешняя сторона системы, не раскрывая её внутреннего построения. Обычно эта диаграмма создаётся разработчиком системы в содружестве с заказчиком - будущим пользователем системы. Диаграмма должна показать связь пользователя с системой настолько полно, чтобы, воспользовавшись этой диаграммой, разработчик или группа разработчиков смогли разработать и вернуть заказчику готовую систему, не имея от него и любого пользователя этой системы никаких претензий. Дело в том, что любые недомолвки в диаграмме вариантов использования, выявленные в середине разработки системы или, что совсем плохо, в конце разработки, приводят к пересмотру внутреннего построения системы и изменению кода программы. Это, как правило, вызывает значительные дополнительные усилия по исправлению уже разработанной версии. Диаграмма вариантов использования может разрабатываться с различной степенью детализации. Некоторые диаграммы вариантов использования или их части могут наследоваться, включаться и расширяться из других диаграмм в соответствии с нотацией языка UML. Надо заметить, что в зависимости от потребителя разрабатываемой системы диаграмма вариантов использования может создаваться не только для непосредственного пользователя системы, но и для программиста, реализующего эту систему, для аналитика, анализирующего систему с какой либо позиции, для группы тестирования, готовящейся тестировать систему при её разработке или позже, и так далее. Для нашей простой программы можно установить следующие варианты использования: - ввести данные о компьютере и магазине (enter data), - вывести все введённые данные о компьютерах и магазинах (show data), - удалить все введённые данные (delete data). На рис. 7.1.1 изображена диаграмма вариантов использования нашей программы.

user

Рис. 7.1.1. Диаграмма вариантов использования Inf-приложения


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

7.2. Диаграмма классов Исключительное место при разработке диаграмм модели создаваемой системы занимает диаграмма классов. Диаграмма классов (class diagram) состоит из взаимосвязанных классов. Каждый класс на диаграмме изображён в виде прямоугольника, состоящего из трёх частей, включающих соответственно имя класса, его данные и его функции (и операции). Диаграмма классов представляет статическую сторону разрабатываемой системы в отличие от диаграммы последовательности, изображающей функционирование системы в динамике. Основная задача диаграммы классов - представить взаимосвязанные классы всех объектов, совместное функционирование которых обеспечивает работу системы. Перед разработкой классов необходимо подробнейшим образом вникнуть в решаемую проблему, выделить основные понятия и термины, проанализировать их и сделать обобщения различного уровня. Обобщённые или абстрактные однотипные сущности представляются в виде классов. Из каждого класса можно создать конкретные экземпляры - объекты, отличающиеся значениями данных и своим поведением. Разработка диаграммы классов - работа, требующая и пунктуальности и творчества. Ведь можно создать классы либо слишком крупными или мелкими, что повлечёт к наличию в системе небольшого числа сложных объектов, либо к появлению очень большого числа мелких. В первом случае будет не только трудно отладить громоздкие классы, но и будет сложно обеспечить связь между объектами из-за громоздкого интерфейса. Во втором случае могут возникнуть трудности с управлением армадой многоликих объектов, хотя отладка их простых классов не представит трудности. У профессионала-программиста со временем появляется опыт, и эти проблемы исчезают. Подумаем, какие классы желательно создать для нашей программы. Прежде всего, нам нужен главный класс, из которого был бы создан объект, общающийся с пользователем. Объект этого класса будет вводить данные от пользователя, связывать их в список, выводить данные и удалять список. В случае нашей программы он предоставляет пользователю оговоренный интерфейс, удовлетворяя требованиям диаграммы вариантов использования. Поскольку главный объект и связанный с ним класс являются владельцем информации, назовём этот класс CInf ( C I n f - Class Information). Для общения с пользователем включим в этот класс интерфейсные функции: - Enter ( ), которая вводит исходные данные о компьютере и магазине;


- Show ( ), которая выводит все введённые данные, и - Clear ( ), которая удаляет всю введённую информацию.

В соответствии с заданием информация о каждом купленном компьютере должна размещаться в отдельном элементе списка. Пусть каждый элемент списка будет объектом класса, например, CData. Теперь можно уточнить данные класса CInf. Так как объект класса CInf - владелец списка, то в его классе должны присутствовать указатели (ссылки), указывающие на начало и конец списка: - pFirst, который указывает на начало списка (объект класса CData), и - pLast, который указывает на конец списка (объект класса CData или nullptr). Наступило время изобразить нашу диаграмму классов первого этапа разработки. Она представлена на рис. 7.2.1. *gnf -pFlrst CData -nLast CData **• ^ +Enter() +Show() +Clear()

?LJ

Рис. 7.2.1. Диаграмма классов Inf-приложения первого этапа На диаграмме связь между классами определяет, что класс CInf является владельцем объектов класса CData, то есть создаёт эти объекты, управляет ими и уничтожает их. Уточним класс CData. Объекты этого класса являются элементами списка, и каждый из них должен содержать обязательное поле - ссылку на другой элемент. Назовём эту ссылку класса CData как pNext и сделаем её открытой, поскольку к ней непосредственно будет осуществляться ссылка из других объектов. Кроме этого каждый элемент списка должен содержать информацию об изготовителе и годе изготовления компьютера, об адресе магазина и стоимости компьютера. Поместим соответствующие данные в класс CData. Интерфейсные функции Enter() и Show() класса CData позволят использовать закрытые переменные этого класса. На рис. 7.2.2 представлена уточнённая диаграмма классов. Итак, класс CData имеет закрытые переменные place (адрес магазина), cost (стоимость), producer (изготовитель) и уеаг (год). Также этот класс содержит открытый указатель pNext для связывания соседних элементов списка и открытые интерфейсные функции Enter() и Show(), которые позволят владельцу записывать и просматривать скрытую информацию.


*Qnf -pFiret CData ^ , -pLast: CData p +Add() +Show() +Clear()

•CData

^s +pNext: CData -place: SPing

-cost: int -producer String -vear int +&ter() +show()

Рис. 7.2.2. Диаграмма классов Inf-приложения второго этапа Давайте на последнем третьем этапе скорректируем полученную диаграмму классов, разбив класс CData на два класса: CComputer и CShop. Класс CComputer является владельцем информации о компьютере, а класс CShop- о магазине. Поскольку для управляемого кода запрещено множественное наследование, то класс CData не может наследовать одновременно классы CShop и CComputer. Поэтому класс CData будет наследовать только класс CShop, а класс CComputer будет представлен объектом с ссылкой pComp. Окончательная диаграмма классов нашей программы представлена на рис. 7.2.3. Из неё наглядно видно, что в объекте класса CData всё, что возможно, спрятано и оставлен открытый доступ только для связи его с другими объектами, а также для наполнения объекта данными и выборки из него информации посредством интерфейсных функций.

Рис. 7.2.3. Диаграмма классов Inf-приложения третьего этапа

7.3. Диаграмма последовательности Если диаграмма классов представляла модель нашей программы статически, то диаграмма последовательности (sequence diagram) на рис. 7.3.1 отобразит функционирование объектов динамически, во времени. Начинает действия стилизованный человечек - актант. Направленные от него стрелки к объекту :CInf требуют ввода или вывода данных. По этим требованиям объект :CInf оживает, превращая свою пунктирную линию жизни в вытянутые по времени вниз прямоугольники. Прямоугольниками


представлено выполнение открьггых функций Enter() и Show() объекта :CInf . Функция Clnf::Enter() создает (new) объект :CData класса CData, заполняет его введёнными данными (CData::Enter) и присоединяет (include) этот объект к списку объектов типа CData. Функция CInf::Show()_иcпoльзyeтoбъeкты:CData списка.которыеуже созданы объектом :CInf, заполнены данными и связаны друг с другом. Функция CInf::Show() просматривает список объектов класса CData и выводит находящуюся в них информацию, воспользовавшись функцией CData::Show() для каждого из этих объектов. При завершении (exit) актантом приложения объекты уничтожаются. Необходимо заметить, что диаграмма последовательности является эффективным средством при разработке сложных систем, в которых, в отличие от нашей программы, объекты могут сосуществовать самостоятельно, выполняя параллельные действия, требующие взаимной синхронизации. Здесь вступают в силу не только механизмы синхронизации, но и механизмы передачи сообщений как в пределах одного компьютера, так и в границах некоторой сети. Язык UML предоставляет графические элементы для отображения этих сложных взаимодействий.

Рис. 7.3.1. Диаграмма последовательности lnf-приложения

7.4. Диаграмма видов деятельности Диаграмма видов деятельности (activity diagram) описывает алгоритм выполнения функций применительно к используемым объектам. В качестве примера рассмотрим диаграмму видов деятельности применительно к функции CInf::Enter(). Диаграмма (рис. 7.4.1) состоит из прямоугольников с округленными левой и правой сторонами, в которых указываются действия. Линии со стрелками связывают эти элементы в требуемой по-


следовательности выполнения, допуская ветвления и слияния. Около разветвления помещаются условия в квадратных скобках. Начинается диаграмма с закрашенного кружка, а кончается в виде "глазка". Согласно нашей диаграмме, вначале создаётся (create) и заполняется (input) информацией объект :CData. Затем следует ветвление. Если список (list) пустой (empty), то этот элемент подсоединяется непосредственно к указателю списка, иначе (isn't empty) - к последнему элементу списка. Итак, первый или очередной элемент включён в список и его адрес также зафиксирован в указателе pLast. Хотелось бы добавить, что язык UML содержит нотацию и для представле-

Рис. 7.4.1. Диаграмма видов деятельности Inf-приложения для функции Enter() класса CInf

7.5. Поэтапная разработка Inf-приложения на языке C++/CLI Самое неразумное решение - это после получения диаграммы классов и других диаграмм языка UML, отображающих многогранно статическую и динамическую стороны нашей программы, приступить к полному написанию её программного кода, а затем к тестированию. Взгляните на диаграмму классов нашего Inf-приложения нарис.7.2.3. Какой бы сложности не была программа, её код должен создаваться и наращиваться поэтапно. Особенностью объектно-ориентированной поэтапной разработки программы является то, что наращиваемый код определяется реализуемым на текущем этапе классом, или группой классов, отдельное использование которых неуместно. Здесь класс или группа классов является кирпичиком или блоком, из которых строится программный код.


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


разно реализовать, разбив их на несколько более простых и понятных функций и операторов. После отладки класса надо ещё раз проанализировать его содержимое, инкапсулировав всё, что не относится к интерфейсу. Реализуем 1пГС++/СЫ-приложение, разрабатывая его поэтапно снизувверх.

Этап 1 Отладим класс CComputer. На этом этапе, располагая только описанием класса CComputer, создадим в главной функции main() объект pComp класса CComputer и вызовем интерфейсные функции Enter() и Show(), как в примере 7.5.1. Пример 7.5.1. Реализация класса CComputer. /////////////// // C + + / C L I Этап 1 #include "stdafx.h" using namespace System; ref class CComputer

{

// Класс компьютера

String ^producer; // Изготовитель компьютера int year; // Год изготовления public: // Интерфейсная функция ввода данных о компьютере void Enter( ) {

>

producer= Console::ReadLine ( ) ; year= System::Convert::Tolnt32 (Console::ReadUne ());

// Интерфейсная функция вывода данных о компьютере void Show ( ) {

>;

>

Console::Write ("Producer= {0}", producer); Console::WriteLlne (" Year= {0}", year.ToString ());

// Главная функция Inf-приложения 1 этапа void main ( ) {

>

CComputer ^pComp= gcnew CComputer; pComp -> Enter ( ) ; // Ввести данные pComp -> Show ( ); // Вывести данные

// Объект-компьютер в куче

Отладка класса сводится к написанию функции main, всесторонне использующей интерфейс объектов класса CComputer.


Отладка этого этапа показала, что при ошибочном вводе вместо цифр года иных символов происходит аварийное завершение программы, то есть функция Enter() требует доработки, связанной с проверкой входных символов. Ошибочную ситуацию легко разрешить, применив рекомендованную в языке Java и C# обработку исключений. Исключениями (exceptions) представляются ошибки, возникающие при функционировании программы, например, при делении на нуль или открытии несуществующего файла. В нашем случае ошибка возникла из-за несоответствия формата данных - функция ToInt32() не смогла преобразовать в число строку, содержащую отличный от цифры символ, и выбросила исключение (throw exception). Для обработки исключений применяются try-catch блоки. В блок try помещается фрагмент программы, в котором возможен выброс исключения. А непосредственно после этого блока помещается один или несколько блоков catch, каждый из которых реагирует на указанный в нём тип исключения. Если исключение указанного типа было выброшено, то срабатывает соответствующий блок catch, который вместо аварийного завершения программы выполняет действия, указанные программистом. Подробнее об исключениях излагается в разделе 10.10. Исправим аналогичную ошибку в следующем этапе разработки программы, поместив обработку исключения в функцию Enter() класса CShop.

Этап 2 На втором этапе, располагая только описанием класса CShop, создадим в главной функции main() объект этого класса и протестируем его с функцией Enter(), использующей обработку исключения. В примере 7.5.2 обработка исключения выделена жирным шрифтом. Пример 7.5.2. Реализация класса CShop. /////////////// // C + + / C L I Этап 2 #include "stdafx.h" using namespace System; refclass CShop

{

//Классмагазина

String A place; // Указатель на строку с адресом магазина int cost; // Стоимость компьютера public: // Интерфейсная функция ввода данных о магазине void Enter ( ) { try { place= Console::ReadLine ( );


cost= System::Convert::ToInt32 (Console::ReadLine ( )); > catch ( E x c e p t i o n ^e) {

>

>

Console::WriteLine ( " О ш и б к а п р и вводе числа"); return;

// Интерфейсная функция вывода данных о магазине

vOiu 3llUW ( ) {

>;

>

Console::Write ("Adress={0}", place); Console::WriteLine (" Cost={0}", cost.ToString ());

// Главная функция Inf-приложения 2 этапа void main ( ) { CShop ^pComp= gcnew CShop; // Объект-магазин в куче pComp -> Enter ( ); // Ввести данные pComp -> Show ( ); // Вывести данные

} Аналогично первому этапу и здесь при вводе данных надо соблюдать правильность типа данных. В отличие от функции Enter() класса CComputer функция Enter() отлаживаемого класса CShop изменена и содержит обработку исключения Exception. Теперь при некорректном вводе цифр года выбрасывается запланированное нами исключение в блоке try, содержащем функцию ToInt32() преобразования строки в число, и тотчас срабатывает перехватывающий это исключение нижеследующий блок catch. Выполняется функция WriteLine(), размещённая в этом блоке. Эта функция выдаёт на консоль сообщение "Ошибка при вводе числа". Затем оператор return завершает работу нашей функции. Можно модифицировать функцию, поместив в блок цикл, обеспечивающий повторный ввод года, пока не будет введён год в виде числа. Это может сделать любознательный читатель. Изменив функцию Enter() класса CComputer аналогично функции Enter() только что отлаженного класса CShop и предполагая, что эти классы тщательно отлажены, переходим к следующему этапу.

Этап 3 Предположив, что функции Enter() и Show() классов CComputer и CShop отлажены, приступаем к разработке класса CData. Программа примера 7.5.3 включает только что отлаженные классы CComputer, CShop и новый класс CData, который надлежит отладить совместно с этими классами. Поскольку


классы CComputer и CShop считаются отлаженными, то причиной ошибок, возникших при отладке программы, следует считать только вновь введённый класс CData. Таким образом ссужается размер программы, подлежашей отладке. Пример 7.5.3. Реализация класса CData. пшпишш // C + + / C L I Этап 3 -,~,-i,iHp "qMafx.h" using namespace System; ref c!ass CComputer

// Класс компьютера

{ btring ^producer; int year;

public:

// Изготовитель компьютера // Год изготовления

// Интерфейсная функция ввода данных о компьютере void Enter( )

{

try

{

producer= Console::ReadLine ( ); year= System::Convert::ToInt32 (Console::ReadLine ( ));

}

catch (Exception ^e)

{

>

>

Console::WriteLine (" Ошибка при вводе числа"); return;

// Интерфейсная функция вывода данных о компьютере void Show ( )

{

};

}

Console::Write ("Producer={0>", producer); Console::WriteLine (" Year={0>", year.ToString ( ));

ref class CShop

// Класс магазина

String ^place; // Указатель на строку с адресом магазина int cost; // Стоимость компьютера Pub!ic: // Интерфейсная функция ввода данных о магазине void Enter ( ) { try { place= Console::ReadLine ( ); cost= System::Convert::ToInt32 (Consolc;:RcadUne ( ));

}


catch ( E x c e p t i o n {

>

>

А

е)

Console::WriteLine (" Ошибка при вводе числа"); return;

// Интерфейсная функция вывода данных о магазине void Show ( ) {

>;

>

Console::Wrlte ("Adress={0>", place); Console::WriteLlne (" Cost={0>", cost.ToString ( ));

ref class CData: public CShop

// Класс элемента списка

{ CComputer ^pComp; // Указатель на объект типа CComputer в куче public: CData ^pNext; CData ( ) {pComp= gcnew CComputer;> // Создать объект в куче // Интерфейсная функция ввода данных о компьютере и магазине void Enter ( ) {CShop::Enter (); pComp -> Enter ();>

>;

// Интерфейсная функция вывода данных о компьютере и магазине void Show ( ) <CShop::Show ( ) ; pComp -> Show ();>

// Главная функция Inf-приложения void main ( )

{

>

CData ^pDt= gcnew CData; // Объект элемента списка pDt -> Enter ( ); // Ввести данные о компьютере и магазине pDt -> Show ( ); // Вывести данные о компьютере и магазине

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

Этап 4 Реализуем оставшуюся часть /я^приложения, поместив в программу корневой класс CInf. Пример 7.5.4. Реализация Inf-приложения. /////////////// // C + + / C L I Этап 4 #include "stdafx.h" using namespace System;


ref class CComputer <

// Класс компьютера

String ^producer; // Изготовитель компьютера lnt year; // Год изготовления public: // Интерфейсная функция ввода данных о компьютере void Enter(} { try

i

>

producer= Console::ReadLine ( ) ; year= System::Convert::ToInt32 (Console::ReadLine ( ));

catch (Exception {

>

>

Л

е)

Console::WriteLine (" Ошибка при вводе числа"); return;

// Интерфейсная функция вывода данных о компьютере void Show ( ) { Console::Write ("Producer={0}", producer); Console::WriteLlne (" Year=<0>", year.ToString ( ));

>;

}

ref class CShop

// Класс магазина

{ String ^place; // Указатель на строку с адресом магазина int cost; // Стоимость компьютера public: П Интерфейсная функция ввода данных о магазине void Enter ( ) { try {

>

place= Console::ReadLine ( ); cost= System::Convert::ToInt32 (Console:;ReadLine ( ));

catch (Exception {

>

>

Л

е)

Console::WriteLlne (" Ошибка при вводе числа"); return;

// Интерфейсная функция вывода данных о магазине void Show ( ) { Console::Write ("Adress={0>", place);


>;

>

Console::WriteLlne (" Cost={0>", cost.ToString ( ));

ref class CData: public CShop {

// Класс элемента списка

CComputer ^pComp; // Указатель на объект в куче public: CData ^pNext; CData ( ) {pComp= gcnew CComputer;> // Конструктор // Интерфейсная функция ввода данных о компьютере и магазине void Enter( ) {CShop::Enter( ); pComp-> Enter( );>

>;

// Интерфейсная функция вывода данных о компьютере и магазине vold Show ( ) {CShop::Show ( ); pComp -> Show ( ) ; }

ref class CInf

// Класс главного объекта

{ CData ^pFirst; // Ссылка на первый элемент списка CData ^pLast; // Ссылка на последний элементсписка public: CInf ( ) {pFirst=nullptr; pLast=nullptr;> // Конструктор void Enter ( ); // Ввести информацию о компьютере и магазине void Show ( ); // Вывести информацию из списка voidClear(); //Удалитьсписок

>;

// Описание функций Enter(), Show() и Clear() класса CInf вне класса void CInf:: Enter ( ) // Ввести информацию в список

{

>

CData ^pNew= gcnew CData; pNew -> Enter ( ); pNew -> pNext= nullptr; if(pFirst== nullptr) pFirst= pNew; else pLast -> pNext= pNew; pLast= pNew; // Запомнить

vold CInf:: Show ( )

{

CData ^pTmp; pTmp= pFlrst; while (pTmp)

{

>

адрес последнего элемента списка

// Вывести информацию из списка // Указатель на текущий элемент списка // Присвоить адрес первого элемента списка // Просмотреть список

pTmp -> Show ( ) ; // Вывести информацию из текущего элемента pTmp=pTmp -> pNext; // Перейти к следующему элементу

void CInf::Clear ( )

{

// Создать новый объект списка и // ввести в него данные // Установить нулевую ссылку // Подключить объект к списку

CData ^pTmp= pFirst;

// Удалить список // Указателю на текущий элемент списка


while (pTmp)

{

>

// присвоить адрсс первого элемента списка // Просмотреть список

CData ^pThis; pThis= pTmp; // Запомнить адрес текущего элемента pTmp= pTmp -> pNext; // Перейти к следующему элементу pThis= nullptr; // Удалить элемент с запомненным адресом

pFirst= nullptr; pLast= nullptr;

// Обнулить адрес начала списка // Обнулить адрес конца списка

> // Главная функция Inf-приложения vold main ( ) {

>

CInf ^inf= gcnew CInf; inf -> Enter ( ); inf -> Show ( ); lnf-> Enter ( ); inf -> Show ( ); Inf-> Clear (); inf -> Enter ( ); inf -> Show ( );

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

7.6. Реализация Inf-приложения иа языке C# Пример 7.6.1. Реализация Inf-приложения иа языке C#. //////////////////// //C# using System;

public class CShop

{

// Класс магазина

string place; // Строка с адресом магазина int cost; // Стоимость компьютера // Ввести данные о магазине public void Enter ( ) {

try {

>

place= Console.ReadLine ( ); cost= System.Convert.ToInt32 (Console.ReadLine ( ));

catch(Exception e)


{

>

>

Console.WriteLine("OuJH6Ka при вводе числа"); return;

// Вывести данные о магазине public void Show ( ) {

>;

>

Console.Write ("Adress=" + place); Console.WriteLine (" Cost=" + cost);

public class CComputer { string producer; int year;

// Класс компьютера // Изготовитель компьютера // Год изготовления

// Интерфейсная функция ввода данных о компьютере public void Enter ( ) { try producer= Console,ReadLine ( ); year= System,Convert.ToInt32 (Console.ReadLine ()); catch(Exception e) Console.WriteLine("OujH6Ka при вводе числа"); return;

> // Интерфейсная функция вывода данных о компьютере public void Show ( )

i

>;

>

Console.Write ("Producer=" + producer); Console.WriteLine (" Year=" + year);

public class CData: CShop { CComputer comp; // Ссылка на объект public CData next; // Ссылка на следующий элемент списка publicCData ( ) {comp= new CComputer();>

// Конструктор

// Интерфейсная функция ввода данных о компьютере и магазине public void Enter (){base.Enter(); comp.Enter ();> // Интерфейсная функция вывода данных о компьютере и магазине public void Show ( ) {base.Show ( ) ; comp.Show ();>


public class CInf

{

// Класс главного объекта

CData first; // Ссылка на первый элемент списка CData last; // Ссылка на последний элемент списка public CInf ( ) {first= null; last= null;} // Конструктор public void Enter ( ) // Ввести информацию о компьютере и магазине {

>

CData data= new CData ( ); // Создать новый объект списка и data.Enter ( ); // ввести в него данные data.next= null; // Установить нулевую ссылку if (first == null) flrst= data; // Подключить объект к списку else last.next= data; last= data; // Запомнить ссылку на последний элемент списка

public void Show ( ) // Вывести информацию из списка { CData tmp; // Ссылка на текущий элемент списка tmp= first; // Присвоить ссылку на первый элемент списка while (tmp 1= null) // Просмотреть список

{

>

>

tmp.Show ( ); // Вывести инф-цию из текущего элемента tmp= tmp.next; // Перейти к следующему элементу

class TestInf { static void Main ( ) {

>

>

CInflnf= inf.Enter inf.Enter inf.Show

new CInf ( ); (); ( ); ( );

Рис. 7.6.1. Ввод и вывод 1пАС#-приложения


8. Управляемые данные и их использование 8.1. С в о й с т в а в языках C++/CL1, C# и Java

C++/CLI и C#. В языках C++/CLI и C# появился новый тип данных свойства. Свойство, описанное в классе, позволяет безопасно использовать связанную с ним закрытую обычную или ссылочную переменную, устанавливая или получая её значение. Свойство приписывает этой переменной имя свойства. В дальнейшем ссылка к переменной осуществляется через имя свойства. Достоинством свойства является не только контролируемое использование его переменной, но и то, что имя свойства можно применять в выражениях языка программирования. В дальнейшем мы увидим, что Visual Studio .NET предоставляет простые средства изменения данных в объектах через свойства этих объектов. Учитывая удобство свойств, почти все классы библиотеки .NET Framework изобилуют ими. При использовании свойства автоматически выполняются функции set и get, которые устанавливают или получают значение переменной свойства, позволяя при этом осуществлять её контроль или выполнять с ней какиелибо действия. Эти функции определяются при описании свойства. Пусть, например, в классе CPlane имеется свойство Speed, определяющее скорость самолёта, то для объекта plane этого класса можно установить свойство как plane.Speed= 600;

/ / C#

а получить так int speedPlane=

plane.Speed; //

C#

При присваивании свойству Speed значения автоматически выполняется функция set, которая может проверить корректность присваиваемого значения или дополнительно преобразовать его и т. д. При получении значения свойства Speed автоматически выполняется функция get. Описание свойств подчиняется строгим правилам, которые отличаются в языках C++/CLI и C#.


C#. В классе объявляется закрытая переменная свойства или ссылка, связанная со свойством. Затем описывается свойство: тип имя-свойства

{

set

{

код реализации set, использующий параметр value и переменную или ссылку свойства

У get {

У

У

код реализации get, возвращающий значение переменной свойства

или ссылки

C++/CLI. В управляемом классе объявляется закрытая переменная или закрытый дескриптор свойства. Потом описывается свойство: p r o p e r t y тип имя-свойства

{

void {

> <

тип

У

У

set (тип value)

код реализации set, использующий параметр value и переменную или ссылку свойства

get ()

код реализации get, свойства

возвращающий значение

переменной или ссылки

Если в языке C# свойство описывается без ключевого слова property в единой конструкции, включающей в себя функции set и get и использующей ключевое слово value, то в языке C++/CLI единая конструкция с функциями set и get начинается с ключевого слова свойства property, а функции set и get описываются с возвращаемым значением и параметром. В языке C# ключевое слово value представляет значение, которое присваивается свойству. Заметим, что при необходимости можно описать свойство только с одной функцией, если свойство только читаемо или только записываемо. И, разумеется, свойство должно иметь доступ public, иначе оно не применимо к объекту. Важно отметить, что из определения свойств явствует, что при установке или при возврате свойства в функциях set и get можно при необходимости выполнить над ними действия и, в частности, проверить вхождение значения свойства в указанный диапазон.


Если только осуществляется присвоение или возвращение значения свойства, то язык C++/CLI допускает простое определение свойства, в котором присвоение и возврат значения осуществляется автоматически. C++/CLI. p r o p e r t y T K n имя-свойства;

В дальнейшем, познакомившись с классами библиотеки .NET Framework, мы приятно удивимся активному использованию свойств в классах этоЙ библиотеки, и эти свойства, действительно, упростят использование объектов этих классов. В примере 8.1.1 можно сравнить реализацию свойства на языках C++/CLI и C#. Пример 8.1.1. Свойства в C++/CLI и C#. /////////////////// // C + + / C L I #include "stdafx.h" using namespace System; ref class CPIane

// Использовать пространство имён System // Класс, содержащий описание свойства Speed

{ private: lnt aSpeed;

// Закрытая переменная свойства Speed

public:

// Описание свойства Speed property int Speed

{

vold set (lnt vProp) // Установить значение свойства {

>

aSpeed= vProp;

int get ( )

>;

>

< >

|| Возвратить значение свойства

return aSpeed;

void main ( ) {

>

CPIane ^pPlane= gcnew CPIane; pPlane -> Speed= 600; Console::WriteLine (pPlane -> Speed);

// Создать объект // Присвоить значение // Получить значение и выдать

//////////////////// // C# using System;

// Использовать пространство имён System

class CPIane

// Класс, содержащий описание свойства Speed


private int aSpeed; public int Speed {

// Закрытая переменная свойства Speed // Описание свойства Speed

set {

>

aSpeed= value;

// Установить значение свойства

get

{

>

}

>

return aSpeed;

// Возвратить значение свойства

class C8_1 { static void Main ( ) { CPIane sPlane= new CPIane ( ) ; // Создать объект sPlane.Speed= 600; // Присвоить значение Console.WriteLine (sPlane.Speed); // Получить значение и выдать

> > /*

Resuft:

600 */

Достоинством свойств является то, что их можно применять в выражениях, например; sPlane.Speed= sPlane.Speed * 2; // C#

Java. В языке Java свойства можно представить состоящими из закрытой переменной свойства и двух функций. Одна из функций, имя которой начинается со слова set с последующим именем свойства, устанавливает значение свойства, а другая, имя которой начинается со слова get с последующим именем свойства, возвращает значение свойства. Пример 8.1.2 иллюстрирует реализацию свойства Speed на языке Java. Пример 8.1.2. Свойства в Java. //////////////////// // Java и J# class CPIane

<

// Класс, содержащий описание свойства

private int aSpeed;

// Закрытая переменная свойства

public void setSpeed (int Sp)

{

aSpeed= Sp;

// Установить свойство


> public lnt getSpeed ( ) // Возвратить значение свойства {

>

return aSpeed;

>

class C8_l

<

public static void main ( ) {

>

CPIane sPlane= new CPIane ( ); // Создать объект sPlane.setSpeed (600); // Присвоить значение свойства System.out.println ((sPlane.getSpeed())); // Получить значение // свойства и выдать на консоль

> /*

Result: 600 */

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

8.2. Делегаты языков C++/CLI и C# Делегат - это особый класс, объекты которого предназначены для хранения указателя функции, подлежащей выполнению. Являясь необычным объектом, объект делегата может объединять в себе с помощью операторов "+=" и "-=" несколько других объектов того же делегата, превратившись при этом в так называемый множественный объект делегат. Таким образом, объект делегата способен инкапсулировать указатели многих функций, но обязательно одного и того же формата. Инкапсулировав указатель функции, объект делегата делает его безопасным, поскольку как управляемый объект делегат контролируется системой. Делегат - это нововведение в .NET технологии и он специально введён, чтобы обезопасить программы от употребления опасных указателей. Как любой объект, объект делегата создаётся из класса. Его класс наследует базовый класс Delegate библиотеки .NET Framework. Учитывая широкое использование делегатов в программах, разработчики .NET технологии существенно облегчили применение делегатов, поручив работу по созданию требуемого класса делегата не программисту, а компилятору. В последней версии языка C# ещё более упростилось создание объекта делегата и включение в него указателей функций.


Перед созданием объекта делегата необходимо описать тип (класс) делегата, но делается это необычно. Класс делегата генерирует компилятор, встретив ключевое слово delegate на языке C# или на языке C++/CLI. Используя ключевое слово delegate, мы должны по определённому правилу определить формат функции, указатель которой инкапсулируется в этом делегате, и имя делегата. В дальнейшем имя этого делегата можно использовать для создания объектов делегата с аргументами его конструктора, задающими указатель конкретной функции. Параметры конструктора делегата несколько отличаются как для языков C# и C++/CLI, так и в зависимости от того, инкапсулируется ли в объекте делегата обычная или статическая функция класса. Вызов объекта делегата приводит к выполнению инкапсулированной в нём функции. Если же к объекту делегата добавить с помощью оператора "+=" другие объекты делегата с инкапсулированными в них функциями, то при вызове этого множественного объекта делегата выполнятся функции всех добавленных объектов в том порядке, в котором они добавлялись. Перед вызовом объекта делегата необходимо убедиться, что он не пуст, то есть обладает инкапсулированной функцией; иначе возникнет системная ошибка. Итак, располагая делегатом, мы выполняем функцию не прямо, а косвенно - через объект делегата. Мы вызываем объект делегата, и в круглых скобках задаём список необходимых аргументов. Все инкапсулированные в объекте делегата функции выполнятся с этими аргументами. Мы можем передать объект делегата в любой объект, в котором требуется выполнить его функции. Можно передать объект делегата в качестве параметра функции. Более того, объект делегата, вызванный в одном объекте, может привести к выполнению функции другого объекта. На делегатах основаны так называемые собьггия, передаваемые между объектами и приводящие к реакции этих объектов на события - выполнению так называемых обработчиков событий. По сути, событие является частным случаем делегата. Итак, делегат - это средство делегирования функций. Объект делегата вызывается в одном месте, а делегированные им функции - там, где им надлежит выполняться "по приказу" (вызову) объекта делегата. Постараемся понять суть и применение делегатов путём рассмотрения ряда примеров программ. Перед использованием делегата необходимо объявить его тип: C#.

delegate тип-возвр-значения имя-делегата (список-парам); C++/CLI. delegate тип-возвр-значения имя-делогата (список-парам); где delegate - ключевое слово делегата, указывающее компилятору о необходимости создания управляемого класса делегата с именем имя-делегата, имя-делегата - это имя типа (или класса) делегата,


тип- возвр-значения - тип возвращаемого значения делегируемой ф у н к ц и и , список-парам - список типов параметров делегируемой функции.

Знающие язык C++.NET обнаружат, что разработчики языка C++/CLI решили в последней версии этого языка определить делегат так же как и в языке C#. Объявление делегата тесно связано с форматом делегируемых им функций. Обратите внимание, что в объявлении делегата кроме его имени указаны только возвращаемое значение и список параметров функции. Такое объявление и реализация делегата предполагает делегирование функции любого имени, но точно с заданньши списком параметров и типом возвращаемого значения. Более того, один объект делегата может быть использован для одновременного делегирования нескольких таких разноимённых функций, которые в нём могут накапливаться с помощью оператора "+=". После объявления делегата перед его применением необходимо объявить ссылку или дескриптор на него, а затем с помощью оператора new или gcnew создать объект делегата описанного типа в управляемой куче и присвоить возвращённое им значение ранее объявленной ссылке или управляемому указателю (дескриптору). Ссылка и дескриптор на объект делегата объявляются следующим образом: C#. имя-делегата ссылка-на-делегат;

C++/CLI. имя-делегата

л

дескриптор;

где

имя-делегата - это имя типа (или класса) делегата, ссылка-на-делегат - имя ссылочной переменной на объектделегата, дескриптор - имя управляемого указателя (дескриптора) на объект

делега-

та.

Поскольку делегат является управляемым типом данных, то объект делегата создаётся в управляемой куче с помощью оператора gcnew на языке C++/CLI. Но создание объекта делегата применительно к обычной и статической функции класса отличается: C#. ссылка-на-делегат = n e w

имя-делегата (ссылка-на-объект.имя-об-функции);

ссылка-на-делегат = n e w

имя-делегата (имя-класса.имя-стат-функции);

C++/CLI. дескриптор-делегата

= gcnew

имя-делегата (дескриптор-объекта.


&имя-класса::имя-об-функции); дескриптор-делегата =

gcnew

имя-делегата ( &имя-класса::имя-стат-функции);

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

Фирма Microsoft постоянно модифицирует свои программные продукты операционные системы, среды разработки программ, компиляторы. Изменения коснулись не только языка C++.NET, но и языка C#. Теперь в языке C# создание объекта делегата упрощено и выглядит так C#. ссылка-на-делегат = ссылка-на-объект.имя-об-функции;// обычная функция ссылка-на-делегат = имя-класса.имя-стат-функции; // статическая функция

Упростилось и добавление в объект делегата и удаление из объекта делегата указателей функций C#. ссылка-на-делегат += {ссылка-на-объект.имя-об-функции| имя-класса.имя-стат-функции}; ссылка-на-делегат -= {ссылка-на-объект.имя-об-функции| имя-класса.имя-стат-функции>;

Подробнее о нововведениях в языке C# рассмотрено в разделе 12. Пример 8.2.1 иллюстрирует создание и использование как одиночного, так и множественного объекта делегата. Пример 8.2.1. Создание и использованисделегата. //////////////////// // C+ + / C L I #include "stdafx.h" using namespace System; delegate void Del (Strlng^);

// Объявление делегата Del

ref class CA {

public: // Статическая функция класса static void S1 (String^ s) {Console::WriteLine ("static S1: {0}", s);} // Обычная функция класса void R l (String^ s) {Console::WriteUne ("usual R1:{0}", s);}


}; void main ( ) <

// Главная функция программы

CA ^pCA= gcnew CA ( );

// Создать объект pCA класса CA

Del ^pDell= gcnew Del (&CA::S1);

// Создать объект pDell делегата Del // со статической функцией S1 // Вызвать объект pDell делегата

pDell ("Статическая");

Del ^pDel2= gcnew Del (pCA, &CA::R1); // Создать объект pDel2 делегата // Del с обычной функцией R1 pDel2 ("Обычная"); // Вызвать объект pDel2 делегата pDel2 += gcnew Del (&CA::S1); // Добавить в объект pDel2 делегата // объект делегата со статической функцией S1 pDel2 ("Обычная и статическая"); // Вызвать множественный объект // pDel2 делегата // Удалить объект pDell делегата из // множественного объекта pDel2 делегата // Вызвать объект pDel2 делегата pDel2 ("После удаления"); pDel2 -= pDell;

//////////////////// //C# using System; delegate void Del (string s);

// Объявление делегата Del

class CA { // Статическая функция класса public static void S1 (string s) {Console.WriteLine ("static S1: {0}", s);} // Обычная функция класса public void R1 (string s) {Console.WrlteLine ("usual R1:{0}", s);} static vold Maln ( )

{

// Создать объект sCA класса CA CA sCA= new CA ( ) ; Del sDell= new Del (CA.S1);

// Создать объект sDell делегата Del // со статической функцией S1 // Вызвать объект sDell делегата Del

sDell ("Статическая"); Del sDel2= new Del(sCA.Rl);

// Создать объект sDel2 делегата Del // с обычной функцией R1 // Вызвать объект sDel2 делегата Del

sDel2 ("Обычная"); sDel2 += new Del (CA.S1); // Добавить в объект sDel2 делегата // Del объект делегата Del со статической функцией S1 sDel2 ("Обычная и статическая"); // Вызвать множественный объект // sDel2 делегата Del


sDel2 -= sDell; sDel2 ("После удаления");

// Удалить объект sDell делегата из // множественного объекта sDel2 делегата // Вызвать объектдслегата

> > /* Result: static S1: usual R1: usual R1: static S1: usual R1: */

Статическая Обычная Обычная и статическая Обычная и статическая После удаления

В программах примера 8.2.1 объявлен делегат Del, предписывающий возвращаемое значение void и только один параметр типа строки для всех делегируемых им функций. В классе CA описаны статическая S1() и обычная R1() функции, выдающие на консоль значение строкового apryMeirra s. Функции Main() и main() создают объект класса CA, а затем объекты одиночного и множественного делегатов, вызывая их с разными строковыми аргументами. Одиночный объект делегата, ссылаемый с помощью ссылочной переменной sDel2 и дескриптора pDel2, после добавления к нему другого объекта делегата превращается во множественный объект делегата. Выполнение этого множественного объекта делегата вызывает выполнение двух функций включённых в него объектов делегата. Потом из множественного объекта делегата удаляется объект делегата - множественный объект делегата становится одиночным. Обратите внимание, что поскольку делегат является управляемым данным, он может применяться только в управляемых классах. Поэтому в программе на языке C++/CLI перед описанием класса CA стоит ключевое слово ref, а объект делегата создаётся в управляемой куче с помощью оператора gcnew, и ссылка на него присваивается дескриптору (управляемому указателю). Пример 8.2.2 интересен тем, что в нём показана передача указателя функции с помощью делегата в качестве параметра в другую функцию. Пример 8.2.2. Использование делегата как параметра функции. //////////////////// // C + + / C L I #include "stdafx.h" using namespace System;

// Использовать пространство имён System

delegate void del (vold); ref class CA {

// Объявление делегата del // Класс с функцией R() для делегата del


pubiic: void R (void) {Console::WriteLine ("R");}

>;

void M (del ^d) {d ( );> void main ()

{

// Глобальная функция для делегата del // Главная функция программы

CA ^pCA= gcnew CA ( );

// Создать объект класса CA

del ^pDel; // Ссылка на объект делегата del pDel= gcnew del (pCA, &CA::R); // Создать объектделегата

>

M (pDel);

// Функция M() вызовет функцию CA::R

//////////////////// // C# using System;

// Использовать пространство имён System

delegate void del ( );

// Объявление

class CA

// Класс с функциями для делегата del

{

>;

делегата del

public void R ( ) {Console.WrlteUne ("R");}

class C8_2_2 { static void Main ( ) { CA a= new CA ( ) ;

// Создать объект а класса CA

del deleg; //Ccылкaнaoбъeктдeлeгaтadel deleg= new del (al.R); // Создать объект deleg делегата del

>

M (deleg);

// Функция M() выполнит функцию CA::R

// Статическая функция для делегата del static void M (del d) { d ();> > /* Result: R */ //////////////////// // J a v a и J # class del

<

// Объявление "делегата" del

public void R ( ) {System.out.println ("R");}


> class C8_2_2 { public static void main ( ) { del d= new del (); // Создать делегирующий объект d класса del M (d); // Выполнить функцию, инкапсулированную в объекте // d "делегата" (класса del)

> // Статическая функция для делегата del static void M (del d) { d . R ( ) ; } > /*

Result: R */

C#. В примере 8.2.2 использовано пространство имён System для доступа к консольному классу и классу делегата. Вначале объявляется делегат del delegate v o i d del ( ) ;

для функций формата v o i d имя-функции ( void );

В классе CA описана обычная функция R() для делегирования делегатом del. Основной класс C8_2_2 кроме описания главной статической функции Main() включает описания статической функций M(), параметром которой является делегат del. Выполнение этой функции M() состоит в вызове делегированной функции R(). Перед использованием делегата главная функция Main() создаёт его объект. Для инкапсулирования обьгчной функции R() класса CA пришлось создать объект а этого класса, и применить ссылку а при создании объекта делегата. CA a= n e w CA ( ) ; del deleg= new del (a.R);

Объект deleg делегата передаётся в качестве аргумента в статическую функцию M(). M (deleg);


C++/CLI. Программа на языке C++/CLI схожа с программой на языке C#. Но здесь применяется управляемый класс, дескрипторы, оператор gcnew. Язык C++/CLI включает ключевое слово delegate управляемого расширения. Встретив delegate, компилятор создаёт управляемый класс делегата. Функция Main() создаёт объект pCA класса CA в управляемой куче CA ^pCA= g c n e w CA ( );

// Создать объект класса, содержащий функцию R()

Потом создаётся объект pDel делегата del del ^pDel= g c n e w del (pCA, &CA::R); // Создать объектделегата // для обычной функции R()

Этотделегат передаётся статической функции M(). J# и Java. Делегирование впервые было введено в языке Java и используется при обработке событий и уведомлений, но позднее создатели языка C# выделили делегат как специальный класс Delegate, существенно упростив его описание с помощью ключевого слова delegate. С помощью объектов делегата значительно упрощается применение функций в языках C# и C++/CLI, о чём свидетельствуют примеры 8.2.1, 8.2.2. И, как в дальнейшем мы увидим, использование делегатов существенно облегчает создание и применение событий в языках C# и C++/CLI, вносит единообразие и простоту. Если появится необходимость выполнить некоторую функцию в теле другой функции, то в языках Java и J++ приходится создать свой специальный класс, подменив им класс делегата языков C# и C++/CLI и поместив в этот класс подлежащую выполнение функцию, а затем объект этого класса, как объект делегата, передать в качестве аргумента, например, так, как в примере 8.2.2.

8.3. С о б ы т и я языков C++/CLI и C# Довольно-таки часто появляется потребность в программной системе, в какой либо её части, среагировать на нечто, случившееся в другой части. Например, выдать строку на консоль или сменить изображение другим изображением при нажатии на клавишу мыши в прикладном окне, и так далее. При нажатии на клавишу происходит (генерируется) событие (event) и на него среагирует специальная функция - обработчик (event handler) этого события, выполнив надлежащее действие. Событие и его обработчик могут локализоваться в одном приложении, но могут принадлежать разным приложениям или разным компьютерам, то есть быть отдалёнными (remoting). Напрашивается мысль о схожести между событием и делегатом, между делегируемой функцией и обработчиком. Сравним собьггие с делегатом. Выполняется событие (вызывается одиночный объект делегата) и соответствующий обработчик реагирует на событие (выполняется делегируемая объ-


екгом делегата функция). Выполняется событие (вызывается множественный объект делегата) и соответствующие обработчики реагируют на событие (выполнятся функции, инкапсулированные в добавленных объектах делегата к множественному объекту делегата). Как много схожего. И действительно, собьггие является частным случаем делегата. А чем отличается событие от делегата, видно из примера 8.3.1. Просмотрите его внимательно, убедитесь, что слово event существенно изменило употребление делегата-события. Превращённый в объект-собьггие объект-делегата можно вызвать только внутри объекта класса, в котором этот делегат-событие объявлен. Волшебное слово event заставляет компилятор внести в код нечто, что делает событие отличным от делегата. Пример 8.3.1. Событиеиделегат. /////////////// //C# using System; // Объявить глобальный DelGlob делегата public delegate void DelGlob (string s); class А

{ // Объявить локальный делегат DelLoc public delegate void DelLoc (string s); // Объявить ссылку на локальное событие public e v e n t DelLoc evLoc; // Объявить ссылку на глобальное событие public event DelGlob evGlob; // Вызвать локальное событие public vold GenerateLoc (string s) <

>

Console.WriteLine ("DelLoc:" + s); if (evLoc != null) // Если событие инициировано, то evLoc (s); // генерировать событие

// Вызвать глобальное событие public vold GenerateGlob (string s)

{

>

}

class Test {

Console.WrlteLine("DelGlob:" + s); if (evGlob != null) // Если событие инициировано, то evGlob (s); // генерировать событие


static void Main ( ) < // Создать объект класса А А a= new А ( ); // Инициировать и сгенерировать глобальное Console.WriteLine ( " — l - - D e l G l o b - - " ) ; a.evGlob+= new DelGlob (Test.Handler); a.GenerateGlob ("Flrst"); //a.evGlob ("Second"); // Error: // The event "A.evGlob' can only appear on the // slde of += or -=the (except when used from //A.evGlob ("Second"); // Error: // The event "A.evGlob' can only appear on the // side of += or -=the (except when used from

>

Console.WriteLine ("Получил:" + s);

> /* Result: —1—DelGlob— . DelGlob: First Получил: First —2—DelLoc— DelLoc: First Получил: First */ /////////////// // C+ + / C L I #include "stdafx.h" using namespace System; // Объявить глобальный делегат DelGlob public delegate vold DelGlob (String^ s); ref class А {

left hand wlthln the type А left hand within the type А

// Инициировать и сгенерировать локальное событие Console.WriteLine ( " - - 2 - - D e l L o c - - " ) ; a.evLoc+= new A.DelLoc (Test.Handler); a,GenerateLoc ("Flrst"); //a.evLoc ("Second"); // Error: // The event "A.evGlob' can only appear on the left hand // side of += or -=the (except when used from within the type А //A.evLoc ("Second"); // Error: // The event "A.evGlob' can only appear on the left hand // slde of += or -=the (except when used from wlthin the type А

static void Handler (string s) {

>

событие


public:

// Объявить локальный делегат DelLoc delegate void DelLoc (String^ s); // Объявить дескриптор на локальное событие event DelLoc ^evLoc; // Объявить дескриптор на глобальное событие event DelGlob ^evGlob; | | Вызвать локальноое событие vold GenerateLoc (String^ s) < Console::WriteLine("DelLoc: {0>", s); evLoc (s); // Генерировать событие

>

// Вызвать глобальное событие void GenerateGlob (String^ s)

{

>;

>

Console::WriteLine("DelGlob: {0}", s); evGlob (s); // Генерировать событие

ref class В

{

public: static void Handler (String^ s) {

>;

>

Console::WriteLlne ("Получил: {0>", s);

void main ( )

{ // Создать объект класса А А ^a= gcnew А ( ); // Инициировать и сгенерировать глобальное событие Console::WrlteLine ( " — i — D e l G l o b — " ) ; а -> evGlob+= gcnew DelGlob (&B::Handler); а -> GenerateGlob ("First");

>

// Инициировать и сгенерировать локальное событие Console::WriteLlne ( " ~ ~ 2 - - D e l L o c - ~ " ) ; а -> evLoc+= gcnew A::DelLoc (&B::Handler); а -> GenerateLoc ("Flrst");

В программе примера 8.3.1 объявлен делегат DelGlob вне класса и делегат DelLoc внутри класса. Но вместо ссылок и дескрипторов на объекты делегатов в класс А помещены ссылки и дескрипторы на объекты событий.


Объявлению ссылки предшествует слово event, а объявлению дескриптора — также event. Интерфейсные функции GenerateGlob() и GenerateLoc() генерируют соответственно собьггия глобального делегата DelGlob и собьггия локального делегата DelLoc. Чрезвычайно важно обратить внимание на строки программы, признанные компилятором ошибочными. Из них следует, что в отличие от делегата, событие нельзя выполнять вне объекта, в классе которого это событие объявлено ! Событие является принадлежностью конкретного объекта. Событие объекта при генерировании "отправляется" к другим объектам, и эти объекты реагируют на пришедшее собьггие с помощью своих обработчиков, связанных именно с этим объектом, отправившим событие. Важно отметить, что как собьггие связано с определённым объектом, также и обработчики других объектов связаны с этим конкретным событием и соответственно - с его объектом. То есть имеет место жесткая фиксация связи между событием и теми обработчиками, которые должны выполниться при генерировании этого события. Применительно к событиям введена следующая терминология. Объект, генерирующий ifiring) события, называют источником (source), а объект, получающий событие, называют приёмником (sink). Функции приёмника, реагирующие на собьггие, называют обработчиками (handlers) собьггия. Часто используются и другие термины. Объект, генерирующий события, называют издателем publisher). Говорят, что издатель публикует события, на которые должны подписаться подписчики (subscribers) - объекты, обработчики которых реагируют на события издателя. На событие указывает ссылка или дескриптор в объекте-издателе (объекте-источнике). Подписка на событие заключается в привязке к событию требуемого обработчика, то есть создание объекта делегата, инкапсулирующего обработчик объектаподписчика (объекта-приёмника), и добавления этого делегата к объектусобьггию издателя. Объект события представляет собой особый множественный объект делегата, который может вызываться только в объекте-издателе. Важна роль собьггий при программировании .NET компонентов. Являясь .NET классом, компонент может быть очень простым или очень сложным. Компоненты могут функционировать параллельно с другими компонентами, взаимодействуя друг с другом посредством собьггий и образуя сложную многокомпонентную программную систему. Перед началом работы программной системы устанавливается связь между её компонентами, то есть осуществляется подписка компонентов-подписчиков на нужные им события конкретных компонентов-издателей. И только после завершения подписки компоненты начинают выполняться, передавать и получать собьггия. Заметим, что собьггие - это не просто факт свершения чего-либо. С событием обычно связывают объект с данными. Так что, получив событие, обработчик компонента-подписчика извлекает из этого объекта данных потребную информацию, которая может меняться с каждым новым появлением этого события.


После этого предварительного введения рассмотрим события подробнее. Также как и в случае делегата, прежде чем объявлять событие, необходимо объявить делегат собьггия. Делегат события определяет формат функции - обработчика события. Подписавшиеся на событие обработчики всех его подписчиков должна соответствовать этому формату, то есть иметь один и тот же список типов параметров и тип возвращаемого значения. Делегат события может быть объявлен вне класса объекта, если он требуется для классов других объектов или используется самостоятельно. Но можно объявить делегат события и в классе объекта, в котором будет генерироваться событие. Если событие важно и часто используется многими программистами, то для него создают свой собственный делегат и класс для объекта данных этого собьггия. Например, для широко используемого собьггия MouseDown нажатия на клавишу мыши создан делегат MouseEventHandler и класс MouseEventArgs для данных. После объявления делегата события объявляется ссылка или дескриптор на событие: C#.

event C++/CLI. event

имя-делегата-события

ссылка-на-событие;

имя-делегата-события ^ дескриптор-события;

где event - ключевое слово, определяющее событие, имя-делегата-события - имя делегата события, определяющего формат функций-обработчиков события, ссылка-на-событие - имя ссылки на объект события, при свершении которого выполняются функции, инкапсулированные в объекте события, дескриптор-события - имя управляемого указателя на объект события, при свершении которого выполняются функции, инкапсулированные в объекте события. C#, В классе объекта-источника события объявляется открытая функция активизации, вызов которой генерирует событие: public void имя-функции-активизации ( ) {

>

if{ ссылка-на-событие != null) ссылка-на-событие (список-аргументов

);

В функции активизации обязательно присутствует условный оператор, который только в том случае, когда объект собьггия инициализирован функциями-обработчиками с помощью объектов делегата события, выполняет объект события - при этом выполняются все зарегистрированные функцииобработчики данного события и им передаётся список значений аргументов. C++/CLI.


public: vold имя-функции-активизации ( )

{

>

if(дескритор-собьггия\= дескриптор-собыгия

nullptr) (список-аргументов );

Здесь выполняется объект события, на который указывает дескрипторсобыгия. Функции-обработчики подписываются на это собьггие посредством накопления их указателей с помощью объектов делегата события и оператора "+=" применительно к объекту события. Обработчик собьггия может быть описан как обычная или как статическая функция класса объекта приёмника собьггия. Как свершить (сгенерировать) событие? Это делается просто - создаётся объект источника собьггия и вызывается его открытая функция активизации собьггия с необходимыми аргументами. Разумеется, до этого необходимо создать объекты-приёмники и связать (подписать) событие объекта источника с функциями-обработчиками приёмников. Пример 8.3.2 иллюстрирует сказанное. Пример 8.3.2. Использование события. //////////////////// // C + + / C L I #include "stdafx.h" using namespace System; delegate void delEv ( ) ;

// Делегат события

ref {

class GenEv

// Класс объекта-источника события

public: eventdelEv ^pEv; void GenerateEv ( ) {

//Дескрипторсобытия // Функция- генератор события

>

>;

ref class UseEv

{

if(pEv

!= nullptr) pEv ( ) ;

// Класс объекта-приёмника события

public: static vold HandlerEv ( )

{

>;

>

vold main ( )

// Генерировать событие !

// Функция-обработчик события

Console::WriteLine ("Объект класса UseEv получил событие");


>

GenEv ^pGenEv= gcnew GenEv; // Создать объект-источник события UseEv ^pUseEv= gcnew UseEv; // Создать объект-приёмник события pGenEv -> pEv += gcnew delEv (UseEv::HandlerEv); // Добавить // обработчик события pGenEv -> GenerateEv ( ); // Сгенерировать событие

//////////////////// //C# using System;

delegate void delEv ( );

// Делегата события

class GenEv

// Класс объекта-источника события

{

>

public event delEv genEv; // Ссылка на событие public void GenerateEv ( ) // Функция- генератор события { if (genEv != null) genEv ( );

>

class UseEv

{

>

// Класс объекта-приёмника события

public void HandlerEv ( )

{

// Событие с функциями-обработчиками ? // Да I Генерировать событие I

// Функция-обработчик

Console.WriteLine ("Объект класса UseEv получил событие");

>

class TestEv { static vold Main (string[] args) {

>

>

GenEv gEv= new GenEv ( ); // Создать объект-источник события UseEv uEv= new UseEv ( ) ; // Создать объект-приёмник события gEv.genEv += new delEv (uEv.HandlerEv); // Добавить обработчик gEv.GenerateEv ( ); // Сгенерировать событие

/*

Result: Объект класса UseEv получил событие

V //////////////////// / / J a v a иЛ++ class GenEv

{

UseEv uE;

// Класс объекта-источника события // Ссылка на объект-приёмник


public GenEv (UseEv UE) { | | Инициализировать ссылку

uE= UE;

} public void GenerateEv ( ) // Функция- генератор события <

>

>

uE.HandlerEv();

| | Выполнить обработчик

class UseEv

{

>

// Класс объекта-приёмника события

public void HandlerEv <

>

( )

// Функция-обработчик

System.out.println ("Object of the UseEv type received the message");

class TestEv { public static void main(String[] args) {

>

>

UseEv uEv= new UseEv ( ); // Создать объект-приёмник события GenEv gEv= new GenEv (uEv); // Создать объект-источник события gEv.GenerateEv ( ) ; // Сгенерировать событие

I* Result: Object of the UseEv type received the message */

Можно отправить одно событие сразу нескольким объектамприёмникам, указав при этом нужные значения аргументов для функцийобработчиков. Такое событие называется широковещательным и иллюстрируется в программе примера 8.3.3. Пример 8.3.3. Широковещательное событие. //////////////////// // C + + / C L I #include "stdafx.h" using namespace System; delegate vold delEv (String^);

// Делегат события

ref

// Класс объекта-источника события

{

class GenEv

public: event delEv ^pEv; void GenerateEv (String {

Л

str)

// Дескриптор события // Функция- генератор события


//if (pEv != NULL) pEv (str);

// Событие с обработчиками ? // Генерировать событие !

>; ref class UseEv

// Класс объекта-приёмника события

{

String ^name; // Название объекта public: UseEv (String ^nameV ) {name= nameV;} void HandlerEv (String ^st)

// Обработчик события

{

>;

>

Console::Write ("Объект {0> ", name); Console::Write (" класса UseEv получил событие"); Console::WriteLine (st);

void main ( ) { GenEv ^pGenEv= gcnew GenEv; UseEv ^pUseEvl= gcnew UseEv ("first"); pGenEv -> pEv += gcnew delEv (pUseEvl, &UseEv::HandlerEv); pGenEv -> GenerateEv ("one" ); UseEv ^pUseEv2= gcnew UseEv ("second"); pGenEv -> pEv += gcnew delEv (pUseEv2, &UseEv::HandlerEv); pGenEv -> GenerateEv ("two"); pGenEv -> pEv -= gcnew delEv (pUseEv2, &UseEv::HandlerEv); pGenEv -> GenerateEv ("three" ); > /*

Result: 06beKTfirst

клaccaUseEvпoлyчилcoбытиe

Объект first класса UseEv получил событие Объект second класса UseEv получил событие Объект first класса UseEv получил событие */

one two two three

//////////////////// // C# using System; delegate void delEv (string s);

// Делегат события

class GenEv

// Класс объекта-источника события

{

public event delEv genEv; Ц Ссылка на событие public void GenerateEv (string s t r ) // Функция- генератор события { if (genEv != null) genEv (str);

// Событие с обработчиками ? // Да I Генерировать событие !


> ciass UseEv

// Класс объекта-приёмника события

{

string name; // Название объекта public UseEv (string nameV ) {name= nameV;} public void HandlerEv (string st) // Обработчик события {

>

>

Console.WriteLine ("Объект " + name + " класса UseEv получил событие " + st);

class TestEv

{

>

static void Main ( ) {

>

GenEv gEv= new GenEv ( ) ; // Создать объект-источник события UseEv uEvl= new UseEv ("flrst"); // Создать объект-приёмник gEv.genEv += new delEv (uEvl.HandlerEv); // Подписать на событие gEv.GenerateEv ("one"); // Генерировать событие UseEv uEv2= new UseEv ("second"); // Создать объект-приёмник gEv.genEv += new delEv (uEv2.HandlerEv); // Подписать на событие gEv.GenerateEv ("two"); // Генерировать событие gEv.genEv -= new delEv (uEv2.HandlerEv); // Отказать gEv.GenerateEv ("three"); // Генерировать событие

//////////////////// / / J a v a и J++ interface IEv {

>

// Интерфейс с обработчиком

public vold HandlerEv

О;

class GenEv <

// Класс объекта-источника события // Интерфейсная ссылка

IEv iE; public GenEv (IEv IE) {

>

iE= IE;

П Инициализировать ссылку на объект

public vold GenerateEv ( ) // Функция- генератор события {

>

>

iE,HandlerEv ();

class UseEvl implements IEv

// Выполнить обработчик

// Класс объекта-приёмника события


private int num=O; public UseEvl (int Num){num= Num;} public vold HandlerEv ( )

{

>

>

// Функция-обработчик

System.out.println ("Object" + num + " o f t h e UseEv type received the message");

class UseEv2 Implements IEv {

// Класс объекта-приёмника события

private int num=O; public UseEv2 (int Num){num= Num;> public vold HandlerEv ( )

{

// Функция-обработчик

System.out.println ("Object" + num + " of the UseEv type received the message"); }

> class TestEv { public static vold main ( ) {

>

UseEvl u E v l = new UseEvl (1); UseEv2 uEv2= new UseEv2 (2); GenEv g E v l = new GenEv (uEvl); gEvl.GenerateEv ( ); GenEv gEv2= new GenEv (uEv2); gEv2.GenerateEv ( ) ;

// Создать приёмник события // Создать приёмник события // Создать источник события // Сгенерировать событие // Создать источник события // Сгенерировать событие

> /*

Result: Object 1 of the UseEvl type received the message the UseEv2 type received the message */

Object 2 of

Java. Заметим, что в классе источника Java программы применена интерфейсная ссылка iE. Чтобы развязать источник собьггия от классов приёмников, использован интерфейс IEv , включающий обработчик HandlcrEv(). В Java программе класс каждого приёмника наследует этот интерфейс IEv, обязывающий реализовать HandlerEv(). Теперь, воспользовавшись интерфейсной ссылкой iE (ссылкой на интерфейс IEv), источник, выполняя функцию GenerateEv(), вызывает (делегирует) функцию этого интерфейса (обработчик), не оперируя с именем класса. Источник может воспользоваться объектом приёмника любого класса;, воспользовавшись единственной интерфейс-


ной ссылкой. Приёмники наследуют интерфейс IEv, поэтому источник, воспользовавшись интерфейсной ссылкой, может вызвать обработчик приёмника любого класса, лишь бы этот класс наследовал данный интерфейс. Рассмотрим более сложную передачу события. Получив событие от объекта-источника, объект-приёмник, так может случиться, теперь уже передаёт своё событие другому объекту-приёмнику и так далее. В этом случае объектприёмник при получении события должен сам сгенерировать новое собьггие для другого объекта, взяв на себя функции и объекта-источника, как показано в программе примера 8.3.4. Пример 8.3.4. Связь между объектами посредством собьггий. //////////////////// // C + + / C L I #include "stdafx.h" using namespace System; delegate void delEv (String ^str); ref class А

// Делегат события

// Класс объекта-исгочника и объекта-приёмника

{

public: event delEv ^pEvAB; void GenerateEv ( )

{

}

Console:.:WriteUne ("Объект класса А послал событие FromA "); pEvAB ("FromA"); // Сгенерировать событие

void HandlerEv (String ^str) {

>;

>

ref class В

// Дескриптор pEvAB события // Сгенерировать событие

// Обработать событие

Console::WriteLine ("Объект класса А получил событие {0> ", str);

// Класс объекта-источника и объекта-приёмника

{

public: event delEv ^pEvBC; vold HandlerEv (String ^str) {

// Дескриптор pEvBC на событие // Обработать событие

Console::WriteLine ("Объект класса В получил событие {0> ", str);

>;

>

ref class С

{

Console::WrlteLlne ("Объект класса В послал событие FromB"); pEvBC ("FromB"); // Сгенерировать событие

// Класс объекта-источника и объекта-приёмника


public: event delEv ^pEvCA; void HandlerEv (String ^str)

// Дескриптор pEvCA события // Обработать событие

{

>;

>

Console::WriteLine ("Объект класса С получил событие {0} ", str); Console::WriteLine ("Объект класса С послал событие FromC"); pEvCA ("FromC"); // Сгенерировать событие

void main (void) { А ^pA= gcnew А; // Создать объект класса А, генерирующий событие pEvAB В ^pB= gcnew В; // Создать объект класса В, генерирующий событие pEvBC С ^pC= gcnew С; // Создать объект класса С, генерирующий событие pEvCA pA -> pEvAB += gcnew delEv (pB, &B::HandlerEv); // Инициализировать pEvAB pB -> pEvBC += gcnew delEv (pC, &C::HandlerEv); // Инициализировать pEvBC pC -> pEvCA += gcnew delEv (pA, &A::HandlerEv); // Инициализировать pEvCA pA -> GenerateEv ( );

>

// Сгенерировать событие из объекта pA

/*

Result: Объект Объект Объект Объект Объект Объект */

класса класса класса класса класса класса

А В В С С А

послал событие получил событие послал событие получил событие послал событие получил событие

FromA FromA FromB FromB FromC FromC

//////////////////// //C# using System; delegate void delEv (string st); class А

{

// Класс объекта, посылающего событие объекту класса В

public event delEv evAB; public void GenerateEv ( )

{

>

> c|

ass В

// Ссылка на событие // Сгенерировать событие

if (evAB 1= null) {

>

Console.WriteLine ("Объект класса А послал событие FromA"); evAB ("FromA"); // Послать событие

public void HandlerEv (string st) {

>

// Делегат события

// Обработать поступившее событие

Console.WriteLine ("Объект класса А получил событие " + st);

// Класс объекта, посылающего событие объекту класса С


public event delEv evBC; public void HandlerEv (string str) {

// Указатель события // Обработать поступившее событие

Console.WriteLine ("Объект класса В получил событие " + str); if (evBC 1= null) {

>

>

>

class С

<

Console.WriteLine ("Объект класса В послал событие FromB"); evBC ("FromB"); // Послать событие

// Класс объекта, посылающего событие объекту класса А

public event delEv evCA; public void HandierEv (string str) {

// Указатель события // Обработать поступившее событие

Console.WriteLine ("Объект класса С получил событие " + str); if (evCA 1= null) {

>

>

>

Console.WriteLine ("Объект класса С послал событие FromC"); evCA ("FromC"); // Послать событие

class TestEv

{

static void Main ( )

{

>

>

// Создать объект а класса А А a= new А ( ); // Создать объект b класса В В b= new В ( ) ; | | Создать объект с класса С С c= new С ( ); a.evAB += new delEv (b.HandlerEv); // Направить событие о т Д к В b.evBC += new delEv (c.HandlerEv); П Направить событие от В к С c.evCA += new delEv (a.HandlerEv); // Направить событие от С к А // Сгенерировать событие a.GenerateEv ( ); // объекта-источника а

Программа на языке C++/CLI и языке C# содержит класс А, содержащий функцию GenerateEv(), только генерирующую событие evAB, и функцию HandlerEv(), только обрабатывающую поступившее событие. Другие два класса В и С идентичны и содержат только одну функцию HandlerEv(), которая не только обрабатывает поступившее собьггие, но и генерирует новое событие. Эта функция обеспечивает событийную связь между двумя объектами: объект получает событие от одного объекта и после этого отправляет собственное событие к другому объекту. Главная функция Main() или main() вначале создаёт три объекта классов А, В и С. Затем устанавливается событийная связь между этими объекта-


ми. Посредством делегата delEv на событие объекта класса А подписывается обработчик HandlerEv() объекта класса В, на собьггие объекта класса В подписывается обработчик HandlerEv() объекта класса С и на событие объекта класса А подписывается обработчик HandlerEv() объекта класса С. Так установлена цепочка последовательности передачи событий между тремя объектами. Когда эта связь установлена, выполняется функция GcnerateEv() первого объекта цепочки. Результат передачи событий между объектами виден на консоли. Можно для каждой потенциально возможной ситуации предложить событие со своим именем. Тогда при решении сложных задач придётся иметь дело с очень большим числом событий и окажется громоздкой реализация связи источников с приёмниками. Для уменьшения числа событий достаточно выделить основные события и связать с каждым из них данные, конкретизирующие это событие применительно к конкретным ситуациям. При этом желательно уплотнить передаваемые данные собьггия, поместив их в один объект. Короче говоря, хорошо бы унифицировать событие, сделав связь с ним единообразной и понятной. То есть придерживаться определённых правил при работе с событиями, которые не только бы упорядочили разработку событий, но и существенно облегчили использование событий, разработанных другими программистами. Особенно это важно при использовании общеупотребительных событий, например, событий интерфейсных элементов типа Button (кнопка), ListBox (список) и других. При программировании событий рекомендуется: - в имени обработчика собьггия использовать слово Handler, - обработчики событий должны иметь возвращаемое значение void, - делегат события и, конечно, обработчик должен иметь следующий список параметров: (object sender, EventArgs args) // C# (Object ^sender, EventArgs ^args) // C++/CLI где sender (^sender) - ссылка (дескриптор) на объект-источник события, args (^args) - ссылка (дескриптор) на объект класса, порождённого из класса EventArgs, который содержит открытые (public) данные или свойства, связанные с событием. Класс EventArgs наследует класс Object и содержит единственное свойство Empty, значение false которого указывает о наличии данных, а true - об их отсутствии. Пример 8.3.5 показывают создание и применение событий с массивом строк на основе высказанных рекомендаций.


Пример 8.3.5. Событие с данными. /////////////// //C# using System; // Класс SourceBventArgs для данных типа string события из класса Source public class SourceEventArgs: EventArgs

{

>

public readonly string [ ] s; public SourceEventArgs (string [ ] s) {

>

this.s= s;

П Класс Source - источник события public class Source {

с объектом типа

SourceEventArgs

public delegate void delEvSourceHandler (object sender, SourceEventArgs se); public event delEvSourceHandler evSource; public vold FireSourceEvent (string [ ] s) {

>

>

SourceEventArgs sourceArgs = new SourceEventArgs (s); evSource (this, sourceArgs);

// Класс Sink с обработчиком события объекта source класса Source class Sink { public Sink (Source source) {

>

source.evSource += new Source.delEvSourceHandler (FireConnectEvent);

void FireConnectEvent (object sender,

{

SourceEventArgs e)

for (int k=0; k<e.s.Length; k++) switch (e.s [k]) {

>

case"Russia": // строка Russia данных события Console.WriteLine ("Russia - Россия"); break; case"Greece": // строка Greece данных события Console.WriteLine ("Greece - Греция"); break; default: // строка данных события, // не совпадающая со строками Russia и Greece Console.WriteLine ("Don't know - Не знаю"); break;


> class Test { static void Main ( ) { Source sSour= new Source ( ); Slnk sSink = new Slnk (sSour);

>

>

// Создать источник // Создать приёмник

// Генерировать события string [ ] st={"Russia", "Greece", "Russia", "France">; sSour.RreSourceEvent (st);

Л

Result: Russia - Россия Greece - Греция Russia - Россия Don't know - Не знаю */

В начале примера 8.3.5 описан простой класс SourceEventArgs данных события evSource объекта типа Source. Класс SourceEventArgs включает только открытые для чтения данные- массив s строк типа string , да конструктор, инициирующий этот массив. Как видим, основное назначение этого класса - создать объект данных с массивом строк. Затем этот объект будет связан с событием. Заметим, что массив имеет доступ public, чтобы можно было его извлечь из объекта данных в обработчике. Ведь массив - это то, что событие "отправляет" объекту, подписавшемуся на это событие. В соответствии с вышеизложенными правилами наш класс SourceEventArgs наследует класс EventArgs. После класса данных события помещён класс Source источника события. Следует обратить внимание на функцию FireSourceEvent() активизации события. Перед генерированием события создаётся объект собьггия типа SourceEventArgs и затем при вызове события в качестве первого аргумента указывается ссылка this на сам объект-источник собьггия, а в качестве второго аргументы - ссылка на только что созданный объект данных типа SourceEventArgs. Класс Sink объекта-подписчика имеет конструктор, который осуществляет подписку на собьггие evSource объекта-издателя типа Source, ссылка source на который передана как параметр конструктора. Класс Sink также содержит обработчик FireConnectEvent(), который, воспользовавшись ссылкой e второго параметра на объект данных события, просматривает строки переданного массива s и выдаёт на консоль сообщения. Как уже говорилось в разделе, посвящённом массивам, каждый управ-


ляемый массив наследует класс Array, содержащий свойство Length. Свойство Length здесь используется в условии окончания просмотра массива. В функции Main() создаются объекты источника и приёмника событий. Приёмник подписывается на событие источника. Обратите внимание, что подписка осуществляется конструктором при создании приёмника. Затем собьггие генерируется при вызове интерфейсной функции FireSource() источника. Для упрощения применения изложенных выше рекомендаций при использовании событий библиотека .NET Framework содержит описание делегата EventHandler событий: C#. [Serlallzable] public delegate v o i d EventHandler(object

sender,

E v e n t A r g s e);

C++/CLI. [Serializable] public: delegate v o i d EventHandler(object

^sender,

EventArgs

A

e);

Этот делегат можно применять со всеми событиями. Но поскольку второй параметр является ссылкой на базовый класс EventArgs, который должен быть наследован каждым классом данных события, то в обработчике собьггия потребуется для извлечения данных события создать ссылку на действительный класс данных. Затем этой ссылке надо присвоить ссылку e, сделав при этом преобразование типов. Пример 8.3.6, являющийся модификацией примера8.3.5, иллюстрируетсказанное. Пример 8.3.6. Событие типа EventHandler.

/////////////// //c#

using System;

// Класс SourceEventArgs для данных типа string события из класса Source public class SourceEventArgs: EventArgs { public readonly string [ ] s; public SourceEventArgs (string [ ] s) {

>

>

this.s= s;

// Класс Source - источник события public class Source {

с объектом типа SourceEventArgs

public event EventHandler evSource; public void FireSourceEvent (string [ ] s)


>

>

SourceEventArgs sourceArgs = new SourceEventArgs (s); evSource (this, sourceArgs);

// Класс Sink с обработчиком события объекта source класса Source class Sink { public Sink (Source source) {

>

source.evSource += new EventHandler (this.FireConnectEvent);

void FireConnectEvent (object sender, EventArgs args) { S o u r c e E v e n t A r g s e= (SourceEventArgs) args; for (int k=0; k<e.s,Length; k++) switch (e.s [k]) {

>

>

>

case"Russla": // строка Russia данных события Console.WriteLine ("Russia - Россия"); break; case"Greece": // строка Greece данных события Console.WriteLine ("Greece - Греция"); break; default: // строка данных события, // не совпадающая со строками Russia и Greece Console.WriteLine ("Don't know - Не знаю"); break;

class Test { static void Main ( )

{ Source sSour= new Source ( ); Sink sSink = new Sink (sSour);

> /*

>

// Создать источник // Создать приёмник

// Генерировать события string [ ] st={"Russia", "Greece", "Russia", "France"); sSour.FireSourceEvent (st);

Result: Russia - Россия Greece - Греция Russia - Россия Don't know - Не знаю


*!

В программе примера 8.3.6 исключено описание нашего делегата delEvSourceHandler, применённого в примере 8.3.5. Вместо него использован стандартный делегат EventHandler. Это отразилось на программе: иначе объявлено событие evSource, список параметров обработчика FireConnectEvent соответствует делегату EventHandler, в теле обработчика пришлось объявить ссылку на класс SourceEventArgs данных события и привести к его типу тип аргумента args. Фирма Microsoft рекомендует предпочтительное применение делегата EventHandler, и мы будем также его использовать.

8.4. У в е д о м л е н и я и с о б ы т и я в Java 8.4.1. У в е д о м л е н и я в Java

Появившись ранее языка C#, язык Java впервые ввел делегирование функций, которое, например, реализовано в механизме уведомления. Уведомить некоторый объект в Java - значит послать ему сообщение (собьггие, уведомление), выполнив предопределённые функции setChanged () и notifyObservers(), на которое среагирует предопределенный обработчик update() этого объекта. При этом, конечно, можно передать и данные. При уведомлении выделяют два объекта - наблюдаемый объект (observable object) и объект-обозреватель (observer). Наблюдаемый объект может послать уведомление объекту-обозревателю, выступая в качестве объекта-источника события. Так что, в общем случае, можно представить объектобозреватель как некий надзиратель над множеством наблюдаемых объектов, которые при необходимости уведомляют его об их изменениях. И он на каждое уведомление реагирует, выступая в качестве объекта-приёмника событий. Итак, если сопоставить это уведомление языка Java с соответствующим делегированием языка C#, то наблюдаемым объектам языка Java соответствуют источники собьггий (издатели) языка C#, а обозревателям языка Java - приёмники (подписчики) языка C#. Один или несколько обозревателей языка Java могут наблюдать за одним или несколькими наблюдаемыми объектами. Вспомним, в языке C# также один или несколько приёмников могут подписаться на события одного или нескольких источников. Специальный класс Observable и интерфейс Observer языка Java позволяют создать наблюдаемые объекты и обозреватели. Наблюдаемый объект, класс которого должен наследовать класс Observable, генерируют событие к обозревателю, на которое реагирует предопределённая функция update() этого обозревателя, являющаяся обработчиком этого события. Генерирование собьггия (уведомление) осуществляется в на-


блюдаемом объекте посредством вызова предопределённых функций ^etChanged () и notifyObservers(), при этом единственный аргумент функции notifyObservers() передаётся обозревателю как второй аргумент функции update() этого обозревателя. В качестве второго аргумента функции update() может использоваться только объект. Итак, создав специальный объект данных, содержащий изменяющиеся данные, можно уведомить обозреватель об изменениях в наблюдаемом объекте, передав ему эгот специальный объект. J io существу, здесь осуществляется делегирование функции updatc(). Но в огл"*:ии от я з ы к ? C&, и к-отпппм я в н о применяется объект делегата, о языке Jova явно объект делегата не применяется. Итак, если возникла необходимость уведомить некоторый объект (некоторые объекты) об изменениях в данном объекте, то следует наследовать класс уведомляемого объекта (обозревателя) из интерфейса Observer, а класс уведомляющего объекта (наблюдаемого объекта) из класса ObservabIe. Затем требуется подписать обозреватель, воспользовавшись функцией adilObserver(). Теперь требуемая связь установлена, и можно уверенно пользоваться функциями setChanged () и notifyObservers() наблюдаемого объекта для уведомления, то есть вызова обработчика update() подписавшегося обозревателя. В наблюдаемом объекте класса, наследующего класс Observable, можно воспользоваться следующими функциями класса Observable: void addObserver (Observer obs) - добавить в список объектов, обозревающих данный наблюдаемый объект, объект obs обозревателя (подписать на данный наблюдаемый объект указанный объект obs обозревателя). int countObservers () - возвратить число обозревателей данного наблюдаемого объекта. void

deleteObserver (Observer obs) - удалить из списка объектов, обозревающих данный наблюдаемый объект, объект obs обозревателя.

void deleteObservers () - удалить все объекты из списка объектов, обозревающих данный наблюдаемый объект (обнулить список подписчиков). boolean hasChanged () - возвратить значение true, если данный наблюдаемый объект был изменён (обозреватели уведомляются об изменении в данном наблюдаемом объекте). orotected void clearChange() - изменить состояние данного наблюдаемого объекта, при котором будут игнорироваться уведомления об изменениях в нём с помощью функции hasChanged ().

void

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


void notifyObservers (Object obj) - уведомить всех обозревателей данного наблюдаемого объекта вызовом функции update() со вторым передаваемым аргументом obj, о том, что он был изменен. То есть объект obj передаётся из данного наблюдаемого объекта в обозреватели в качестве второго аргумента функции update() в объектах обозревателей. protected void setChanged () - вызвать при изменении данного наблюдаемого объекта (когда объект, инициирующий обращение, изменился)

Интерфейс Observer обозревателя содержит единственную функцию update() - обработчик уведомлений:. void update(Observable observable. Object obj) - эта функция данного объекта обозревателя вызывается при выполнении функции notifyObservers() наблюдаемого объекта observable, на который подписан данный объект обозревателя. Значение аргумента obj пересылается в качестве аргумента функции notlfyObservers() наблюдаемого объекта.

Функция update() вызывается, когда происходят изменения в наблюдаемом объекте. Осознавая важность событий и уведомлений при разработке сложных программ, рассмотрим разные примеры, поясняющие применение уведомлений в различных ситуациях. Пример 8.4.1.1. Обозреватель и наблюдаемый объект. /////////////// // J a v a и J # // Наблюдатель и наблюдаемый Import java.util.*; class Watcher i m p l e m e n t s Observer <

||

Класс обозревателя

public void u p d a t e (Observable obj, Object arg) { System.out.println ("1= " + ( ( I n t e g e r ) a r g ) . i n t V a l u e ( ));

>

}

rlass BeingWatched e x t e n d s Observable // Класс наблюдаемого объекта

(

public void perform ( ) { for (int i=0 ;i < 3; 1++) { s e t C h a n g e d ( ); notifyObservers ( n e w I n t e g e r (i)); try{ Thread.sleep (100);

>

catch(InterruptedException e){ System.out.println("' perform: sleep' interrupted");

>

>

>


public class OneWatcherOneBeingWatched { public static vold main ( )

{

>

// Создать наблюдаемый объект bW BelngWatched bW= new BeingWatched ( ) ; // Создать объект w обозревателя Watcher w= new Watcher ( ) ; /'/' Подписать обозреватель bW.addObserver (w); // Инициировать уведомления bW.perform ( );

> /* Result: i= 0 i= 1 i= 2 */

Пример 8.4.1.1 иллюстрирует применение одного наблюдаемого объекта bW класса BeingWatched и одного обозревателя w класса Watcher. Класс BeingWatched наблюдаемого объекта, как положено, наследует класс Observable и включает открытую функцию perform(), при вызове которой трижды выполняется пара функций setChanged() и notifyObservers(), которые трижды уведомляют подписавшегося обозревателя. Класс Watcher обозревателя наследует, как надлежит, интерфейс Observer и включает функцию update() - обработчик уведомления. Функция main() класса OneWatcherOneBeingWatched создаёт наблюдаемый объект bW и объект w обозревателя. Затем функция addObserver() подписывает обозреватель w. Теперь, вызвав функцию perform(), наблюдаемый объект трижды уведомляет обозревателя. При этом наблюдаемый объект через аргумент функции notifyObservers() передаёт объект-оболочку класса Integer, инкапсулирующую значение целочисленной переменной i, значения которой извлекаются обозревателем из второго параметра arg функции updateO. В примере 8.4.1.2 применяются два обозревателя и один наблюдаемый объект. Пример 8.4.1.2. Два обозревателя и один наблюдаемый объект. /////////////// // J a v a и 3# II Два наблюдателя и один наблюдаемый объект importjava.util.*; class Watcherl i m p l e m e n t s Observer

|| Класс первого обозревателя


public void update (Observable obj, Object arg)

{

>

>

System.out.println("Watcherl: f= " + ((Float)arg).floatValue ( ));

class Watcher2 implements Observer

{

public void update (Observable obj, Object arg)

\r

>

// Класс второго обозревателя

System.out.println("Watcher2: f= " + ((Float)arg).floatValue ( ));

>

class BeingWatched e x t e n d s O b s e r v a b l e // Класс наблюдаемого объекта

{

public void perform {

()

for (float i=O.OF ;i < 0.3; i= 1+ 0.1F) { s e t C h a n g e d ( ); n o t i f y O b s e r v e r s ( n e w Float (i)); try {

>

Thread.sleep (100);

catch(InterruptedException e) {

>

>

>

>

System.out.println('" perform: sleep' interrupted");

public class TwoWatchersOneBeingWatched

<

public static void main ( ) { // Создать наблюдаемый объект bW BeingWatched bW= new BeingWatched(); // Создать первый объект w l обозревателя Watcherl w l = new Watcherl(); // Создать второй объект w2 обозревателя Watcher2 w2= new Watcher2(); // Подписать обозреватель w l bW.addObserver(wl); // Подписать обозреватель w2 bW.addObserver(w2); // Инициировать уведомления bW.perform ( ); System.out.print!n("countObservers()= " + bW.countObserversQ);


> /* Result: Watcherl: f= 0.0 Watcher2: f= 0.0 Watcherl: f= 0.1 Watcher2: f= 0.1 Watcherl: f= 0.2 Watcher2: f= 0.2 countObservers()= 2 */

В примере 8.4.2 два обозревателя wl и w2 наблюдают один наблюдаемый объект bW. Наблюдаемый объект bW меняет значение числа с плавающей точкой от значения 0.0F до значения 0.2F с шагом 0.1F и о каждом изменении уведомляет обоих обозревателей, передавая при этом значение числа. Объекты обозревателей подписаны на уведомления с помощью функции addObserver(). Из результата, выданном в консольное окно, видно, что уведомление при каждом шаге изменения одновременно поступает к двум подписавшимся обозревателям. Вызов функции countObservers()) подтверждает, что наблюдаемый объект bW обозревается двумя обозревателями. Конечно, наблюдаемые объекты могут передать обозревателям при уведомлении любой объект, что иллюстрируется в примере 8.4.1.3. Пример 8.4.1.3. Один обозреватель и два наблюдаемых объекта с объектом данных. /////////////// // Java // Один наблюдающий и два наблюдаемых importjava.utll,*; class Data

// Класс объекта данных уведомления

{

>

boolean b; String s; public Data (boolean В, String S) {b= В; s= S;}

class Watcher Implements O b s e r v e r

{

>

// Класс обозревателя

public void update (Observable obj, Object arg) {

>

System.out.println (((Data)arg).s + " " + ((Data)arg).b);

class BeingWatchedl e x t e n d s O b s e r v a b l e // Класс наблюдаемого объекта

{


public void go ( ) {

>

>

Data d = n e w Data (true, " B e i n g W a t c h e d l " ) ; setChanged ( ); notifyObservers (d);

class BeingWatched2 e x t e n d s Observable // Класс наблюдаемого объекта

{

public void go ( ) { setChanged ( ); notifyObservers (new Data (false, " B e i n g W a t c h e d 2 " ) ) ;

>

>

public class OnelWatcherTwoBeingWatched { public static vold main ( ) {

>

// Создать первый наблюдаемый объект b W l BelngWatchedl b W l = new BeingWatchedl ( ) ; // Создать второй наблюдаемый объект bW2 BeingWatched2 bW2= new BeingWatched2 ( ) ; // Создать объект w обозревателя Watcher w= new Watcher(); // Подписать обозреватель w bWl.addObserver (w); bW2.addObserver (w); // Инициировать уведомления bWl.go ( ); bW2.go ( );

> /* Result: BeingWatchedl true BeingWatched2 false */

Возможно взаимное уведомление однотипных объектов, что показывает программа примера 8.4.1.4. Пример 8.4.1.4. Два однотипных объекта уведомляют друг друга. ////////// // J a v a // Два объекта уведомляют друг друга importjava.util.*; class Data

// Класс объекта данных уведомления


>

String s; public Data (String S) {s= S;>

class BeingWatchedWatcher extends O b s e r v a b l e i m p l e m e n t s Observer

{ int num; // Номер уведомляющего объекта BeingWatchedWatcher(int N) {num= N;> public void go ( ) // Уведомить < setChanged ( ); notifyObservers (new Data ("BeingWatched "+ num));

}

// Обработать уведомление public void update (Observable obj, Object arg) {

>

>

System.out.println (((Data) arg).s);

public class OnelWatcherAndBeingWatched

{ public static void main ( )

i

>

// Создать первый наблюдаемый объект-обозреватель Ы BeingWatchedWatcher b l = new BeingWatchedWatcher(l); // Создать второй наблюдаемый объект-обозреватель b2 BeingWatchedWatcher b2= new BeingWatchedWatchcr(2); // Подписать обозреватель b l bl.addObserver(bZ); // Подписать обозреватель b2 b2.addObserver(bl); // Уведомить bl.go(); b2.go();

> /* Result: BeingWatched 1 BeingWatched 2 */

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


наследует этот интерфейс. В отличие от уведомления здесь не требуется использовать заранее предопределённый интерфейс, класс, функции и обработчик. Имя интерфейса и обработчика события могут быть выбраны произвольно. Для подключения объектов-приёмников к объекту-источнику применим функцию вида public vold add7ypeLlstener (TypeLlstener tL),

рекомендуемую для этих целей в модели делегирования собьггий. Здесь 7^peListener - это имя интерфейса, включающего обработчик. Если придерживаться рекомендаций модели делегирования событий, то желательно оставить в имени интерфейса слово Listener. Тогда имя интерфейса будет соответствовать установленным правилам и, кстати, аналогичные имена нам встретятся в дальнейшем при обработке собьггий мыши и управляющих элементов, размещённых в прикладном окне приложений. Итак, класс объекта-источника собьггия должен определить указанную функцию для того, чтобы в дальнейшем, создав объект источника, можно было бы подключить к нему требуемые объекты приёмников. В соответствии с терминологией язьнса Java объект-приёмник, воспринимающий события (или уведомления), называется блоком прослушивания (listener). Блок прослушивания должен обязательно удовлетворять двум требованиям. Во-первых, он должен бьггь подписан (зарегистрирован) объектомисточником события. Во-вторых, он обязан реализовать обработчик события. Таким образом, разрабатывая объект-источник, необходимо разработать интерфейс события, включающий обработчик, и класс объекта данных события. Объект данных события будет сопровождать событие, чтобы передать обработчику конкретные данные. Класс объекта-приёмника собьггия (блок прослушивания) должен наследовать этот интерфейс, чтобы реализовать корректно соответствующий обработчик. И как только что говорилось, объект-источник должен обеспечить возможность добавления любого или ограниченного количества объектов приёмников (блоков прослушивания), располагая специальными функциями добавления и удаления объектов приёмников в специальный их список, размещённый в этом объекте-источнике. Генерирование события в объекте-источнике осуществляется путём просмотра списка объектов приёмников (блоков прослушивания) и выполнения предопределённого интерфейсом обработчика для каждого из них. При этом в качестве аргумента в эти функции-обработчики передаётся объект события. В примере 8.4.2.1 предсгавлены класс DateSource источника события, класс DateReceiver приёмника собьггия, интерфейс DateListener и класс DataDate объекта данных события, созданные в соответствии с только что сказанным.


Данным события является строка, содержащая текущую дату и время, полученные посредством объекта класса

Date. Эту

строку объект-источник посы-

лает вместе с событием объектам-приёмникам.

Пример 8.4.2.1. Реализация источника и приёмника события // java и J# // Один объект-источник (наблюдаемый объект) и // три объекта-приёмника (наблюдателя), package cjExample_8_4_2_l; import java.utii.*; Ц Класс объекта данных события public class DataDate

< >

public String date; public DataDate (String S) {date= S;>

// Интерфейс public interface DateListener

{

>

void DatePerformed (DataDate dd);

// Обработчик

// Класс источника (наблюдаемого объекта) class DateSource

{

ArrayList aL;

// Ссылка на список

public DateSource ( ) {

>

aL= new ArrayList ();

// Создать объект списка

// ДобавиТь объект-приёмник в список public void addDateListener (DateListener dL) {

}

aL.add (dL);

// Удалить объект-приёмник из списока public void removeDateListener (DateListener dL)

{

>

aL.remove (dL);

// Сгенерировать событие public vold performedDate ( )

<

Date d= new Date ( ); // Создать объект даты System.out.println ("The event happened a t " +


d.toLocaleString()); String date= d.toLocaleString ( ) ; DataDate dd= new DataDate (date); // Создать объектданных // Выполнить подписавшиеся обработчики for (int i=0; i < aL.size(); i++) <

>

// Выбрать текущий объект-приёмник из списка DateListener m= (DateListener) aL.get(i); // Выполнить делегированный обработчик m. DatePerformed (dd);

// Класс приёмника события public class DateRecelver implements DateListener { int n;

// Номер объекта-приёмника

public DateReceiver (int N) {n= N;} // Обработчик public void DatePerformed(DataDate bD)

{

>

System.out.println ("Object" + n + " has just got the event a t " + bD.date);

public static void main ( ) {

> /*

>

// Создать объект-источник события DateSource dS= new DateSource(); // Создать первый объект-приёмник события DateReceiverdRl= new DateRecelver (1); // Создать второй объект-приёмник события DateReceiverdR2= new DateRecelver (2); // Создать третийй объект-приёмник события DateReceiver dR3= new DateRecelver (3); // Подписать первый объект-приёмник на событие dS.addDateLlstener(dRl); // Подписать второй объект-приёмник на событие dS.addDateListener (dR2); // Подписать третий объект-приёмник на событие dS.addDateListener (dR3); // Сгенерировать событие dS.performedDate ();

Result: The event happened at 05.05.2008 17:18 The object 1 has just got the event at 05.05.2008 The object 2 has just got the event at 05.05,2008

17:18 17:18


The object 3 hasjustgottheeventat05.05.2008

i7:18

*/

Класс DateSource источника события включает объект aL списка класса ArrayList, Содержимое списка пополняется при каждом вызове функции addDateListener(). Элементами объекта списка класса ArrayList являются объекты типа Object, то есть объекты корневого класса. Поэтому в список можно включить (ad()) разнотипные объекты, но при извлечении (get()) объектов из списка требуется преобразование типа Object к требуемому типу, что и делается в функции performedDate(). Осуществляется преобразование к типу DateListener - интерфейсу, из которого обязан наследоваться класс каждого объекта-приёмника. Таким образом, любой действительный приёмник, наследующий интерфейс DateListener, будет и добавляться в список aL, поскольку параметром функции addDateListener() является интерфейсная ссылка типа DateListener, и удаляться из списка, поскольку параметром функции removeDateListener() является интерфейсная ссылка типа DateListener, и извлекаться из списка по той же интерфейсной ссылке типа DateListener. Класс DateReceiver каждого объекта-приёмника, как и надлежит, наследует интерфейс DateListener и реализует обработчик DatePerformed(), навязанный этим интерфейсом. В качестве параметра обработчик использует объект данных события типа DataDate, извлекая из него для выдачи на консоль строку с датой генерирования события. Функция main() класса DateReceiver создаёт объект dS источника события и три объекта-приёмника, включает эти объекты в список aL объектаисточника, воспользовавшись функцией addDateListener(). Затем генерируется событие при вызове функции performedDate() объекта-источника. Если собьггие широко используется, то для упрощения разработки классов источника и приёмника этого события создают специальный класс события, инкапсулирующий список и предлагающий предопределённые функции, оперирующие с этим списком. С этим событием связывают конкретный интерфейс и класс данных события. Пример 8.4.2.2 иллюстрирует такие классы и интерфейс применительно к событию Happen, объект данных которого несёт информацию о причине генерирования события, дате и времени его генерирования. Причина what события является целым числом, с которым можно связать любой факт. Пример 8.4.2.2. Реализация класса и интерфейса события /////////////// // Java и J # package cjExample_8_4_2_2; importJava.utll.*; // Класс данных события class HappenData { String date;

// Дата


>

int what; // Что произошло public HappenData (String D, int W) <date= D; what= W;}

// Интерфейс interface HappenListener {

>

void performedHappen (HappenData hD);

// Класс события class Happen < ArrayLlst aL; public Happen () {

>

aL= new ArrayList ( ) ;

// Ссылка на список

// Создать список

// Добавить приёмник в список public vold addHappenListener (HappenListener hL) {

>

aL.add (hL);

// Удалить приёмник из списока public vold removeHappenListener (HappenListener hL) {

>

aL.remove (hL);

// Сгенерировать событие public vold HappenWhat (lnt What) { / / Получить объектдаты Date d= new Date ( ); // Получить текущую дату и время String s= d.toLocaleString ( ) ; // Создать объект данных события HappenData hD= new HappenData (s, What); // Выполнить событие for (int k=0; k < aL.size(); k++) {

>

>

// Выбрать приёмник из списка HappenListener h= (HappenListener) aL.get(k); // Выполнить обработчик выбранного приёмника h.performedHappen (hD);

> // Класс источника события


class HappenSource e x t e n d s H a p p e n

<

>

lnt num; // Номер объекта источника события // Сгенерировать событие public void HappenWhat (int What} {

>

super.HappenWhat (What);

// Класс приёмника события public class HappenReceiver i m p l e m e n t s H a p p e n L i s t e n e r

{

int num;

// Номер объекта приёмника

public HappenReceiver(int N) {

>

num= N;

// Обработчик public void performedHappen (HappenData hD) {

>

System.out.println ("numRec= " + num + " what= " + hD.what + " " + hD.date);

public static void main ( ) <

>

// Создать объект источника события HappenSource hS= new HappenSource ( ); // Создать первый объект приёмника события HappenReceiver h R l = new HappenReceiver (1); // Создать второй объект приёмника события HappenReceiver hR2= new HappenReceiver (2); // Подписать первый приёмник на событие hS.addHappenListener (hRl); // Подписать второй приёмник на событие hS.addHappenListener (hR2); // Сгенерировать событие hS.HappenWhat (5);

> /* Result: numRec= 1 what= 5 06.10.2007 20:10 numRec= 2 what= 5 06.10.2007 20:10

V

Классом события Happen является одноимённый класс Happen. Класс Happen включает список aL типа ArrayList и функции addHappenListener(), removeHappenListener() и HappenWhat(), оперирующие с этим списком.


Функция addHappenListener() дополняет список ссылкой на очередной приёмник, класс которого обязан наследовать интерфейс HappenListener. Функция removeHappenListener() удаляет из списка ссылку на приёмник, класс которого обязан наследовать интерфейс HappenListener. Функция HappenWhat(), просматривая элементы списка, вызывает для каждого подписавшегося приёмника его делегируемый обработчик performedHappen(). Перечисленные функции применяют объект данных события класса HappenData, интерфейс HappenListener и обработчик performedHappen(), определённый интерфейсом HappenListener. Класс HappenReceiver приёмника события наследует интерфейс HappenListener и включает обработчик performedHappen() и функцию main(), которая создаёт объект источника, объекты приемника, подписывает приёмники, воспользовавшись функцией addHappenListener(), и генерирует событие источника. Обычно класс события, интерфейс и класс собьггия скрывают в какойлибо библиотеке, предлагая воспользоваться ею при надобности в данном событии. В примере 8.4.2.3 приведена библиотека cJHappenListerner, включающая класс HappenData данных собьггия, интерфейс HappenListener и класс Happen события. Приложение cjExample_8_4_2_3, приведённоетакже в этом примере, использует библиотеку cJHappenListerner. Для применения её классов эта библиотека импортируется import cJHappenListerner.*;

Также библиотека должна быть подключена с помощью команды File/Add Reference к приложению, если используется среда разработки VisuaI Studio .NET. Как видно из примера 8.4.2.3, класс HappenSource источника наследует (extends) класс Happen собьггия, класс HappenReceiver приёмника наследует (implements) интерфейс HappenListener. Наследование (extends) классом источника класса собьггия существенно ограничивает этот класс, не позволяя ему наследовать иной класс, поскольку в языке Java допустимо наследование только одного класса. С этим приходится считаться. Если же требуется наследование класса, отличного от класса собьггия, то придется реализовать класс источника иначе, включив в него объект требуемого класса. Пример 8.4.2.3. Использование библиотеки с классом и интерфейсом события /////////////// // J # Б и б л и о т е к а (пакет) c J H a p p e n L i s t e r n e r package cJHappenListerner; importjava.uti).*;


/ / Класс данных события public class HappenData ^

>

public String date; //Дата public int what; // Что произошло public HappenData (String D, int W) {date= D; what= W;}

// Интерфейс HappenListener public interface HappenLlstener

{

>

// Обработчик void performedHappen (HappenData hD);

// Класс Happen public class Happen

{

ArrayList aL; // Конструктор public Happen ( ) {

>

aL= new ArrayList ( ); // Список

// Добавить элемент в список public vold addHappenListener (HappenListener hL) {

>

aL.add (hL);

// Удалить элемент из списка public vold removeHappenListener (HappenListener hL) {

}

aL.remove (hL);

// Сгенерировать событие public vold HappenWhat (int What) < // Получить объект даты Date d= new Date (); // Получить текущую дату и время String s= d.toLocaleStrlng ( ) ; // Создать объект данных события HappenData hD= new HappenData(s, What); // Выполнить делегированные обработчики for (lnt k=0; k < aL.size(); k++) { // Выбрать приёмник из списка HappenListener h= (HappenListener) aL.get(k); // Выполнить обработчик текущего приёмника


h.performedHappen (hD);

> > ///////////////

| | J # П р и л о ж е н и е , используюицее п а к е т c J H a p p e n L f s t e r n e r package cjExample_8_4_2_3; import cJHappenListerner.*; | | Импортировать библиотеку // Класс источника события class HappenSource e x t e n d s H a p p e n { lnt num;

// Номер объекта

public void HappenWhat (lnt What) {

>

>

super.HappenWhat (What);

|| Класс приёмника события public class HappenReceiver i m p l e m e n t s H a p p e n L i s t e n e r

{

lnt num;

// Номер объекта

public HappenReceiver (int N)

i

>

num= N;

// Обработчик public vold performedHappen (HappenData hD) {

>

System.out.println ("numRec= " + num + " what= " + hD.what + " " + hD.date);

public static vold maln ( )

{

>

>

// Создать объект-источник HappenSource hS= new HappenSource ( ) ; // Создать первый объект-приёмник HappenReceiver h R l = new HappenReceiver (1); // Создать второй объект-приёмник HappenReceiver hR2= new HappenReceiver (2); // Подписать первый приёмник на событие hS.addHappenListener (hRl); | | Подписать второй приёмник на событие hS.addHappenListener (hR2); // Сгенерировать событие hS.HappenWhat (5);


/* Result: numRec= 1 what= 5 07.10.2007 20:51 numRec= 2 what= 5 07.10.2007 20:51 */

Созданный пакет cJHappenListener существенно упростил создание приложения, применяющего событие Happen, но и наложил ряд ограничений. Класс HappenSource источника собьггия включает номер num объекта, который может быть полезен при тестировании приложения, включающего несколько объектов-источников события. Класс HappenSource наследует класс Happen библиотеки, что, ограничив его расширение другими классами, упрощает применение события Happen, поскольку наследуемый класс Нарреп уже содержит функции addHappenListener(), removeHappenListener() и HappenWhat(). Класс HappenReceiver обязан наследовать интерфейс HappenLiatener, поскольку иначе его объект не будет корректно применён в объекте источника класса HappenSource - ведь в классе HappenSource применяется интерфейсная ссылка типа HappenLiatener. Наследовав интерфейс HappenListener, класс HappenReceiver обязан реализовать обработчик performedHappen(). Подытоживая только что сказанное и взглянув ещё раз на приложение, убеждаемся в простоте применения события Нарреп. Подключаем требуемый пакет (библиотеку), создаем классы источника и приемников события, не забыв наследовать их соответственно из класса и интерфейса события, а затем создаём их объекты и обеспечиваем требуемую взаимосвязь этих объектов и генерирование собьггия, воспользовавшись предопределёнными функциями события Нарреп.


9. Потоки и синхронизация их выполнения 9.1. Процессы Процесс process) - это программа (EXE файл), представленная в операционной системе как задача {task) и управляемая супервизором задач. Для выполнения операционная система выделяет процессу необходимые ресурсы и, в частности, область памяти, называемую адресным пространством процесса. По мере надобности процесс может загружать в своё адресное пространство динамически компонуемые библиотеки (DLL — Dynamic Link Library) или выделять в нём память для своих объектов или данных, которые при необходимости могут быть удалены или заменены новыми. Процесс может создать другие процессы, используя для управления ими необходимые ЛР/-функции (API - Application Programming Interface). Будучи созданными, процессы становятся независимыми и выполняются параллельно с другими процессами. Обычно каждый процесс в операционной системе представлен своим прикладным окном (application window). Наличие окна с интерфейсными элементами позволяет не только управлять программой, но и завершить процесс, нажав на кнопку закрытия в его прикладном окне. Выполнение процесса состоит в выполнении составляющих его потоков. Для обеспечения безопасности программного кода .NET технология в рамках одного процесса создаёт один (по умолчанию) или произвольное количество (по желанию программиста) так называемых доменов, в которых выполняется управляемый код.

9.2. Потоки в C# и C++/CLI Под потоком (thread) понимают выполняющийся программный код. Выполнение процесса начинается с выполнения основного потока primary thread), представленного главной функцией main() или Main(). Основной поток обычно реализует диалог с пользователем. Основной поток инициирует, выполняет и завершает работу процесса. Если основной поток создаёт окно, то его называют основным оконным потоком. Обычно с окном связан цикл обработки сообщений операционной системы. Основной поток может создать и запустить так называемые рабочие потоки (work threads), которые будут выполняться параллельно с основным и другими потоками.


В отличие от процесса потокам не предоставляются ресурсы. Поэтому каждый из запущенных потоков будет выполняться в одном и том же адресном пространстве, выделенном процессу, к которому эти потоки принадлежат. Потоки создаются, запускаются, приостанавливаются и возобновляются путём вызова API-функций. После завершения процесса все принадлежавшие ему потоки уничтожаются. Если выполнение основного потока сводится к выполнению главной функции Main() или main() приложения, то выполнение каждого рабочего потока связано с выполнением так называемой его потоковой функции. Завершение выполнения потоковой функции завершает выполнение потока. В примерах программ данной книги потоковая функция содержит цикл while, определяющий время жизни потока. В теле оператора while вызывается функция Sleep() или sleep(), приостанавливающая выполнение данного потока на заданный промежуток времени, в течение которого выполняются другие потоки (ведь, процессор, выполняющий потоки, один!). Перед запуском некоторого потока необходимо установить значение булевского выражения его оператора while, равным true. Для завершения выполнения потока достаточно сделать это выражение, равным false. В языках Java и C# потоки создаются и применяются по разному. В каждом из этих языков рабочий поток является объектом класса Thread. Но если в языке C#, использующем библиотеку .NET Framework, объект потока можно создать везде, где он, действительно, нужен, и можно создать просто в одном потоке несколько "дочерних" потоков, то в языке Java, в виду исключительности потока, наложены ограничения, которые приходится учитывать. В языке Java имя потоковой функции для всех рабочих потоков одно и то же - run(). В классе может быть создан поток, если класс порождён (extends) из потокового класса Thread, либо наследует (implements) интерфейс Runnable. Потоки языка Java будут рассмотрены в разделе 9.3. Для реализации потоков на языке C# библиотека .NET Framework содержит более 20 классов и структур, среди которых следует, прежде всего, выделить потоковый класс Thread и делегат ThreadStart потока. Класс Thread используется для создания и управления потоками, а тип ThreadStart - для создания объекта делегата потока, инкапсулирующего потоковую функцию. Среди множества функций и свойств класса Thread мы выделим функцию Start() запуска потока (запуска потоковой функции потока), функцию Suspend() приостановки потока, функцию Resume() возобновления выполнения потока и функцию Join() ожидания завершения указанного потока. При создании экземпляра потока конструктору класса Thread передаётся в качестве аргумента объект делегата, инкапсулирующий потоковую функцию. При этом возможны два варианта перегруженного конструктора делегата: 1) указывается статическая потоковая функция некоторого класса или 2) указывается потоковая функция конкретного объекта определённого класса.


При объектно-ориентированном программировании целесообразно связать с потоком ряд данных некоторого класса, то есть, по сути дела, привязать поток к конкретному классу, данные которого будут использоваться и обрабатываться потоком. Будем такой класс называть классом потокового объекта. Создав из класса потокового объекта множество потоковых объектов и запустив их потоковые функции, мы получим параллельно функционирующие потоковые объекты. Они могут представлять, например, совокупность самолётов или людей. Каждая из этих совокупностей характеризуется набором своих данных, определяющих состояние её объектов, и набором функций, определяющих поведение объектов. Целесообразно включить в потоковый объект булевскую переменную (например, bool life), которая будет определять жизненный цикл потока и которой перед запуском этого потока присваивается истинное значение (true). Жизненный цикл реализуется в потоковой функции с помощью оператора while, условие которого содержит эту переменную. Поток выполняется при значении life, равным true, но достаточно переменной life присвоить значение false, и потоковая функция завершит своё выполнение, закончив тем самым и функционирование потока и потокового объекта. Для координации параллельного совместного выполнения нескольких потоков используются объекты синхронизации - мютексы (mutexes), мониторы (monitors) и события (events). Они позволяют выполняться в данный момент ограниченному числу потоков, связанных с некоторым объектомразделяемым ресурсом, и приостанавливать потоки до свершения определенных действий. Чтобы овладеть разделяемым ресурсом, потоки выстраиваются в очередь. Для отладки класса потоковых объектов в этот класс включим переменную num - номер экземпляра конкретного объекта, а в цикл while потоковой функции включим переменную n, определяющую номер цикла. Значения num и n, выданные на консоль, информируют о выполняющихся потоковых объектах. В нижеследующих примерах пояснено создание и использование потоковых объектов. Особое внимание уделяется использованию событий потоковыми объектами как для информирования об их выполнении, так и для связи разных функционирующих потоковых объектов с помощью событий. В примере 9.2.1 рассматривается параллельное выполнение двух потоковых объектов. Пример 9.2.1. Два параллельно выполняющихся потоковых объекта на языке C#. //////////////////// //C# using System; using System.Threading;

namespace CsTl_2 {


class CThread

// Класс потокового объекта

int num; || Номер потокового объекта Thread thread; // Ссылка на поток bool run; // Признак выполнения потока bool llfe; // Признак жизни потока public CThread (lnt Num ) {num= Num; llfe= false; run= false;> ~CThread ( ) {llfe= false;> public void Start ( )

{

if (!life)

{

>

>

// Стартовать потоковый объект // Если не жил, то

run= true; // пусть выполняется и life= true; // живёт Н Создать поток thread= n e w T h r e a d ( n e w ThreadStart (ThreadFunc)); thread.Start ( ); | | Запустить поток

public vold Finish ( ) {life= false;} public void Suspend ( ) { if (run)

{

>

>

if (irun)

{

>

>

// Если выполнялся, то

run= false; // пусть не выполняется thread.Suspend ( ); // Приостановить поток

public void Resume ( )

{

// Приостановить потоковый объект

// Возобновить потоковый объект // Если не выполнялся, то

run= true; // пусть выполняется t h r e a d . R e s u m e ( ); // Возобновить поток

private void T h r e a d F u n c ( )

{

int n= 0; while (llfe)

// Выполнить поток

// Номер цикла потока // Пока живём, выполнять:

{ Console.WriteLine ("thread " + num + " n= " + n);

n++;

}

Thread.Sieep (50);

Console.WriteLine ("Thread " + num + " flnished");


class TestThread { static void Mairi ( ) {

>

CThread threadl= new CThread (1); // Создать первый объект CThread thread2= new CThread (2); // Создать второй объект threadl.Start ( ); // Стартовать первый потоковый объект thread2.Start ( ) ; // Стартовать второй потоковый объект Thread.Sleep (100); // Поспать 100 мс Console,WrlteLlne (" Приостановить поток первого объекта"); threadl.Suspend (); // Приостановить первый потоковый объект Thread.Sleep (100); // Поспать 100 мс Console.WriteLine (" Возобновить поток первого объекта"); threadl.Resume (); // Возобновить первый потоковый объект Thread.Sleep (100); // Поспать 100 мс Console.WriteLine (" Завершить потоки объектов"); threadl.Flnlsh (); // Завершить первый потоковый объект thread2.Flnish ( ); // Завершить второй потоковый объект

> ////////////////////

// C + + / C L I #include "stdafx.h" using namespace System; using namespace System::Threading; // Управляемый класс потокового объекта

ref class CThread {

// Номер потокового объекта int num; // Дескриптор на поток Thread ^thread; bool run; // Признак выполнения потока bool life; // Признак жизни потока public: CThread (int Num ) {num= Num; life= false; run= false;> -CThread ( ) <life= false;} vold Start ( ) { if(!life) {

// Стартовать потоковый объект // Если не живёт, то

run= true; // пусть живёт и life= true; // выполняется // Создать поток thread= g c n e w Thread ( g c n e w ThreadStart (this, &CThread::ThreadFunc)); thread ->Start ( ); // Стартовать поток

void Finish ( ) <life= false; thread -> J o i n ( );}


vold Suspend ( )

{

If (run)

{

>

>

if (!run)

{

>

>

int n= 0; while (llfe)

> >;

>

// Возобновить потоковый объект // Если не выполнялся, то

run= true; // выполнять thread - > R e s u m e ( );

void T h r e a d F u n c ( ) {

{

// Если выполняется, то

run= false; // не выполнять t h r e a d - > S u s p e n d ( ); // Приостановить поток

vold Resurne ( )

{

// Приостановить потоковый объект

// Возобновить поток

// Выполнить поток // Номер цикла // Если живём, то выполнять:

n++; Console::Wrlte ("thread {0}", num.ToString ()); Console::WriteUne (" n= {0}", n.ToString ()); Thread::Sleep (50);

Console::Write ('Thread {0>", num.ToString ()); Console::WrlteLine (" finished");

void ma!n ( ) { CThread ^threadl= gcnew CThread ^thread2= gcnew threadl -> Start ( ) ; thread2 -> Start ( ); Thread::Sleep (100); Console::WriteLine (" threadl -> Suspend ( ) ; Thread::Sleep (100); Console::WriteLine (" threadl -> Resume (); Thread::Sleep (100); Console::WrlteUne (" threadl -> Finish ( ) ; thread2 -> Finish ( ) ; > /* Result: thread 1 n= 1

CThread (1); // Создать первый объект CThread (2); // Создать второй объект // Стартовать первый потоковый объект // Стартовать второй потоковый объект // Поспать 100 мс Приостановить поток первого объекта"); // Приостановить первый потоковый объект // Поспать 100 мс Возобновить поток первого объекта"); // Возобновить первый потоковый объект // Поспать 100 мс Завершить потоки объектов"); // Завершить первый потоковый объект // Завершить второй потоковый объект


thread 2 n= 1 thread 1 n= 2 thread 2 n= 2 Приостановить поток первого объекта thread 2 n= 3 thread 2 n= 4 Возобновить поток первого объекта thread 1 n= 3 thread 2 n= 5 thread 1 n= 4 tnread 2 n= 6 Завершить потоки объектов Thread 1 finished Thread 2 finished */

C#. Класс CThread потокового объекта содержит данные: num - номер экземпляра потокового объекта, life - признак жизни потока, run - признак выполнения потока и thread - ссылка на поток. Поток thread является объектом класса Thread библиотеки .NET Framework и создаётся с помощью оператора new в теле функции StartQ класса CThread потокового объекта с обязательным предварительным присваиванием переменной life значения true. Аргументом конструктора Thread является объект потокового делегата ThreadStart, создаваемый также с помощью оператора new. Конструктор делегата содержит в качестве аргумента имя потоковой функции ThreadFunc(). Созданный поток thread запускается посредством функции StartQ класса Thread. Интерфейсные функции Start(), Suspend(), Resume() и Finish() потокового объекта обеспечивают его запуск, приостановку и возобновление функционирования, и завершение функционирования. Объявленная в классе переменная run используется функциями Suspend() и Resume() для переключения потока из приоетановленного состояния в состоянии выполнения, и наоборот. Потоковая функция ThreadFunc(), выполняясь пока значение life равно true, после каждого цикла while выдаёт на консоль строку вида: thread

номер-потокового-объекта

n=

номер-цикла

Столбик таких строк на консоли свидетельствует о выполнении циклов потоковой функции потокового объекта. Главная функция Main() приложения создаёт потоковые объекты threadl и thread2 класса CThread. Конструктор класса CThread только создаёт поток, связанный с этим объектом, и лишь после применения функции Start() класса CThread к объектам threadl и thread2 эти объекты начинают функционировать. Потоки выполняются параллельно, о чём свидетельствует информация, выданная на консоль: строки, выданные потоковыми функциями разных объектов, чередуются. Поспав 100 мс (причиной чего является вызов


функции Sleep()), основной поток, представленный главной функцией Main(), позволяет этим объектам функционировать самостоятельно и выдать результативную информацию на консоль. Затем к объекту threadl применяется функция Suspend(), которая приостанавливает его функционирование. Поскольку функция Sleep() опять задерживает выполнение основного потока, то теперь выполняется только поток объекта thread2. О чём свидетельствуют две выданные подряд строки объекта thread2 и отсутствие строк от объекта threadl. Через 100 мс функция Resume() возобновляет приостановленное выполнение объекта threadl. Теперь параллельно выполняются потоки обоих объектов. А через 100 мс функция Finish(), применённая к объектам, завершает их функционирование. Функция Finish() присваивает переменной life значение false - цикл потоковых функций прекращается. C++/CLI. Программа на языке C++/CLI очень схожа с программой на C#. Прежде всего бросается в глаза присутствие ref в описании класса CThread. Это связано с тем, что класс CThread должен бьггь управляемым, поскольку он использует управляемый класс Thread и управляемый делегат ThreadStart библиотеки .NET Framework. Вместо ссылочной переменной в классе CThread объявлен дескриптор thread на объект управляемого класса Thread. При ссылке на функции Start(), Suspend(), Resume() класса Thread в функциях Start(), Suspend(), Resume() нашего класса CThread использован этот дескриптор со стрелкой. Также отличается получение объекта делегата в аргументе конструктора класса Thread при создании объекта потока. Как видим, создание объектов с потоками не представляет сложности в языках C# и C++/CLI. Использование потокового делегата существенно упрощает создание в объекте множества потоков с разноимёнными потоковыми функциями. Заметим, что применение функций Suspend() и Resume() класса Thread не рекомендуется при разработке многопоточных сложных программ, поскольку может привести к непредвидимым аварийным ситуациям при параллельном выполнении потоков. В случае нашей простой программы использование этих функций допустимо. Как избежать применения функций Suspend() и Resume() будет изложено в разделах книги, посвященных синхронизации параллельного выполнения потоков и в конце книги при рассмотрении разработки многопоточной программы.

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


нем. Язык Java использует специальную предопределённую потоковую функцию с именем runO, которую обязан применить любой поток. Также в языке Java появляются неудобства, связанные с созданием и применением объектов потоков. Если в языке C# объекты потока типа Thread можно создать так, как создаются любые управляемые объекты, то на языке Java возникает ряд ограничений. Они связаны не только с функцией run(), но и с наследованием. Класс может создать поток только в том случае, если он наследует либо класс Thread, либо интерфейс Runnable, содержащий функцию run(). Запуск потока осуществляет функция start(), а для задержки его выполнения применяется функция sleep(). Если в языке C# не запрещают, но предупреждают об опасности применения функции Suspend() приостановки потока и функции Resume() возобновления потока класса Thread, то e Java функции suspendO и resumeO запрещены. Поэтому появляются проблемы, если, действительно, требуется приостановить или возобновить выполнение некоторого потока. Как эта проблема разрешена при разработке многопоточной программы, смотрите в разделах 11 и 9.6.2. А сейчас взгляните и постарайтесь понять разные варианты создания и применения потоков в программах примера9.3.1. Класс имеет много перегруженных конструкторов, среди которых мы выделим public Thread ( ) // вариант 1 publlcThread(Runnable runnable) //вариант2 где runnable - интерфейсная ссылка на объект, наследующий интерфейс Runnable и содержащий потоковую функцию run().

Использование этих конструкторов поясняет пример 9.3.1. Пример 9.3.1. Два параллельно выполняющихся потоковых объекта на языке Java. //////////////////// // J a v a и J # Версия 1 class CThread e x t e n d s T h r e a d

{

// Класс потокового объекта

// Номер потокового объекта private int num; // Признак жизни потока private boolean life; public CThread (int Num ) {num= Num; life= true;> public void Finish ( ) {life= false;>; public v o i d run ( ) { int n= 0; while (life) {

// Выполнить поток // Номер цикла потока // Пока живём, выполнять:

System.out.println ("thread " + num + " n= " + n);

n++;

try


> > >

Thread.sleep (50);

catch (InterruptedExceptlon e)<>

System.out.println ("Thread " + num + " finished");

>

class TestThread

i

public static void main ( ) { CThread threadl= new CThread (1); CThread thread2= new CThread (2); threadl.start ( ); // Стартовать thread2.start ( ); // Стартовать try

{

>

Thread.sleep (100);

// Создать первый объект // Создать второй объект первый потоковый объект второй потоковый объект

// Поспать 100 мс

catch(InterruptedException e){> threadl,Finlsh ( ); thread2.Finish ( );

//////////////////// // Java и J #

// Завершить первый потоковый объект // Завершить второй потоковый объект

Версия 2

class CThread i m p l e m e n t s R u n n a b l e

// Класс потока

{ lnt num; boolean life; public T h r e a d thr;

// Номер потокового объекта // Признак жизни потока

public CThread (lnt Num ) <

>

num= Num; life= true; t h r = n e w T h r e a d (this);

public void Flnlsh ( ) {life= false;>; public v o i d run ( )

{

int n= 0; while (life)

{

// Выполнить поток // Номер цикла потока // Пока живём, выполнять

System.out.println ("thread " + num + " n= " + n);

n++;

try

{


T h r e a d . s l e e p (50); catch(InterruptedExceptlon e){}

> >

>

System.out.println ("Thread " + num + " finished");

class TestThread

{

public static void main ( ) { CThread cThreadl= new CThread (1); // Создать первый объекте потоком CThread cThread2= new CThread (2); // Создать второй объект с потоком cThreadl.thr.start ( ); cThread2.thr.start ( );

// Стартовать первый потоковый объект // Стартовать второй потоковый объект

try

{

>

Thread.sleep (100);

// Поспать 100 мс

catch(InterruptedException e){>

>

>

cThreadl.Finlsh ( ); cThread2,Finish ( );

//////////////////// // J a v a и J #

// Завершить первый потоковый объект // Завершить второй потоковый объект

Версия 3

class CThread i m p l e m e n t s R u n n a b l e

{

int num; boolean life;

// Класс потока

// Номер потокового объекта // Признак жизни потока

public CThread (int Num ) {num= Num; life= true;> public void Finish ( ) {llfe= false;}; public v o i d run ( )

{

int n= 0; while (life)

{

// Выполнить поток // Номер цикла потока // Пока живём, выполнять

System.out.println ("thread " + num + " n= " + n);

n++;

try

{

>

Thread.sleep (50);

catch(InterruptedException e){}

}


System.out.println ("Thread " + num + " finished");

> class TestThread { public static void main ( ) { CThread cThreadl= new CThread (1); // Создать первый объект Thread threadl= new Thread (cThreadl);// Создать первый поток CThread cThread2= new CThread (2); // Создать второй объект Thread thread2= new Thread (cThread2); // Создать второй поток threadl.start ( ); thread2.start ( );

// Стартовать первый поток // Стартовать второй поток

try

{

>

Thread.sleep (100);

// Поспать 100 мс

catch(InterruptedException e){> cThreadl.Finish (); cThread2,Finish ( );

> /*

// Завершить первый потоковый объект // Завершить второй потоковый объект

>

Result: thread 1 thread 2 thread 1 thread 2 Thread 1 Thread 2 */

n= 0 n= 0 n= 1 n= 1 finished finished

Версия 1. Чтобы класс CThread обладал потоком, этот класс наследует потоковый класс Thread и может воспользоваться всеми его открытыми (public) свойствами и функциями. В классе CThread переопределяется наследуемая потоковая функция run() класса Thread. Класс CThread также наследует функцию start() запуска потока. Предопределённая потоковая функция run() циклически выполняется, пока булевская переменная life не примет значение false. Каждый цикл while увеличивает порядковый номер n, выдаёт это значение на консоль и приостанавливается функцией sleep(). Функция main() создаёт в куче объекты threadl и thread2 типа CThread с их потоковыми функциями. Функция start() класса Thread применяется к этим объектам, запуская их потоки. Функция Finish() класса CThread присваивают переменным life значение false, прекратив тем самым выполнение соответствующих функций run() потоковых объектов.


Версия 2. В варианте 2 программы класс CThread наследует интерфейс Runnable, что обязывает его реализовать функцию run(). Поскольку функция run() реализована в этом (this) классе, то, создав в этом классе с помощью оператора new объект thr типа Thread, можно этому объекту thr передать эту функцию run(), воспользовавшись вторым вариантом конструктора класса Thread. Для чего в его конструктор при создании потокового объекта передаётся ссылка на объект (this) класса CThread, содержащий функцию run() и наследующий интерфейс Runnable. Обратите внимание на интересный запуск потоковой функции объекта класса CThread. В функции main() создаются два объекта типа CThread с номерами 1 и 2. Поскольку объявленному в классе CThread потоковому объекту thr типа Thread передана при его создании потоковая функция run(), то для её запуска достаточно применить к этому объекту thr функцию start(). Это и делается в функции main() применительно к объектам cThreadl и cThread2. Версия 3. В варианте 3 класс CThread наследует интерфейс Runnable и реализует только потоковую функцию run(). Вначале создаются два объекта типа CThread со своими потоковыми функциями, а затем - два объекта типа Thread, в которые при их создании передаются ссылки на объекты типа CThread и, конечно, реализованные в них потоковые функции run(). Затем запускаются потоки. Потоковые функции объектов threadl и thread2 выполняются параллельно. Обратите внимание: хотя потоковые функции run() выполняются в потоковых объектах threadl и thread2, завершение их выполнения осуществляют функции Finish(), примененные к объектам cThreadl и cThread2, в которых реализованы функции Finish() и потоковые функции run()Итак, здесь созданы два объекта типа Thread с конструктором второго варианта, в который передаётся объект с потоковой функцией run(), объект, наследующий интерфейс Runnable.

9.4. Потоковый объект, выдающий событие Функционируя в некоторой системе, потоковые объекты обычно взаимодействуют между собой. В примере 9.4.1 на языках C# и Java реализован потоковый объект, информирующий о своём выполнении объекту без потока. Пример 9.4.1. Потоковый объект, выдающий событие. //////////////////// //C# using System; using System.Threading;


namespace C s T E l _ l { delegate vold delEv (string st); class CUseEv {

// Делегат событий

// Класс объекта, реагирующего на событие

public void HandlerEv (string st) // Обработать поступившее { // событие Console.WriteLine ("Объект класса CUseEv получил событие: " + st);

>

>

class CThread {

// Класс потокового объекта

p u b l i c e v e n t d e l E v ev; // Ссылка на событие к объекту класса CUseEv lnt num; // Номер потокового объекта Thread thread; // Ссылка на поток bool life; // Признак жизни public CThread (int Num ) {num= Num; life= false;> ~CThread ( ) {life= false;} public void Start ( )

{

If (lllfe)

{

>

>

// Стартовать потоковый объект // Если потоковый объект не функционирует,

life= true; // то пусть живёт // Создать поток thread= new Thread (new ThreadStart (ThreadFunc)); thread,Start ( ) ; // Стартовать поток

public vold Finish ( ) {life= false; > private void ThreadFunc ( ) // Потоковая функция

{

int n= 0; while (life)

<

> >

>

// Номер цикла выполнения потока // Пока живёт, выполнять

n++; Console.WriteLine ("thread " + num + " n= " + n); if ((ev != n u l l ) & & ( n % 2 == 1)) // Если событие активизировано и нечётный цикл потока, ev ("Привет"); // то послать событие ev Thread.Sleep (50);

Console.WriteLine ("Thread " + num + " finished");

class TestThread

// Главный класс приложения


static void Main ( )

<

>

>

т /

CThread t h r e a d l = new CThread (1); // Создать потоковый объект CUseEv useEv= new CUseEv ( ) ; // Создать объект useEv // Подписать обработчик объекта useEv на событие ev t h r e a d l . e v += n e w delEv (useEv.HandlerEv); threadl.Start ( ); // Запустить потоковый объект Thread.Sleep (200); // и пусть он выполняется 200 мс threadl.Finlsh ( ) ; // Завершить потоковый объект

//////////////////// // J a v a и J # interface ICThread

{

>

// Интерфейс с обработчиком

public void H a n d l e r E v (String st);

// Обработчик события

class CUseEv i m p l e m e n t s I C T h r e a d / / Класс объекта, реагирующего на событие

{ public void HandlerEv (String st) // Обработать поступившее { // событие System.out.println ("Object o f t h e CUseEv type received message: " + st);

>

}

class CThread e x t e n d s T h r e a d { ICThread iCThread; lnt num; boolean life;

// Класс потокового объекта // Интерфейсная ссылка // Номер потокового объекта // Признак жизни

public CThread (int Num, I C T h r e a d IC) {num= Num; life= false; i C T h r e a d = IC;} public void Start ( )

{

if (llife)

{

>

>

life= true; this.start();

// Стартовать потоковый объект // Если потоковый объект не функционирует, // то пусть

живёт

public vold Finish ( ) {life= false; > public void run() { lnt n= 0; while (life)

// Потоковая функция || Номер цикла выполнения потока // Пока живёт, выполнять


n++; System.out.println ("thread " + num + " n= " + n); lf(n%2 == 1) ICThread.HandlerEv ("hello"); // Послать сообщение try {

>

>

Thread.sleep (50);

catch(InterruptedException e){>

System.out.println ("Thread " + num + " finished");

>

}

class TestThread

{

// Главный класс приложения

public static void main ( )

{ CUseEv useEv= new CUseEv ( ); // Создать объект useEv CThread threadl= new CThread (1, useEv); // Создать потоковый // объект threadl.Start ( ); // Запустить потоковый объект try

{

Thread.sleep (200);

}

>

>

catch(InterruptedException e){} threadl.Finish (); // Завершить // потоковый объект threadl.Finish();

////////////////////

// C+ + / C L I #include "stdafx,h" using namespace System; using namespace System::Threading; delegate vo!d delEv (String^); ref class CThread

{ lnt num; Thread ^thread; bool life; public: event delEv ^ev ; // Дескриптор события к объекту класса CUseEv CThread (intNum ) {num= Num; life= false;> ~CThread ( ) <life= false;>


void Start ( ) < if (llife) {

>

>

life= true; thread= gcnew Thread (gcnew ThreadStart (this, &CThread: :ThreadFunc)); thread ->Start ( );

vold Flnlsh ( ) {llfe= false;} vold ThreadFunc ( )

{

int n= 0; while (life) { n++; Console::Write ("thread {0}", num.ToStrlng ( )); Console::WriteLlne (" n= {0}", n.ToString ()); If (n%2 == 1) ev ( " П р и в е т " ) ; // Сгенерировать событие ev Thread::Sleep (50);

}

};

}

Console::Write ("Thread <0}", num.ToString ()); Console::WriteLine (" finished");

ref dass CUseEv

// Класс объекта, получающего событие из потокового // объекта

{ public: v o i d H a n d l e r E v (String A s t ) // Обработать поступившее { // событие Conso!e::WriteLine ("Объект класса CUseEv получил событие: {0}", st);

>;

}

void main ( ) { CThread ^threadl= gcnew CThread (1); CUseEv ^useEv= gcnew CUseEv ( ); t h r e a d l - > e v += g c n e w delEv (useEv,&CUseEv::HandlerEv); threadl -> Start ( ) ; Thread::Sleep (200); threadl -> Finish ( ); } /* Result: thread 1 n= 1


Объект класса CUseEv получил событие: Привет thread 1 n= 2 thread 1 n= 3 Объект класса CUseEv получил событие: Привет thread 1 n= 4 Thread 1 finished */

C#. В пространстве имён CsTEl_l приложения объявлен делегат delEv, устанавливающий формат делегируемой функции - обработчика события. Эта функция имеет возвращаемое значение void и параметр типа string, позволяющий передать с собьггием некоторую строку сообщения. Такой же формат обязательно имеет обработчик HandlerEv() класса CUseEv, которому надлежит реагировать на каждое событие, посылаемое из потоковой функции ThreadFunc() потокового объекта класса CThread. Класс CThread потокового объекта содержит данные: num - номер потокового экземпляра объекта, life - признак жизни потока, thread - ссылка на поток и ev - ссылка на событие. В виду отсутствия в их надобности функции Suspend() и Resume(), а также связанная с ними переменная run, присутствовавшие в предыдущем примере 9.3.1, удалены из класса CThread рассматриваемого примера 9.4.1. Поток thread является объектом класса Thread и создаётся с помощью оператора new в теле функции Start() класса CThread потокового объекта и запускается посредством функции Start() класса Thread (!). Интерфейсные функции Start() и Finish() потокового объекта обеспечивают его запуск и завершение функционирования. Потоковая функция ThreadFunc(), выполняясь пока life равно true, после каждого нечётного цикла генерирует событие ev с аргументом "Привет". Главная функция Main() приложения создаёт потоковый объект threadl класса CThread и объект useEv класса CUseEv. Затем, используя делегат delEv, она создаёт объект делегата и подписывает обработчик HandlerEv() объекта useEv на событие ev потокового объекта threadl, применив оператор "4"ss" Установив событийную связь между объектами, функция Main() стартует потоковый объект threadl, а через 200 мс завершает его выполнение. Выдача на консоль подтверждает, что после каждого нечётного цикла потоковой функции ThreadFunc() потокового объекта threadl объект useEv получает сообщение "Привет". C++/CLI. В отличие от языка C# в программе на C++/CLI класс CThread помечается как управляемый, появились дескрипторы, оператор gcnew. Java. В программе на языке Java примера 9.4.1 свяжем объект-источник сообщения с объектом-приемником этого сообщения посредством интерфейсной ссылки. Интерфейс ICThread включает функцию HandlerEv() объекта-


приёмника сообщения, которая должна быть делегирована объектуисточнику для выполнения. Объект-источник сообщения класса CThread, наследующего потоковый класс Thread, содержит предопределённую потоковую функцию run(). Циклическая потоковая функция run() при выполнении нечётных циклов выполняет делегированную функцию HandlerEv() объекта без потока, воспользовавшись интерфейсной ссьшкой iCThread. Объект любого класса, наследовавшего интерфейс ICThrcad, можно передать (с помощью ссылку на этот объект) в конструктор объекга-источника и тем самым обеспечить выполнение его функции HandlerEv() при нечётных циклах потока объекта-источника сообщения. Представляет интерес иная реализации связи объекта с потоком с объектом без потока, реализованная в примере 9.4.2. Здесь применён механизм уведомления, о котором говорилось в разделе 8.4. Пример 9.4.2. Потоковый объект, уведомляющий обозреватель. //////////////////// // Java и J # // Потоковый объект с наблюдаемым объектом уведомляют обозреватель importJava,util.*; class CUseEv i m p l e m e n t s O b s e r v e r // Класс обозревателя

{

public void u p d a t e (Observable obj, Object arg)

{

>

>

System.out.println("Notify: 1= + ( ( I n t e g e r ) a r g ) . i n t V a l u e ( ));

class Notify e x t e n d s O b s e r v a b l e {

// Класс наблюдаемого объекта

public void s e n d (int i)

{

}

>

s e t C h a n g e d ( ); n o t i f y O b s e r v e r s ( n e w I n t e g e r (i));

class CThread extends Thread { Notify notify; int num; boolean life; public CThread (int Num) { num= Num; llfe= false;

// Класс потокового объекта // Ссылка на наблюдаемый объект // Номер потокового объекта // Признак жизни


notify= n e w Notify();

// Создать наблюдаемый объект

public void Start ( )

{

if (!life)

{

>

>

// Стартовать потоковый объект // Если потоковый объект не функционирует,

life= true; thls.start();

// то пусть живёт

public void Finish ( ) {llfe= false; > public void run()

{

// Потоковая функция

int n= 0; while (life)

{

// Номер цикла выполнения потока // Пока живёт, выполнять

n++;

notify.send(n); try {

>

> >

>

Thread.sleep (50);

catch(InterruptedException e){>

System.out.println ("Thread " + num + " finished");

class TestThread

{

// Инициировать уведомление

// Главный класс приложения

public static vold main ( ) { CUseEv useEv= new CUseEv ( ) ; // Создать объект useEv CThread t h r e a d l - new CThread (1); // Создать потоковый объект threadl.notify-addObserver(useEv);// Подписать обозреватель threadl,Start ( ) ; // Запустить потоковый объект try {

>

> > /* Result: Notify: 1= 1 Notify: i= 2 Notify: 1= 3 Notify: i= 4

Thread.sleep (200);

catch(InterruptedException e){}threadl.Finish (); // Завершить // потоковый объект threadl.Finish ();


Thread 1 finished */

9.5. Поток получил событие из объекта Пример 9.5.1 иллюстрирует в отличие от примера 9.4.1 противоположный случай, когда не потоковый объект посылает событие, а потоковому объекту посылается событие из обычного объекта.

Пример 9.S.1. Поток получил событие из объекта. //////////////////// //C# using System; using System.Threading; namespace CsTEl_2 { delegate v o i d delEv (string st);

// Тип делегата событий

class CGenEv // Класс объекта, посылающего событие потоковому объекту { public e v e n t delEv ev; // Событие к потоковому объекту public v o i d GenerateEv (string str ) // Сгенерировать событие <

>

>

if (ev 1= null) ev (str);

class CThread

{

// Послать событие ev

// Класс потокового объекта

bool received; string st; int num; Thread thread;

// Признак получения события

bool life; public CThread (int Num ) {num= Num; life= false;> ~CThread ( ) {iife= false; received= false;} public void Start ( ) { if (!life) { life= true; thread= new Thread (new ThreadStart (ThreadFunc)); thread.Start ( ) ;

>

}

public void Finish ( ) {life= false;}


p u b l i c v o i d H a n d l e r E v (string {

>

r e c e i v e d = true; st= str;

str)

| | Обработать поступившее // событие // Событие получено

private vold ThreadFunc ( ) { int n= 0; while Hife^ <" " " " ' ^" n+ + ; Console.WriteLine ("thread " + num + " n= " + n); if ( r e c e i v e d ) { r e c e i v e d = false; Console.WriteLine ("Поток получил событие: " +

}

>

>

>

Thread.Sleep (50);

Console.WriteLine ("Thread " + num + " finished");

class TestThread

{

> I*

}

static void Maln ( ) {

>

CThread threadl= new CThread (1); CGenEv genEv= new CGenEv ( ) ; // Подписать обработчик HandlerEv на событие ev g e n E v . e v += n e w d e l E v ( t h r e a d l . H a n d l e r E v ) ; threadl.Start ( ) ; Thread,Sleep (100); genEv.GenerateEv ("Здравствуй"); Thread.Sleep (100); threadl.Finish ( ) ;

Result: thread 1 n= 1 thread 1 n= 2 thread 1 n= 3 Поток получил событие: Здравствуй thread 1 n= 4 Thread 1 finished */ //////////////////// // J a v a и J #


interface I C T h r e a d {

>

public v o i d H a n d l e r E v (String str);

class CGenEv // Класс объекта, посылающего событие потоковому объекту

{

ICThread ICThread;

// Интерфейсная ссылка

public CGenEv (ICThread IC) / iCThread= IC;

// Установить ссылку на потоковый объект

>

public vold GenerateEv (String str) {

>

>

iCThread.HandlerEv(str);

// "Сгенерировать событие" // Послать сообщение

class CThread extends Thread implements ICThread { boolean received; String st; int num; boolean life; public CThread (int Num ) <num= Num; life= false; received= false;> public vold Start ( ) { if (!life) {

>

>

life= true; this.start();

public vold Finish ( ) {life= false;} public void HandlerEv (String str) { r e c e i v e d c true; st= str;

// Обработать поступившее // событие

>

public void run ( )

{

int n= 0; while (llfe)

{

n++; System.out,println ("thread " + num + " n= " + n); if (received) {


>

received= false; System.out.println ("The thread received message: " + st);

try {

>

>

>

>

Thread.sleep (50);

catch(InterruptedException

e){}

bystem.out.println ("inread " + num +

::

nnisnea");

class TestThread

{

public static void main ( ) { CThread threadl= new CThread (1); CGenEv genEv= new CGenEv (threadl ); try {

>

threadl.Start ( ) ; Thread.sleep (100);

catch(InterruptedException e){} genEv.GenerateEv ("hello"); try {

>

>

Thread.sleep (100);

catch(InterruptedException e){> threadl.Finish ( );

> /* Result: thread 1 n= 1 thread 1 n= 2 thread 1 n= 3 The thread received message: hello thread 1 n= 4 Thread 1 finished */

////////////////////

// C + + / C L I #include "stdafx.h" using namespace System; using namespace System::Threading; delegate vofd delEv (String^}; ref class CGenEv // Класс объекта, посылающего событие в потоковый объект


{

public: e v e n t de!Ev ^ e v ; v o i d G e n e r a t e E v (String ^str ) // Сгенерировать событие <

>;

ev (str);

>

// Послать событие ev

refclass CThread // Потоковый класс, объект которого получает событие

<

int num; Thread ^thread; bool life; bool received; String ^st; public: CThread (int Num ) {num= Num; life= false; received= false;> ~CThread ( ) {llfe= false;} void Start ( ) { If (llife) {

>

>

life= true; thread= gcnew Thread (gcnew ThreadStart (thls, &CThread::ThreadFunc)); thread -> Start ( );

void Finish ( ) {life= false;} v o i d H a n d l e r E v (String ^str) // Обработать поступившее < // событие r e c e i v e d = true; st= str;

>

void ThreadFunc ( ) { int n= 0; while (life) { n++; Console::Write ("thread {0}", num.ToString ( )); Console::WriteLine (" n= {0}", n.ToString ()); if (received) {

>

received= false; Console::WrlteLine ("Поток получил событие: {0}", st);

Thread::Sleep (50);


>;

>

Console::Write (Thread {0>", num.ToString ()); Console::WriteLine (" finished");

void main ( ) { CThread ^threadl= gcnew CThread (1); CGenEv ^genEv= gcnew CGenEv ( ); t h r e a d l -> Start ( ); genEv -> ev += g c n e w delEv ( t h r e a d l , &CThread::HandlerEv); Thread::Sieep (100); genEv -> GenerateEv ("Здравствуй"); Thread::Sleep (100); threadl -> Finish (); > /* Result: thread 1 n= 1 thread 1 n= 2 thread 1 n= 3 Поток получил событие: Здравствуй thread 1 n= 4 Thread 1 finished */

Класс CGenEv обычного объекта объявляет ссылку (или дескриптор) ev на событие, использующее делегат delEv. Класс CGenEv содержит функцию GenerateEv(), генерирующую событие ev. Эту функцию применяет функция main() к объекту genEv класса CGenEv через 100 мс после его старта. Причём функция GenerateEv() имеет аргумент "Здравствуй". Класс CThread потокового объекта включает обработчик HandlerEv() события ev, который подписывается на событие ev объекта genEv с помощью делегата delEv или интерфейсной ссылки после создания этого объекта, но перед его запуском. Потоковый объект использует булевскую переменную received для фиксации потоком получения события. При этом поток выдаёт на консоль строку, подтверждающую получение сообщения. В примере 9.5.2 рассмотрена иная реализация воздействия из объекта на объект с потоком. Здесь объект с потоком представлен как обозреватель, то есть объект класса, наследующего интерфейс Observer и содержащий обработчик update() уведомлений от наблюдаемого объекта. Такая реализация возможна только на языках Java и J# , библиотека которых включает требуемый класс Observable и интерфейс Observer.


Пример 9.5.2. Поток получил уведомление из наблюдаемого объекта. //////////////////// // J a v a и J # importjava.utll.*; class CGenEv e x t e n d s O b s e r v a b l e

{

>

public void GenerateEv (String str ) {

>

// Класс наблюдаемого объекта // Осуществить уведомление

s e t C h a n g e d ( ); notifyObservers ( n e w String (str));

class CThread extends T h r e a d i m p l e m e n t s O b s e r v e r // Класс обозревателя { boolean received; String st; lnt num; boolean life; public CThread (int Num ) {num= Num; life= false; received= false;> public void Start ( ) { If (lllfe) {

>

>

life= true; this.start ( ) ;

public void Finish ( ) <iife= false;} public v o i d u p d a t e ( O b s e r v a b l e obj, O b j e c t arg) {

>

received= true; st= ((String)arg).toString ( );

public void run ( ) { int n= 0; while (life) { n+ + ; System.out.println ("thread " + num + " n= " + n); if (received) { received= false; System.out.println ("The thread received message: " +

st);

}


try

{

>

>

>

>

Thread.sleep (50);

catch(InterruptedException e) { >

System.out.println ("Thread " + num + " finished");

class i estTnreaa { public static void main ( ) { CThread thread= new CThread (1); CGenEv genEv= new CGenEv ( ); genEv.addObserver (thread); try

{ }

thread.Start ( ); Thread.sleep (100);

catch (lnterruptedException e) { > genEv.GenerateEv ("hello"); try {

>

>

Thread.sleep (100);

catch (InterruptedException e) { > thread.Finish ( ) ;

> /* Result: thread 1 n= 1 thread 1 n= 2 thread 1 n= 3 The thread received message: hello thread 1 n= 4 Thread 1 finished */

Уведомления языка Java сродни с событиями языка C#. Об этом подробно было рассказано в разделе 8.4. Класс CGenEv источника события, являясь классом наблюдаемого объекта, обязан наследовать класс Observable и реализовать две функции: setChanged() и notifyObservers(). Аргументом функции notifyObservers() является объект типа String, инкапсулирующий передаваемую обозревателю строку str. Обозреватель - объект класса CThread, содержит поток и должен получать уведомления. Поэтому класс CThread должен наследовать интерфейс


Observer и класс Thread. Интерфейс Observer обязывает класс реализовать обработчик update() уведомлений. В функции main() функция addObserver() подписывает обозреватель thread на уведомления из наблюдаемого объекта genEv.

9.6. Синхронизация выполнения потоков П л « л п ч л п г i m 1 lttp<<UUIWtDnV

n r T n n n i T n a t i T i a DOlllVJtniAVinoiV

п л т л о т ! I l U 1V/AM

« ( п г ч ш inVi J L

11ЛГ1Лrv> олпO*T*T riVU4AUPJVDUlD

n ^ ^ n e n n o i > t t a pttJ^Wl4wiViOiV

ресурсы. Например, несколько потоков используют некоторую таблицу, заполняя её данными и извлекая из неё информацию. При неконтролируемом одновременном обращении к таблице каждый из этих потоков может извлечь часть уже изменённых каким-либо другим потоком, так и часть ещё неизменённых старых данных, которые совместно не отражают её надлежащее новое состояние. Поэтому здесь требуется синхронизация выполнения параллельных потоков - пока некоторый поток модифицирует таблицу, выполнение других потоков должно быть приостановлено и возобновлено после окончания модификации. Затем им может быть предоставлен параллельный не синхронизированный доступ к таблице, если потоки только извлекают информацию. Среди средств синхронизации параллельного выполнения потоков, предоставленных .NET Framework, мы рассмотрим класс Monitor, а в языке Java - специальные функции wait(), notify() и notifyAH(). Но в начале познакомимся с особыми операторами lock языка C# и synchronized языка Java, предназначенными для синхронизированного выполнения указанного оператора программы, обычно представленного в виде составного оператора.

9.6.1. Операторы lock и synchronized Операторы synchronized языка Java и lock языка C# позволяют представить любой оператор программы в виде критической секции, которая связывается с некоторым объектом и может выполняться только одним потоком среди потоков, имеющих доступ к этому объекту. Выполнение других потоков, пытающихся воспользоваться этим объектом, приостанавливается, пока указанный оператор не выполнится, освободив тем самым этот объект от своего монопольного использования. То есть операторы lock (lock - блокировать) и synchronized (synchronized - синхронизирован) захватывают (блокируют) объект, на который они ссылается, на время выполнения указанного оператора, и освобождает его, как только оператор выполнится. Операторы synchronized языка Java и lock языка C# имеют следующую грамматику:


s y n c h r o n i z e d (object) оператор // Java lock (object) оператор // C#

где object - ссылка на объект, который нужно синхронизировать, оператор - оператор, реализующий критическую секцию. Ссылаемый объект (разделяемый ресурс) используется одновременно несколькими потоками. Если этот объект используется некоторым потоком, то он блокируется от использования другими потоками. Часто в качестве ссылки на объект применяется ключевое слово this, если предполагается употребить критическую секцию применительно к объекту, к которому она принадлежит. Как известно, обращение к статическим данным и функциям осуществляется через класс, в котором они размещены. Поскольку обычно к блокируемому объекту применяется не один, а последовательность из нескольких операторов, то в качестве оператора, следующем за ключевыми словами synchronized и lock, используют составной оператор, включающий требуемую последовательность операторов в фигурных скобках. Эта последовательность операторов представляет собой критическую секщно, которая защищена операторами synchronized и lock и является единственным владельцем процессора на время своего выполнения с объектом. Пример 9.6.1.1. Оператор lock и потоки на языке C#. /////////////// //c# using System; using System.Threading; class Obj

{

// Класс

разделяемого объекта

private int count= 0; public int Count

{

>

>

class User

{

// Счётчик // Свойство Count

set {count= value;} get {return count;>

// Класс объектов с потоками

private Obj obj; int num; Thread thr;

// Ссылка на разделяемый объект // Номер объекта // Ссылка на поток

// Конструктор public User (Obj obj, int num) { this.obj= obj; thls.num= num; // Создать и стартовать поток


thr= new Thread(new ThreadStart(this.ThrFunc)); thr.Start();

// Потоковая функция private void ThrFunc ( )

{

lock(obJ) // 1 { while (obJ.Count < 5) { //lock(obj)

{

}

// Блокировать разделяемый объект//2

obj.Count= obj.Count +1; // Увеличить счётчик Thread.Sleep (1); // Поспать Console.WrlteLine("User" + num + " count= " + obj.Count);

>

>

>

>

class LockTest

{

Console.WriteLine("Thread of User" + num + " flnished");

// Касс с основным потоком

static vold Maln ( ) {

> /*

>

Obj obj= new Obj ( ) ; User u s l = new User (obj, 1); User us2= new User (obj, 2); Console.WriteLine ("OKl"); Thread.Sleep (100); Console.WriteLine ("OK2");

// // // //

Создать Создать Создать Создали

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

// Завершился основной поток

Result: Вариант А. О п е р а т о р lock отсутствует (Строки 1 и 2 закомментированы).

OKl

Userl count= 1 User2 count= 1 Userl count= 3 User2 count= 3 Userl count= 5 Thread of Userl finished User2 count= 5 Thread of User2 finished

OK2

Result: В а р и а н т Б. О п е р а т о р lock в н у т р и ц и к л а w h i l e о х в а т ы в а е т б л о к , в к л ю ч а ю щ и й у в е л и ч е н и е с ч е т ч и к а , ф у н к ц и и Sleep и W r i t e L i n e (строка 1 закомментирована, а строка 2 не закомментирована).

OKl


Userl count= 1 User2 count= 2 U s e r l count= 3 User2 count= 4 U s e r l count= 5 Thread of U s e r l finished User2 count= 6 Thread of User2 finished

OK2

Result: В а р и а н т В. О п е р а т о р lock о х в а т ы в а е т о п е р а т о р w h i l e (строка 1 не закомментирована, а строка 2 закомментирована).

OKl

U s e r l count= 1 U s e r l count= 2 U s e r l count= 3 U s e r l count= 4 U s e r l count= 5 Thread of U s e r l finished Thread of User2 finished

OK2 */

В примере 9.6.1.1 класс Obj описываеттип объекта, доступ к которому осуществляется несколькими потоками, которые изменяют значение счётчика count и считывают это значение. Доступ к счётчику осуществляется посредством свойства Count. Класс User описывает тип объектов, каждый из которых содержит поток, осуществляющий увеличение счётчика разделяемого объекта класса Obj на 1 и выдачу полученного значения на консоль. Потоковая функция перед выдачей на консоль задерживается на 1 мс, подразумевая, что поток мог бы выполнить и другие какие либо операции с разделяемым объектом. В течение выполнения потоковой функции другие потоки могут изменить значение счётчика, если доступ к используемому разделяемому объекту не синхронизировать - это будет показано ниже. Выполнение потоковой функции состоит в пятикратном обращении к разделяемому объекту obj класса Obj. При каждом обращении разделяемый объект блокируется оператором lock с составным оператором (критической секцией), осуществляющим увеличение счётчика, задержку и выдачу на консоль. Функция Main() класса LockTest создаёт объект obj класса Obj и объекты usl и us2 класса User, потоки которых используют объект obj. При создании объектов us 1 и us2 их конструкторы устанавливают номера этим объектам и фиксируют ссылки на разделяемый объект obj. Затем создаются и запускаются их потоки. Рассмотрим несколько вариантов выполнения этой программы. Вариант А. Созданы два объекта usl и us2 с потоками. Синхронизация параллельного выполнения потоков отсутствует, оператор lock не используется. Обращение к разделяемому объекту obj класса Obj не управляется, поэтому результат изменения счётчика не предсказуем. Он может быть как в


примере 9.6.1.1, но может и отличаться в зависимости от производительности процессора. Вариант Б. Созданы два объекта usl и us2 с потоками и их потоки запущены. Оператор lock охватывает нутро цикла while, позволяя только одному потоку менять значение счётчика count разделяемого объекта obj. Результат работы программы показывает, что, действительно, объект obj разделяется между потоками. При изменении значения счетчика count некоторым потоком, этот поток захватывает объект obj, не позволяя другому потоку также измеПОТ1 ?uaupuTjp pujirouva pnnnt lA'1 nMuntTwrg^uvu.ut mfifm.,u^winT,nrnci***jr4 лиггцл i итл . u . . u w . . w .W11I.W V . w . Ц . V W U . . . > . ^J j ^ ~ ' J " " * " . " ^wuuiAM U ( ^ i l v , .W значение счетчика count последовательно меняется потоком каждого объекта класса User, поскольку после обращения к разделяемому объекту с использованием оператора lock поток выходит из оператора lock, позволив другому потоку на один цикл его потоковой функции захватить разделяемый объект. Итак, не мешая друг другу, потоки раздельно используют разделяемый объект obj, увеличивая последовательно значение его счётчика на 1. Благодаря оператору lock поток каждого объекта захватывает разделяемый объект, исключив доступ к нему со стороны другого потока Вариант В. Созданы два объекта usl и us2 с потоками и их потоки запущены. Оператор lock теперь охватывает оператор while, позволяя только одному потоку изменять значение счётчика count разделяемого объекта obj до завершения циклов while. Результат работы программы показывает, что, действительно, объект obj захватывается одним потоком. По результату работы варианта В возникает вопрос: почему же после завершения потока объекта usl не появились строки, указывающие о выполнении циклов извлечённого из очереди ожидающих потоков потока объекта us2? Почему появилась строка 'Thread of User2 finished"? Дело в том, что завершившийся поток объекта usl увеличил значение счетчика до 5, поэтому цикл while потока объекта us2 не выполнялся, и посему сразу появилось сообщение об его завершении. Стоит вставить в конце потоковой функции оператор obj.setCount (0); и ситуация изменится, поток объекта us2 оживёт. и и

J

1

Использование оператора synchronized языка Java, аналогичное применению lock в языке C#, приведено в примере 9.6.1.2.

Пример 9.6.1.2. Оператор synchronized и потоки на языке Java. /////////////// // J a v a и J #

class Obj

{

private lnt count= 0;

// Класс разделяемого объекта // Счётчик

// Свойство Count public void setCount(lnt Count) {count= Count;>


public int getCount ( ) <roturn count;>

} class User extends Thread

{

// Класс объектов с потоками

private Obj obj; int num;

// Ссылка на разделяемый объект // Номер объекта

// Конструктор public User (Obj obj, int num)

{

>

thls.obj= obj; this.num= num;

// Потоковая функция public void run ( )

{

/ / s y n c h r o n i z e d (obj)

// 2

{

while (obj.getCount ( ) < 5) { s y n c h r o n i z e d (obj) // 1

{

obj.setCount (obj.getCount( )+l); // Увеличить // счётчик try {

>

Thread.sleep (1);

// Поспать

catch(InterruptedException e) < }

>

System.out.println ("User " + num + " count= " + obj.getCount( )); >

System.out.println ("Thread of User " + num + " finished"); //obj.setCount (0);

>

}

>

class SynchronizedTest

{

// Касс с основным потоком

public static void maln() { Obj obj= new Obj ( ); // Создать разделяемый объект User u s l = new User (obj, 1); // Создать первый объект с потоком User us2= new User (obj, 2); // Создать второй объект с потоком usl.start ( ) ; us2.start ( ) ; System.out,println ("0K1"); // Создали объекты try

{


Thread.sleep (500);

>

catch(InterruptedException e) { > System.out.println ("OK2"); //Завершилсяосновнойпоток

> /* Result: В а р и а н т А. О п е р а т о р s y n c h r o n i z e d отсутствует (строки 1 и 2 з а к о м м е н т и р о в а н ы ) .

OKl

Userl count= 1 User2 count= 1 Userl count= 3 User2 count= 3 Userl count= 5 Thread of Userl finished User2 count= 5 Thread of User2 finished

OK2

Result: В а р и а н т Б. О п е р а т о р s y n c h r o n i z e d внутри ц и к л а w h i l e о х в а т ы вает блок, в к л ю ч а ю щ и й у в е л и ч е н и е счетчика, ф у н к ц и и Sleep() и WriteLine(). (строка 1 р а с к о м м е н т и р о в а н а и строка 2 з а к о м м е н т и р о в а н а ) .

OKl

Userl count= 1 User2 count= 2 Userl count= 3 User2 count= 4 Userl count= 5 Thread of Userl finished User2 count= 6 Thread of User2 finished

OK2

Result: В а р и а н т В. О п е р а т о р s y n c h r o n i z e d о х в а т ы в а е т о п е р а т о р w h i l e . (строка 1 з а к о м м е н т и р о в а н а и с т р о к а 2 р а с к о м м е н т и р о в а н а ) .

OKl

Userl count= 1 Userl count= 2 Userl count= 3 Userl count= 4 Userl count= 5 Thread of Userl finished Thread of User2 finished

OK2 */

Как подтверждают примеры 9.6.1.1 и 9.6.1.2, отсутствие синхронизации параллельного выполнения потоков приводит к неуправляемому употреблению разделяемого ими ресурса, поскольку во время применения разделяемого объекта одним потоком этот объект также используется и другим потоком. Применение оператора lock в языке C# и оператора synchronized в языке Java блокируют разделяемый ресурс на время его использования каждым потоком.


Рассмотренные в данном разделе операторы lock и synchronized применимы только к одному оператору программы и не позволяют управлять гибким доступом к разделяемому объекту, что не разрешает ожидающим потокам временно изменить свою работу, пока требуемый объект заблокирован. Для синхронизации параллельного выполнения потоков применяются классы Mutex и Monitor в языке C# и функции wait(), notify() и notifyAll() в языке Java, предоставляющие большие возможности. О применении монитора Monitor и функций wait(), notify() и notifyAII() рассказано в следующем разделе. 9.6.2. Связи м е ж д у потоками Если задачи, выполняемые параллельными потоками, независимы, то каждый из потоков самостоятельно выполняет свою работу, получает надлежащий результат и завершается, сообщив, при необходимости, об окончании работы с помощью события или уведомления. Но, как правило, параллельные потоки осуществляют совместную работу, что требует обмен между ними промежуточными данными в процессе их выполнения. То есть в ряде точек выполнение некоторого потока должно приостанавливаться (блокироваться), и он должен ждать, когда другой поток получит результат, требуемый для данного потока, и возобновит его выполнение (разблокирует). Теперь оба потока выполняются опять параллельно, при этом первый поток берет ожидаемый результат. Так может происходить обмен данными между многими потоками в требуемых точках их выполнения. Для обеспечения связи между потоками язык Java использует специальные функции wait(), notify(), notifyAll(), а язык C# - специальные классы Monitor и Mutex.

9.6.2.1. Связи меяеду потоками в Java Функции wait(), notify() и notifyAll() позволяют не только синхронизировать параллельное выполнение потоков, но и организовать приостановку и выполнение одного потока без применения запрещённых функций suspend() и resume(). Функции wait(), notify() и notifyAll() являются членами класса Object, который наследуется любым классом. То есть эти функции доступны в любом объекте. В классе они определены как: final vold wait() throws InterruptedException; final vold notify(); final void notifyAII();


Эти функции должны вызываться в синхронизированном блоке с ключевым словом synchronized. А функция wait() ещё должна быть заключена в блок try-catch, поскольку возможен выброс исключения InterruptedException. Функция wait() приостанавливает (блокирует) выполнение потока объекта, в котором она вызывается, до тех пор, пока этот поток не будет разблокирован вызовом функции notify() или notifyAll(). Вызов функции notify() возобновляет (разблокирует) в объекте выполнение первого потока из потоков, приостановленных ранее функцией wait() на этом объекте. Вызов функции notifyAll() возобновляет (разблокирует) в объекте выполнение всех потоков, приостановленных ранее функцией wait() на том же объекте. Пример 9.6.2.1.1 показывает, что функции wait() и notify() позволяют приостановить и возобновить выполнение потока без применения запрещённых функций suspend() и resume() класса Thread. Пример 9.6.2.1.1. Приостановка и возобновление потока в Java /////////////// // J a v a и J # // Класс объектов с потоками

class User e x t e n d s T h r e a d

{

lnt num; boolean waiting = false; int n=0;

// Признак ожидания // Порядковый номер цикла

public User (lnt Num) {num= Num;> public void suspendUser ( )

{

>

waiting= true;

s y n c h r o n i z e d public void resumeUser ( ) {

>

// Приостановить поток

// Возобновить поток

waiting= false; notify();

// Потоковая функция public void run ( ) { while (n < 5) { try

{

Thread.sleep (10); // Поспать s y n c h r o n i z e d (this) {


if(waiting) w a i t ( ) ; // Подождать

>

System.out.println ("num= "+ num+ » n= " + n++);

catch(InterruptedException e) { >

>

System.out.println ("Thread num=" + num+ " finished");

> class SuspendResume

{

// Класс с основным потоком

public static void main ( )

{ User u s l = new User (1); User us2= new User (2);

// Создать первый объект с потоком // Создать второй объект с потоком

usl.start ( ) ; // Стартовать первый поток us2.start ( ) ; // Стартовать второй поток System.out.println ("Потоки стартовали");

try

{

>

Thread.sleep (30);

catch (InterruptedException e) { > usl.suspendUser ( ) ; // Приостановить первый поток System.out.println ("Первый поток приостановлен");

try

{

>

Thread.sleep (40);

catch (InterruptedExceptlon e) { > usl.resumeUser ( ); // Возобновить первый поток System.out.println ("Первый поток возобновлён");

try

{

>

>

>

Thread.sleep (70);

catch (InterruptedExceptlon e) { >

/*

Result: Потоки стартовали n u m = l n= 0 num=2 n= 0 n u m = l n= 1 num=2 n= 1 Первый поток приостановлен num=2 n= 2 num=2 n= 3


Первый поток возобновлён num=l n= 2 num=2 n= 4 Thread num=2 finished num=l n= 3 num=l n= 4 Thread n u m = l finished */

Класс User наследует потоковый класс Thread и включает потоковую функцию run(). Функция run() выполняет 5 циклов с выдачей номера объекта и порядкового номера цикла n на консоль. Выполнение потоковой функции может быть приостановлено (блокирован поток) при истинном значении переменной waiting. Такое значение присваивает переменной waiting функция suspendUser(). Вызов функции resumeUser() присваивает переменной waiting значение false и выполняет функцию notify(), которая снимает блокировку потока. Поток возобновляет своё выполнение. Это подтверждает результат работы программы примера 9.6.2.1.1, создавшей и запустившей два потока, один из которых приостанавливается и возобновляется.

9.6.2.2. Связи между потоками в C# Монитор (monitor) реализует механизм, обеспечивающий синхронизированный доступ к разделяемому объекту из критической секции, представленной фрагментом программы. При выполнении критической секции потоком доступ к разделяемому объекту других потоков блокируется. Класс Monitor содержит статические функции Enter(), TryEnter(), Wait(), PuIs(), PulsAll() и Exit(). Поскольку эти функции статические, при применении этого класса нет необходимости в создании объектов монитора. Достаточно зафиксировать начало и конец критической секции, используя функции Enter() и Exit() с указанием ссылки на разделяемый объект. Функции TryEnter(), Wait(), Puls(), PulsAll() управляют синхронизацией. Функция TryEnter() фиксирует начало критической секции только в том случае, если разделяемый объект не заблокирован, то есть не используется другим потоком. Функция Wait() ожидает, когда разделяемый объект будет разблокирован иным потоком; после разблокировки критическая секция возобновит выполнение. Функции Puls() и PulsAll() посылают сигнал одному или всем ждущим потокам, информируя об изменении состояния блокированного разделяемого объекта. Ожидаемые потоки выстраиваются в очередь готовых к выполнению потоков. После разблокировки они синхронно разделят доступ к объекту между собой. Пример 9.6.2.2.1 показывает использование монитора вместо оператора lock программы примера 9.6.1.1.


Пример 9.6.2.2.1. Monitor и потоки. /////////////// //C# uslng System; using System.Threading;

class Obj

{

private lnt count= 0; public int Count

< >

// Класс разделяемого объекта

>

class User {

set {count= value;} get {return count;>

// Класс объектов с потоками

private Obj obj; int num; Thread thr; public User (Obj obj, int num) {

>

this.obj= obj; this.num= num; thr= new Thread (new ThreadStart (this.ThrFunc)); thr.Start ( ) ;

private void ThrFunc ( ) { //Monitor.Enter (obj); while (obj.Count < 5) {

// 3 Начать критическую секцию

Monitor.Enter (obj); // 2 Начать критическую секцию obj.Count= obj.Count +1; Thread.Sleep (1); Console.WriteLine("User " + num + " count= " + obj.Count); Monitor.Exit (obj); // 2 Завершить критическую секцию

}

> >

Console.WriteLine (Thread of User " + num + " flnlshed"); // Monitor.Exit (obj); // 3 Завершить критическую секцию

public void Finish ( ) {thr.Join ();>

class LockTest { static void Main ( ) {


>

Obj obj= new Obj ( ); User u s l = new User (obj, 1); User us2= new User (obj, 2); Console.WriteLine ("OKl"); usl.Finish ( ); us2.Finish ( ); Console.WriteLine("OK2");

> /* R e s u l t l : M o n i t o r не п р и м е н ё н (строки 2 и 3 закомментированы)

OKl

Userl count= 1 User2 count= 1 Userl count= 3 User2 count= 3 Userl count= 5 Thread of Userl finished User2 count= 5 Thread of User2 flnished

OK2

Result2: Monitor.Enter (obj);w Monitor.Exit (obj); охватывает оператор while внутри (строки 2 не закомментированы, а строки 3 закомментированы)

OKl

Userl count= 1 User2 count= 2 Userl count= 3 User2 count= 4 Userl count= 5 Thread of Userl finished User2 count= 6 Thread of User2 finished

OK2

Result3: Monitor.Enter (obj);n Monitor.Exit (obj); о х в а т ы в а е т оператор w h i l e с н а р у ж и (строки 2 закомментированы, а строки 3 не закомментированы)

OKl

Userl count= 1 Userl count= 2 Userl count= 3 Userl count= 4 Userl count= 5 Thread of Userl finished Thread of User2 finished

OK2 */ В примере 9,6.2.2.1, как и в примере 9.6.1.1, класс Obj описывает тип разделяемого объекта. Класс User описывает тип объектов, каждый из которых включает поток, потоковая функция которого содержит критическую секцию. Критическую секцию начинает функция Enter() и завершает функция Exit(). Эти функции привязывают критическую секцию к разделяемому объ-


екту, ссылка obj на который передаётся конструктором при создании объекта с потоком. При каждом выполнении критической секции разделяемый объект блокируется монитором, и критическая секция увеличивает значение счётчика разделяемого объекта, осуществляет задержку и выдачу на консоль. При этом разделяемый объект не доступен другим потокам (и, разумеется, содержащимся в них критическим секциям). Пример 9.6.2.2.2 показывает, что функции Wait() и Pulse() позволяют, как и функции wait() и notify() языка Java (см. пример 9.6.2.1.1), приостановить и возобновить выполнение нотока без применений запрещённых функций Suspend() и Resume() класса Thread. П р и м е р 9.6.2.2.2. П р и о с т а н о в к а и возобновление п о т о к а в C#

/////////////// //C#

using System; using System.Threading; class User

{

// Класс объектов с потоками

int n; Thread thr; bool moving;

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

public User ( ) { thls.n= 0; moving= true; // Создать и стартовать поток thr= newThread (newThreadStart(this.ThrFunc)); thr.Start ();

} public vold SuspendUser ( )

{

>

moving= false;

public void ResumeUser ( ) { lock (this) {

>

// Приостановить поток

>

// Возобновить поток // Создать синхронизируемый блок

m o v i n g = true; M o n i t o r . P u l s e (this); // Разблокировать поток

// Потоковая функция private void ThrFunc ( ) { while (n < 10) { Thread.Sleep (10); lock (this)

// Создать синхронизируемый блок


if (I m o v i n g ) Monitor.Wait (this); // Ожидать разблокировки

>

>

>

Console.WriteLine(" n= " + n++);

Console.WriteLine ("Thread finished");

class SuspendResume

{

static void Main ( ) < User us= new User ( );

>

// Создать объект с // выполняющимся потоком Console.WriteLine ("Поток стартовал"); Thread.Sleep (70); us.SuspendUser( ); // Приостановить поток Console.WriteLine ("Поток приостановлен"); Thread.Sleep (70); us.ResumeUser ( ); // Возобновить поток Console.WriteLine ("Поток возобновлён"); Thread.Sleep (70);

> /* Result: Поток стартовал n= 1 n= 2 n= 3 n= 4 Поток приостановлен Поток возобновлён n= 5 n= 6 n= 7 n= 8 n= 9 Thread finished */

Программа примера 9.6.2.2.2 на языке C# аналогична программе примера 9.6.2.1.1 наязыке Java, отличаясьтолько применением функций монитора Monitor и созданием синхронизированного блока с помощью synchronized. Результат работы программ совпадает (отличие возможно только из-за разной производительности процессора).


9.6.2.3. Использование монитора в C++/CLI Ограничимся рассмотрением монитора для приостановки и возобновления потока, чтобы, воспользовавшись этим, исключить в дальнейшем использование запрещённых функций Suspend() и Resume() класса Thread на языке. C++/CLI. Пример 9.6.2.3 программы на языке C++/CLI аналогичен примеру 9.6.2.2.2 программы на языке C#. Части программы, связанные с синхронизацией выполнения потока, необходимо представить в виде синхронизированных блоков. То есть сформировать их в виде критических секций, начинающихся вызовом функции Enter() монитора и завершающихся вызовом функции Exit(). Поскольку в программе примера 9.6.2.3.2 приостанавливается и возобновляется выполнение потока объекта, которому принадлежит поток, то аргумент функций Enter() и Exit(), ссылающийся на объект, должен быть равен this.

Пример 9.6.2.3.2. Приостановка и возобновление потока в C++/CLI /////////////// // C + + / C L I #include "stdafX.h" #using <System.Drawing.dll> #using <System.Windows.Forms.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawlng; using namespace System::Threading; ref class User

{

int num; int n; Thread ^thr; bool movlng;

// Класс объектов с потоками // Номер объекта // Порядковый номер цикла потока // Ссылка на поток // Признак выполнения потока

public: User (int Num) <

>

num= Num; n= 0; moving= true; // Создать и стартовать поток thr= gcnewThread (gcnew ThreadStart(this, &User::ThrFunc)); thr->Start ();

void SuspendUser ( )

{

moving= false;

// Приостановить поток


void ResumeUser ( )

// Возобновить поток

{

>

Monitor::Enter (thls); // Создать синхронизируемый блок moving= true; Monltor::Pulse (this); // Разблокировать поток Monitor::Exit (this); // Завершить синхронизируемый блок

private: // Потоковая функция void ThrFunc ( ) { while (n < 10) { Thread::Sleep (10); Monitor::Enter (this); // Создать синхронизируемый блок if (I moving) Monitor::Wait (this); // Ожидать разблокировки Monitor::Exit (this); // Завершить синхронизируемый блок

n++;

> >;

>

Console::WrlteLlne("num= {0> n= {1}", num, n);

Console::WriteLine ("Thread num= {0> finished", num);

void main ( ) {

>

User ^usl = gcnew User (1); // Создать первый объект с потоком User ^us2= gcnew User (2); // Создать второй объект с потоком Console::WriteLine ("Потоки стартовали"); Thread::Sleep (40); usl->SuspendUser ( ); // Приостановить поток первого объекта Console::WriteLine ("Поток первого объекта приостановлен"); Thread::Sleep (40); usl->ResumeUser ( ); // Возобновить поток первого объекта Console::WriteLine ("Поток первого объекта возобновлён");

/*Result: num=l n = l num=2 n = l num=l n=2 num=2 n=2 Поток первого объекта приостановлен num=2 n=3 num=2 n=4 Поток первого объекта возобновлён num=l n=3 num=2 n=5 Thread num= 2 finished num=l n=4 num=l n=5


Thread num= 1 finished */

Класс User потокового объекта для управления выполнения потоком включает логическую переменную moving. При создании потокового объекта конструктор присваивает переменной moving значение true, создаёт поток с потоковой функцией ThrFunc() и стартует его. Истинное значение переменной moving не требует разблокировки выполняющегося потока. Выполнение интерфейсной функции SuspendUser() присвоит переменной moving значение false, чте приводит к вызову функции Ws!t() монитора в критической секции потоковой функции и, как результат, к блокировке потока выполнение потоковой функции ThrFunc() приостанавливается. Выполнение потока возобновится, когда функция ResumeUser() присвоит переменной moving значение true и вызовет в критической секции функцию монитора Pulse(). Функция Pulse() разблокирует поток, и потоколвая функция ThrFunc() продолжит своё выполнение.


10. Библиотека .NET Framework и библиотеки языка Java 10.1. Приложение Приложение (an application) - программа для выполнения под управлением операционной системы определенного вида работ, например: редактирования текста, выдачи текущего времени и так далее. Каждое приложение является процессом и представлено на экране своим прикладным окном. Прикладное или главное окно (an application или main window) - прямоугольная область на экране, выделяемая одному приложению для ввода и вывода данных и управления приложением. Операционная система Windows допускает параллельное одновременное выполнение нескольких приложений с возможной синхронизацией их работы, при этом каждое из них может быть представлено своим прикладным окном. В свою очередь каждое прикладное окно может содержать так называемые дочерние окна (child windows). Имеется набор стандартных дочерних окон - интерфейсных элементов (elements of interface или controls), которые активно используются для связи пользователя с приложением. В качестве примера интерфейсных элементов можно привести кнопки (buttons), списки (list boxes), ползунки (sliders), редакторы (editors или text fields, или text boxes). Интерфейсные элементы размещаются в области клиента (client area или work area) окна. В область клиента приложение выдаёт различную текстовую и графическую информацию.

10.2. Классы, объекты и элементы приложения Создание приложений (для Windows) требует знания объектноориентированного программирования. Разрабатывая приложения на С++, желательно использовать библиотеки классов .NET Framework, MFC, OWL, ATL или другие, которые существенно облегчают реализацию программ. При разработке программ на языке C# применяется библиотека .NET Framework, а на языке Java - пакеты java.lang, java.awt, java.util,java.swing и другие. Все классы библиотеки .NET Framework и пакетов Java порождаются из класса Object. С языком Visual J# можно использовать как пакеты языка Java так и библиотеку .NET Framework. В данной книге в программах на языке Visual J# будет применяться только пакеты языка Java. Наличие в среде разработки


Visual Studio .NET как библиотеки .NET Framework, так и пакетов языка Java упрощает изучение как языка Java, аналогичному языку Visual J#, так и языка C# в одной среде разработки с родными для них библиотеками и пакетами. При объектно-ориентированном программировании приложений следует различать такие понятия, как объект и элемент. Объект - понятие языков С++, C# и Java. Вне модуля приложения объект размещается в памяти, будучи созданным из класса с помощью оператора new или gcnew. Из одного класса можно создать несколько объектов, которые, как правило, отличаться своим состоянием. Элемент - это то, что представляет объект на экране, обеспечивая интерактивную связь пользователя с приложением. Обычно элемент представляется в виде специализированных дочерних окон - кнопок, панелей редактирования, ползунков и других. При применении библиотеки .NET Framework окна являются объектами класса Form и называются формами, а при применении пакетоа java.awt языка Java окна являются объектами класса Frame и называются фреймами. Управляющие элементы - объекты, создаваемые из классов Control и UserControl.

10.3. Простейшие приложения Создадим простейшее Windows-пршожеше, прикладное окно с именем "Hello".

Рис.10.3.1. Прикладное

окно приложения

Hello

которое выдаёт на экран

примера

10.3.1

Пример 10.3.1. C#. Простое Windows-приложение. Два варианта. //////////////////// // C # Вариант 1 class MainClass

{

static void Main ( ) {

>

System.Windows.Forms.Form form= new System.Windows.Forms.Form ( ) ; form.Size= new System.Drawing.Size (300, 100); form.Text= "Hello"; System.Windows.Forms.Application.Run (form);


//////////////////// // C# В а р и а н т 2 using System.Wlndows.Forms; using System.Drawlng; class MalnClass { static void Main ( ) {

>

>

Form form= new Form ( ) ; form.Size= new Size (300, 100); form.Text= "Hello"; Application,Run (form);

C#. Windows-приложение начинает выполнять главная функция Main(). Функция Main() примера 10.3.1 создаёт объект form прикладного окна из класса Form пространства имён System.Windows.Forms библиотеки .NET Framework. Затем она устанавливает свойство Size объекта form окна, присвоив этому свойству ссылку на объект класса Size пространства имён System, содержащий ширину и высоту окна. Потом определяется свойство Text, указывающее заголовок объекта form окна. Сформировав прикладное окно, функция Main() запускает приложение, вызвав статическую функцию Run() класса Application пространства имён System.Windows.Forms и передав ей ссылку form на объект окна в качестве её аргумента. Аргументом функции Run() является ссылка на главное (прикладное) окно приложения. Получив ссылку на прикладное окно, функция Run() высвечивает его на экране. В дальнейшем будут рассмотрены и другие окна, но они отображаются на экране с помощью функции Show(). Для сокращения текста программы во втором варианте программы употреблён оператор using, указывающий на возможность применения в программе классов пространства имён System.Windows.Forms и пространства имён System.Drawing без указания их перед каждым классом, как это имело место в первом варианте программы. Пример 10.3.2. Java. Простое оконное приложение. /////////////// // J a v a и J # import java.awt.*; class MainClass { public static void main ( ) { Frame frame= new Frame ( ) ; frame.setSize (300, 100); frame.setTitle ("Hello"); frame.show ( );


Java. В отличие от C# программы примера 10.3.1 функция main() Java программы примера 10.3.2 создаёт объект frame прикладного окна из класса Frame пакета java.awt. Затем функция main(), используя функцию-свойство setSize(), устанавливает ширину и высоту размер окна. Потом функциясвойство setTitle() определяет заголовок прикладного окна. Как видим, в случае языка Java не требуется явно вызывать функцию Run(), она вызывается неявно. Чтобы показать прикладное окно на экране, надлежит обязательно вызвать функцию show(). C# и Java. Примеры 10.3.1 и 10.3.2 показывают сходство и отличие C# и Java программ при написании простых приложений. Разные библиотеки содержат разноимённые классы и разноимённые свойства для создания сходных объектов и при использовании сходных свойств. В C# программе для подключения требуемых стандартных классов применяется оператор using, а в Java программе - оператор import. Функция Main() в C# программе пишется с большой буквы, а в Java программе - с маленькой. В C# программе используются непосредственно свойства Size и Text, а в Java программе - функции setSize() и setTitle() свойств. В C# программе явно применяется функция Run() и показывает прикладное окно, а в Java программе эта функция вызывается неявно и не показывает окно, так что приходится вызвать функцию show(). Надо заметить, что на языке C# принято писать имена функций с большой буквы, а на языке Java с маленькой. Пример 10.3.3 иллюстрирует оконное приложение наязыке C++/CLI. Пример 10.3.3. C++/CLI. Простое Windows-приложение. /////////////// // C++/CLI #include "stdafx.h" #using<System.dll> #uslng<System.Windows.Forms.dll> #using<System.Drawlng.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing; ref class MalnC!ass : public Form < public: MainClass (vold) { this->Slze = *gcnew Drawing::Size (300,300); // или thls->Slze = Drawing::Size (300,300); this->Text = "Hello";

>;


void main ( ) {

>

Application::Run (gcnew MainClass ());

C++/CLI. Программа на языке C++/CLI требует не только указания об использовании требуемых пространств имен библиотек классов с помощью using, но и подключения этих библиотек с помощью операторов #using. Управляемый класс MainClass помечается словом ref. Используются двоеточия и стрелки. Функция main() пишется с маленькой буквы. Как просто получается приложение с классом Form библиотеки .NET Framework, как просто создаётся порождением из этого базового класса наш класс MainClass прикладного окна в примере 10.3.3. Но сам класс Form сложен. Он содержит множество свойств, собьггий и интерфейсных функций, с некоторыми из которых мы познакомимся позднее. Взгляните на рис. 10.3.1.1. Оказывается, класс Form сам наследует множество других классов, каждый из которых обогащает его функциональность. Как любой другой класс библиотеки .NET Framework, класс Form наследует корневой базовый класс Object, позволяющий всем объектам в системе сравниваться друг с другом, воспользовавшись функцией Equals() этого класса, или получать описание любого объекта с помощью функции ToString(). Обратим особое внимание на класс Control, включающий богатый набор свойств, событий и функций, которые наследуются не только классом окна, но многими другими классами, объекты которых, как и объекты окна, должны обладать общим поведением управляющих элементов (controls). Потом мы рассмотрим такие управляющие элементы, как кнопки и редакторы, классы которых наследуют также класс Control. Мы скоро убедимся, что объекты окон, кнопок и редакторы содержат не только одноимённые свойства Size, Text, применённые в примерах 10.3.1 и 10.3.3, но и многие другие свойства, например, Location, Name, Visible и др. I

*Form

]

f~>l +ContalnerControl I

^+ScrollableControl 1

t ^ *ControT~

I +Qbjfirt fd l*MarehalByRefOhjgrt КЗ i*Campnnent Рис. 10.3.1.1. Наследование классов классом Form Пакеты, содержащие классы языка Java, отличается от библиотеки .NET Framework языков C# и C++/CLI. Отличаются и наследования классов, в чём убеждает рис. 10.3.1.2. В языке Java классу Control библиотеки .NET Framework соответствует класс Component. Этот класс Component включает свойства setSize(), getSize(), setLocation0, getLocation(), setVizible(), setName, getName() и др., используемых при создании интерфейсных элементов. Но в отличие от класса Component языка Java класс Component библиотеки .NET Framework используется иначе - он применяется при разработке так назы-


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

I *Frame~i

D^ *Wlndow I

DH+Cnntainar I Й*СотиопвпЛ~С>Г~*0Ыес1 I

Рис. 10.3.1.2. Наследование классов классом Frame

10.4. Сообщения Windows, события и делегаты Важно понять, что Windows - операционная система с управляемой сообщениями архитектурой. Windows находится в постоянном ожидании того, когда же пользователь подведёт указатель мыши к элементу в прикладном окне приложения и нажмёт на левую клавишу мыши или передвинет ползунок, или нажмёт клавишу клавиатуры и т. д. Windows тотчас генерирует соответствующее сообщение и помещает его в системную очередь сообщений приложения. Каждое сообщение имеет свой идентификатор и с ним может быть связана какая-либо информация (например, координаты указателя мыши в прикладном окне). Имеется более сотни стандартных сообщений Windows. Для каждого стандартного сообщения (собьггия) есть обработчик сообщения (события) по умолчанию - специальная функция, которая обычно ничего не делает. Но мы можем реализовать свою обработку сообщения, переопределив требуемую функцию-обработчика для этого сообщения. C# и C++/CLI. Сообщения выполняются в рамках окон, то есть форм, и для связи функций-обработчиков с сообщениями используется механизм событий и делегатов. Посредством делегатов к событиям привязывают требуемые функции-обработчики определённого формата. Каждая форма или управляющий элемент (который также является формой, но специализированной) имеет фиксированный набор предопределённых событий с предопределённьши делегатами, которые определяют формат функций-обработчиков этих событий. При желании для простоты можно воспользоваться предопределённьши обработчиками событий. В этом случае не требуется их подписка на события. Java и J#. Обработка событий основана на делегировании обработчиков с помощью интерфейсов. Вместо делегатов используются предопределённые интерфейсы и включённые в них обработчики. Итак, реализация обработки событий в языках C# и Java отличаются; она рассмотрена в разделах 10.4.1 и 10.4.2 на примере обработки события от мыши.


X0.4.1. Обработка событий мыши на языке C# Для собьггия MouseDown, возникающего при нажатии на клавишу мыши, используют делегат MouseEventHandler. Событие определено в классе System.Windows.Forms.Control, из которого порождён класс Form, следующим образом: public event

MousetventHandier

MouseDown;

С событием MouseDown. можно с помощью оператора "+=" связать один или несколько объектов делегата MouseEventHandler собьггия с инкапсулированными в них обработчиками, допустим, обработчик OurMouseDown(): MouseDown

+= n e w

MouseEventHandler(OurMouseDown);

Разумеется, формат обработчика определяется делегатом MouseEventHandler и должен быть следующим: v o i d OurMouseDown

( o b j e c t sender,

где sender - ссылка на объект-источник события, args - ссылка на объект типа MouseEventArgs, ся к свершившемуся событию.

MouseEventArgs args);

содержащий данные, относящие-

Обработчик OurMouseDown() получает аргумент типа MouseEventArgs, содержащий данные, относящиеся к обрабатываемому событию и представленные следующими свойствами класса MouseEventArgs, специфичными для собьггия MouseDown: -

B u t t o n , указывающее, какая клавиша нажата; Clicks, указывающее, сколько раз была нажата и отпущена клавиша; Delta, указывающее, сколько раз было повёрнуто колёсико мыши; X, указывающее значение координаты x носика мыши и Y , указывающее значение координаты у носика мыши.

Пример 10.4.1 содержит C# программу с собьггием, происходящим при нажатии на клавишу мыши. При каждом нажатии на клавишу появляется строка на консоли. Пример 10.4.1. Программа обработки события мыши на C#. Приложение создаёт прикладное окно и ожидает нажатия клавиши мыши в этом окне (точнее, в области клиента окна). При нажатии на клавишу в консольное окно выдаётся строка с координатами носика курсора мыши.


Рис. 10.4.1. При нажатии на клавишу мыши появилась очередная строка на консоли Ниже приводятся три варианта реализации этого приложения на языке C#. //////////////////// // C # Вариант 1 using System; using System.Drawing; using System.Windows.Forms; class MainClass : Form

{

// Класс прикладного окна

// Использовать предопределённый обработчик OnMouseDown мыши protected override void O n M o u s e D o w n ( M o u s e E v e n t A r g s e) { Console.WriteLine ("Mouse: x= " + e.X + " y= " + e.Y);

} static void Main ( )

// Главная функция

{

>

MainClass mC= new MainClass ( ); // Создать прикладное окно mC.Size= new Size (400, 200); mC.Text= "Hello"; Application.Run (mC); // Выполнить приложение

> /* Result: При каждом нажатии на клавишу мыши в прикладном строка вида M o u s e : x = 10 y = 55, где x, у - координаты

окне выдаётся на консоль

носика мыши

*/ //////////////////// // C # Вариант 2 using System; using System,Windows.Forms; public class MainClass: Form

// Класс прикладного окна


// Использовать наш обработчик OurMouseDown мыши private v o i d O u r M o u s e D o w n ( o b j e c t sender, M o u s e E v e n t A r g s e)

{

>

Console.WriteLine ("Mouse: x= " + e.X + " y= " + e.Y);

static void Maln ( )

{

>

>

// Главная функция

MainClass mC= new MainClass ( ); // Создать прикладное окно // Подписать наш обработчик на событие MouseDown m C . M o u s e D o w n += n e w M o u s e E v e n t H a n d l e r (mC.OurMouseDown); Application.Run (mC); // Выполнить приложение

//////////////////// // C# Вариант 3 using System; using System.Windows.Forms; public class MainClass : Form

<

public MainClass ( )

// Класс прикладного окна // Конструктор

{

>

// Подписать наш обработчик на событие MouseDown M o u s e D o w n += n e w M o u s e E v e n t H a n d l e r ( O u r M o u s e D o w n ) ;

// Использовать наш обработчик OurMouseDown мыши private v o i d O u r M o u s e D o w n (object sender, M o u s e E v e n t A r g s e)

<

>

Console.WriteLine ("Mouse: x= " + e.X + " y= " + e.Y);

static void Main ( )

{

>

>

// Главная функция

Application.Run (new MainClass ()); // Выполнить приложение

C#. В С#-программах примера 10.4.1 рассмотрены три варианта применения обработчиков мыши. Предопределённый обработчик protected override void OnMouseDown (MouseEventArgs e) уже описан в базовом классе Form и в первом варианте нашей программы он переопределяется для выдачи координат носика мыши на консоль. Во втором и третьем вариантах программы используется наш обработчик OurMouseDown, формат которого соответствует делегату MouseEventHandler события мыши. Во втором варианте программы подписка обработчика на


объект собьггие MouseDown мыши осуществляется в теле функции Main (), а в третьем варианте - в теле конструктора MainClass ( ) прикладного окна. Достоинством второго и третьего вариантов является то, что на одно событие можно подписать много обработчиков различных объектов. В нашем случае подписывается один обработчик объекта, являющегося самим окном, поэтому вариант первый предпочтителен. В первом варианте приложения задаётся размер и заголовок прикладного окна. В двух других вариантах размеры окна берутся по умолчанию, поэтому в них отсутствует using System.Drawing, поскольку отпала необходимость в использовании класса Size.

10.4.2. Обработка событий мыши на языке C++/CLI Пример 10.4.2 иллюстрирует один из вариантов обработки события

мыши на языке C + + / C L I .

Пример 10.4.2. Программа обработки события мыши на C++/CLI. ///////////////

// C + + / C L I #include "stdafx.h" #using<System.dll> #using<System.Windows.Forms.dll> #uslng<System.Drawing.dll>

using namespace System; using namespace System::Windows:;Forms; using namespace System::Drawing; ref class F o r m l : public Form { public: Forml (void) <

>

this->Size = Drawing::Size (300,300); this->Text = "Forml"; thls->MouseDown += gcnew MouseEventHandler (this, &Forml::OurMouseDown);

protected: void OurMouseDown (Object^ sender, MouseEventArgs ^e) <

};

>

Console::WriteLine ("Mouse: x= " + e->X + " y= " + e->Y);

vold maln ( ) < System: :Windows::Forms::Application::Run (gcnew Forml());


} C++/CLI. Программа на языке C++/CLI примера 10.4.2 соответствует третьей версии обработки собьггия мыши на языке C# примера 10.4.1. Подписка нашего обработчика OurMouseDown() мыши на событие MouseDown осуществляется в конструкторе управляемого класса Forml окна. В программе, оперирующей с управляемыми классами, активно применяются дескрипторы и стрелки. Обратим внимание, - в соответствии с грамматикой языка C++/CLI конструктор делегата MouseEventHandler включает два аргумента, указывающих на объект (this), в котором находится обработчик, и сам обработчик OurMouseDown. Правила использования делегатов, собьггий и классов библиотеки .NET Framework на языках C++/CLI и C# схожи и делают похожим программирование на этих языках, поэтому в дальнейшем языку C++/CLI будет уделяться меньшее внимание, чем языкам Java и C#. При программировании на Java и C# мы намеренно применим разные библиотеки, что облегчит в дальнейшем читателю освоение существенно различающихся Java и .NET технологий программирования.

10.4.3. Обработка событий мыши на языке Java с использованием предопределённого обработчика mouseDown Различные варианты обработки собьггий на языке Java будут рассмотрены в разделе 10.5. В программе же примера 10.4.3 иллюстрируется обработка собьггия мыши на языке Java, аналогичная обработке на языке C# с использованием предопределённой функции. Пример понятен без комментариев. Пример 10.4.3. Программа обработки события мыши на Java. /////////////// //Java Вариант1 lmportjava.awt.*; lmportjava,awt.event.*;

(Другиевариантывпримере10.5.2)

class MainClass extends Frame {

// Класс прикладного окна

// Выполнить предопределённый обработчик mouseDown мыши public b o o l e a n m o u s e D o w n (Event e, int x, int у)

< >

System.out.println ("Mouse: x= " + x + " y= " + у); return true;

public static void maln ( ) {

// Главная функция

MalnClass mC= new MainClass ( ) ; mC.setSize (400, 200);


mC.setTitle ("Hello"); mC.show ( ); > /* Result: При каждом нажатии на клавишу мыши в прикладном окне выдаётся на консоль строка вида M o u s e : x = 10 y = 55, где к, у - координаты носика мыши */

Java и J#. В программу варианта 1 (с другими вариантами Вы познакомитесь вскоре) включён naKeTjava.awt, содержащий оконные библиотечные классы, и naKeTjava.awt.event, включающий классы и интерфейсы, связанные с событиями. Функция public boolean mouseDown (Event e, int x, int у) описана в базовом классе Frame окна. В нашей программе она переопределяется, осуществляя выдачу на консоль координат носика мыши. Завершим рассмотрение события MouseDown, связав его со статическими и обычными обработчиками объектов двух разных классов в примере 10.4.4. При нажатии на клавишу мыши в прикладном окне выдаётся панель сообщений для каждого обработчика со строкой, содержащей значения координат носика курсора мыши и имя класса, к которому принадлежит функцияобработчик.

10.4.4. Связь события с несколькими объектами на языке C# Как правило, во много объектной программе событие из объектовисточников посылается нескольким объектам-приёмникам. Самостоятельно функционирующие объекты взаимодействуют посредством событий. В программе примера 10.4.4 событие мыши обрабатывается как в объекте источнике класса MainClass, так и в объектах-приёмниках классов CObjl и CObj2. Подписка обработчиков события этих объектов на событие источника осуществляется в теле конструктора объекта источника класса MainClass. При нажатии клавиши мыши в окне источника выдаются блоки сообщений типа MessageBox от обработчиков события мыши. Пример 10.4.4. Связь события с несколькими объектами. //////////////////// //c# using System; using System.Windows.Forms; class C O b j l <

// Класс со статической функцией обработки события

static public void MouseDownObjl (object sender, MouseEventArgs e) <

MouseDown


MessageBox.Show ("CObjl:x=" + (e.X).ToStrlng() + " y=" + (e.Y).ToString());

> ciass CObJ2

{

>

// Класс с обычной функцией обработки события MOuseDown

public vold MouseDownObj2 (object sender, MouseEventArgs e) {

>

MessageBox.Show ("CObj2:x=" + (e.X).ToStrlng( ) + " y=" + (e.Y).ToStrlng( ));

class MainClass : Form

{

public MalnClass ( )

// Класс прикладного окна // Конструктор

{

>

// Инициализация события MouseDown this.MouseDown += new MouseEventHandler (CObJl.MouseDownObjl); CObj2 obj2= new CObj2( ); this.MouseDown += new MouseEventHandler (obj2.MouseDownObj2); this.MouseDown += new MouseEventHandler (OurMouseDown);

// Обработать событие MouseDown private void OurMouseDown (object sender, MouseEventArgs e)

<

>

// Выдать блок сообщений типа MessageBox MessageBox.Show ("CMainClass:x=" + (e.X).ToString ( ) + " y=" + (e.Y).ToString ( ) ) ;

static void Main ( )

{

>

>

// Главная функция

Application.Run (new MainClass ( ) ) ;

// Выполнить приложение

C#. Программа примера 10.4.4 иллюстрирует применение и подписку обработчиков на событие мыши статического обработчика и обычных обработчиков, принадлежащих объектам различных классов. Статический обработчик MouseDownObjl() описан в классе CObjl. Обычный обработчик OurMouseDown() описан в классе прикладного окна, а обычный обработчик MouseDownObj2() - в классе CObj2. Следует обратить внимание на отличие в подписке на событие обычных и статических обработчиков. При подписке обычного обработчика в аргументе конструктора делегата указывается обработчик через ссылку на объект, а при подписке статического обработчика в аргументе конструктора делегата указывается обработчик через ссылку на класс.


Рис. 10.4.4. При нажатии на клавишу мыши появился блок сообщений очередного обработчика

10.5. Ещё раз о событиях и уведомлении в языке Java Пополнив наши знания о событиях, генерируемых при применении мыши, остановимся на них подробнее, вспомнив о событиях, возникающих в наблюдаемых объектах и воспринимаемых обозревателями, Рассмотрим ещё раз делегирование, которое было впервые предложено в языке Java и формирует механизм обработки событий, Итак, объект-источник генерирует событие, на которое реагирует объект-приёмник события. Естественно, что на событие от одного источника могут реагировать один или несколько приёмников. Очевидно, из этих соображений авторы делегирования для класса источника установили наследование класса (extends), а для классов приёмников - наследование интерфейса (implements). Таким образом, приёмники могут реагировать на события разных источников, каждый из которых представлен своим интерфейсом. Что касается источников, то каждому из них надлежит наследовать только один класс события (это требования наследования классов в языке Java и в управляемом коде), что ограничивает источник генерированием только одного типа событий. Но самое неприятное здесь то, что, наследовав класс события, источник уже не может наследовать другой базовый класс, так, возможно, желательный для расширения его функциональности. Ключевое слово extends, существенно ограничив расширение функциональности источника из-за исключения наследования иных необходимых классов, приводит к необходимости применения специальных разноликих объектов с надлежащей функциональностью. Располагая объектом, класс которого уже наследует какой-либо класс и не наследует класс собьггия и в котором требуется генерировать события, достаточно в этом объекте применить один или несколько специальных объектов. Каждый же из этих специ-


альных объектов будет генерировать только событие определённого типа, класс которого он для этого и наследует. Теперь вспомним раздел, посвящённый наблюдаемым объектам и обозревателям. Обозреватель с помощью предопределённой функции update() реагирует на события (уведомления) от наблюдаемых объектов, генерированных (уведомляющих) при выполнении предопределённых функций setChanged() и notifyObservers(). Предварительно надлежит, применив функцию addObserver(), подписать обозреватель на собьггие каждого наблюдаемого объекта, т а к , наблюдаемый объект выступает как источник события, а обозреватель - как приёмник. И, как это требовалось, наблюдаемый объектисточник наследует класс Observable, а обозреватель-приёмник - интерфейс Observer. Как и следовало ожидать, применяется тот же механизм делегирования. Заметим, что уведомление предоставляет возможность использовать любые наблюдаемые объекты и обозреватели, в отличие от делегирования событий конкретных классов, наследующих суперкласс EventObject. Уведомление удобно применять в многопоточных приложениях.

10.5.1. Обработка событий элементов интерфейса пользователя в языке Java Пользователь, располагая прикладным окном, оперирует для связи с программой интерфейсными элементами, каждый из которьгх способен генерировать событие. К таким элементам относится уже используемая нами мышь, но также кнопки, флажки, списки с выбором, пункты меню, полосы прокрутки, клавиши клавиатуры, текстовые редакторы и прикладное окно, генерирующее события при изменении его размера, при его закрытии и др. Эти элементы представляются в программе в виде объектов соответствующих классов, в которые встроен механизм делегирования, предполагающий применение определённых интерфейсов и предопределённых функций. Причём, ради простоты, не требуется наследовать класс события, соответствующего элементу, поскольку он запретил бы наследовать более значимый класс, определяющий основное функционирование объекта. Но обязательно надо наследовать некоторый интерфейс, предписанный для данного элемента. naKerjava.awt.event содержит следующие классы собьгтий интерфейсных объектов, которые определяют множество констант, свойств и функций, используемых с соответствующими элементами: ActionEvent, AdjustimentEvent, ComponentEvent, FocusEvent, InputEvent, ItemEvent, KeyEvent, MouseEvent, TextEvent и WindowEvent. Приёмники собьггий (которые также называют блоками прослушивания) должны наследовать интерфейсы соответствующих интерфейсных объектов. Определены следующие интерфейсы прослушивания событий: ActionListener, AdjustmentListener, ComponentListener, ContainerListener, Focus-


Listener, ItemListener, KeyListener, MouseListener, MouseMotionListener, TextListener и WindowListener. Эти интерфейсы включают предопределённые обработчики событий. Например, интерфейс ActionListener включает предопределённый обработчик actionPerformed(), а интерфейс WindowListener включает предопределённые обработчики windowClosed(), windowClosing(), windowOpened() и др. Так же как при уведомлении обозреватели подписывают на наблюдаемый объект с помощью функции addObserver(), так и в случае интерфейсных элементов приёмники (блоки прослушивания) подписывают на источник с помощью специальной функции класса источника, которая имеет вид: public void add7ypeUstener (7>peListener tL); где Т/ре-типисточникасобытия.

При наступлении события источника выполняется предопределённый обработчик подписавшегося приёмника. 10.5.2. Обработка событий мыши на языке Java, используя интерфейс MouseListener Из предыдущего раздела явствует, что для подписки приёмника прослушивания на событие мыши необходимо в качестве типа источника события Туре в общей записи public vold add7ypeListener (7ypeListener tL);

взять Mouse. Полученная функция public void addMouseLlstener (MouseListener tL);

применена в примере 10.5.2. Пример 10.5.2. Обработка событий мыши на Java, используя интерфейс MouseListener. /////////////// // J a v a и J # Вариант 2 lmportjava.awt.*; importjava.awt.event.*;

(Вариант 1 смотрите в примере 10.4.3)

// Класс прикладного окна public class MainClass extends Frame i m p l e m e n t s M o u s e L i s t e n e r

{

// Предопределённые обработчики интерфейса MouseListener p u b l i c v o i d mouseCI!cked ( M o u s e E v e n t mE){} public v o i d mouseEntered ( M o u s e E v e n t m E ) { } public v o i d mouseExited ( M o u s e E v e n t mE){} public v o i d mouseReleased ( M o u s e E v e n t mE){>


// Выполнить предопределённый обработчик mousePressed мыши public v o i d m o u s e P r e s s e d ( M o u s e E v e n t m E )

<

System.out.println ("Mouse: x= " + mE.getX ( ) + • y= " + mE.getY( ));

}

public static void main () // Главная функция

{

>

Ma!nC!ass mC= new MainClass ( ); mC.setSize (new Dimension (400, 200)); mC.setTitle ("Hello"); mC.show ( ); mC.addMouseListener(mC);

>

///////////////

//Java и J# Вариант 3 importjava.awt.*; import java.awt.event.*; // Класс прикладного окна public class MainClass extends Frame i m p l e m e n t s M o u s e L i s t e n e r

{

public MainClass ( ) {

>

thls.setSize (new Dimension (400, 200)); this.setTitle ("Hello"); t h i s . a d d M o u s e L i s t e n e r (this);

// Предопределённые обработчики интерфейса M o u s e L i s t e n e r мыши public v o i d mouseClicked ( M o u s e E v e n t m E ) { } public v o i d m o u s e E n t e r e d ( M o u s e E v e n t mE){> public v o i d m o u s e E x i t e d ( M o u s e E v e n t m E ) { ) public v o i d m o u s e R e l e a s e d ( M o u s e E v e n t m E ) { ) // Выполнить предопределённый обработчик mousePressed мыши public v o i d m o u s e P r e s s e d ( M o u s e E v e n t mE) {

>

System.out.println ("Mouse: x= " + mE.getX ( ) + » y= " + mE.getY( ));

public static void main (String[] args) // Главная функция {

>

>

MainClass mC= new MainClass ( ) ; mC.show ( ) ;

///////////////

//Java и J#

Вариант 4


importjava.awt.*; import java.awt.event.*;

// Класс объекта-приёмника событий

class Listener implements MouseListener

{

// Предопределённые обработчики интерфейса MouseListener мыши public void mouseClicked (MouseEvent mE){} public void mouseEntered (MouseEvent mE){} public void mouseExited (MouseEvent mE){} pubiic void mouseReieased ( M o u s e c v e n i mE}{} // Выполнить предопределённый обработчик mousePressed мыши public void mousePressed (MouseEvent mE) {

>

}

System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE.getY( ));

// Класс прикладного окна public class MainClass extends Frame { public MainClass ( ) { Listener 1= new Listener ( ); // Создать объект приёмника thls,setSize (new Dimension (400, 200)); this.setTitle ("Hello"); this.addMouseListener (l}; // Подписать приёмник I событий

} public static void main ( )

{

>

// Главная функция

MainClass mC= new MainClass ( ) ; mC.show ();

> /* Result: При каждом нажатии на клавишу мыши в прикладном окне выдаётся на консоль строка вида Mouse: x= 10 y= 55, где x, у - координаты носика мыши */

В примере 10.5.2 рассмотрены несколько вариантов реализации обработки события мыши. Прежде всего, обратим внимание, что в отличие от предопределённого обработчика mouseDown() примера 10.4.3 (вариант 1) обработчики мыши интерфейса MouseListener отличаются не только именами, но также возвращаемым значением и списком параметров. Интерфейс MouseListener требует реализовать в классе MainClass, наследующий этот интерфейс, всех обработчиков.


Непотребные обработчики имеют пустые тела. И только необходимый нам обработчик mousePressed() выдаёт на консоль координаты носика мыши. Во втором и третьем вариантах программы функция addMouseListener() добавляет объект приемника собьггий к источнику событий. Поскольку приёмник событий (объект окна класса MainClass), содержащий обработчики, является и источником событий, генерирующим событие (при нажатии на мышь в окне генерируются события мыши), приходится функцию, ссылающуюся в аргументе на объект окна, применить к тому же окну: MainClass mC= new MainClass ( ); // Вариант2. В функции main() mC.addMous6Llstener (mC); this.addMouseLlstener (this);

// ВариантЗ. В конструкторе MainClass()

В варианте 4 программы предложен отдельный класс Listener приёмника, наследующий интерфейс MouseListener и реализующий все функции этого интерфейса. Функции mouseClicked(), mouseEntered(), mouseReleased() и mouseExited() реализованы с пустыми телами, а функция mousePressed() выдаёт на консоль координаты носика мыши. В конструкторе класса MainClass источника создаётся объект приёмника, наследующий интерфейс MouseListener, и этот объект приёмника добавляется к источнику (this) с помощью функции addMouseListener ().

10.5.3. О б р а б о т к а с о б ы т и й м ы ш и н а я з ы к е Java, и с п о л ь з у я к л а с с ы адаптеров

Классы адаптеров упрощают обработку собьггий, поскольку уже наследуют интерфейсы событий и реализуют обработчики по умолчанию. Классы адаптеров ComponentAdapter, ContainerAdapter, FocusAdapter, KeyAdapter, MouseAdapter, MouseMotionAdapter и WindowAdapter реализуют соответственно интерфейсы прослушивания событий ComponentListener, ContainerListener, FocusListener, KeyListener, MouseListener, MouseMotionListener и WindowListener. Для использования класса адаптера необходимо создэть новый класс объекта приёмника, который наследует требуемый класс адаптера. В созданном классе можно переопределить требуемые обработчики интерфейса. Остальные обработчики могут быть оставлены без внимания, поскольку они уже реализованы в наследуемом классе адаптера. Затем создаются объект-источник собьггия и объект нашего нового класса с переопределённым в нём нужным обработчиком. И, наконец, к объекту источнику его функция add7>>peListener () добавляет объект приёмника. Из примера 10.S.3.1 видно, насколько упрощается программа. Сравните варианты 5 и 6 примера 10.S.3.1 с вариантом 4 примера 10.5.2. В классе Listener теперь отпала необходимость обязательной реализации всех обработчиков интерфейса MouseListener. Класс Listener упростился, но наследование класса MouseAdapter дозволяет для этого класса Listener теперь наследование только интерфейсов.


Программу можно ещё упростить, применив внутренние или анонимные классы, о чём рассказано в разделе 10.5.4. Пример 10.5.3.1. Обработка событий мыши на Java, используя классы адаптеров. /////////////// // J a v a и J #

Вариант 5

importjava.awt.*; importjava.awt.event.*;

class Listener e x t e n d s M o u s e A d a p t e r

{

>

// Выполнить предопределённый обработчик mousePressed мыши p u b l i c v o i d m o u s e P r e s s e d ( M o u s e E v e n t mE) {

>

System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE.getY( ));

// Класс прикладного окна class MainClass extends Frame { public MainClass ( ) {

>

Listener ls= n e w Listener ( ); this.setSize (400, 200); thls.setTltle ("Hello"); this.addMouseListener(is);

public static void main ( ) // Главная функция {

>

>

MainClass mC= new MainClass (); mC.show ();

///////////////

// Java и J # Вариант 6 importjava.awt.*; importjava.awt.event.*; class Listener e x t e n d s M o u s e A d a p t e r

{

// Выполнить предопределённый обработчик mousePressed мыши public v o i d m o u s e P r e s s e d ( M o u s e E v e n t mE) { System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE.gctY( ));

}


// Класс прикладного окна class MainClass extends Frame { public MainClass ( ) <

>

this.setSize (400, 200); this.setTitle ("Hello"); t h i s . a d d M o u s e L i s t e n e r ( n e w Listener ( ));

pubiic static void main ( ) /'/' Главная функция {

>

MalnClass mC= new MainClass ( ); mC.show ( );

> /* Result: При каждом нажатии на клавишу мыши в прикладном окне выдаётся на консоль строка вида Mouse: x= 10 y= 55, где x, у - координаты носика мыши

*,

Java и J#. Если бы в вариантах 5 и 6 программы класс Listener приёмника наследовал интерфейс MouseListener, то пришлось бы описать в классе все интерфейсные функции этого интерфейса, поскольку наследование интерфейса требует это. То есть поступить так, как в примере 10.5.2. Но класс Listener примера 10.5.3.1 наследует класс MouseAdapter адаптера, который наследует интерфейс MouseListener и уже реализовал все его интерфейсные функции по умолчании. Это позволило переопределить в классе Listener только нужную нам функцию mousePressed(). Подписка на событие мыши в вариантах 5 и 6 отличается. В варианте 5 в теле конструктора MainClass() создаётся объект ls приёмника (слушателя) в куче, и функция addMouseListener использует эту ссылку ls. В варианте 6 ссылка автоматически создаётся в качестве аргумента в результате создания объекта приёмника класса Listener.

10.5.4. Обработка событий мыши на языке Java, используя внутренние классы адаптеров и анонимные классы Классы языков программирования C++/CLI, C# и Java позволяют описывать и использовать внутри себя другие классы, которые называются внутренними классами. В программе примера 10.5.4.1 внутри класса MainClass источника описан внутренний класс Listener приёмника, наследующий класс MouseAdapter адаптера. Конструктор класса MainClass с помощью функции addMouseListener() присоединяет приёмник к источнику.


Пример 10.5.4.1. Обработка событий мыши на Java, используя внутренние классы адаптеров. /////////////// // Java и J # Вариант 7 import java.awt.*; import java.awt.event.*; // Класс прикладного окна public class MainClass extends Frame { public MainClass ( )

{

>

this.setSize (new Dimension (400, 200)); thls.setTitle ("Hello"); t h i s . a d d M o u s e L i s t e n e r ( n e w Listener ( ));

// Внутренний класс Listener приемника событий мыши class Listener extends MiouseAdapter { // Выполнить предопределённый обработчик mousePressed мыши public void m o u s e P r e s s e d ( M o u s e E v e n t mE) {

>

>

System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE.getY( ));

public static void main ( ) // Главная функция {

>

MainClass mC= new MainClass ( ); mC.show ();

> /* Result: При каждом нажатии на клавишу мыши в прикладном окне выдаётся на консоль строка вида Mouse: x= 10 y= 55, где x, у - координаты носика мыши */

В программах языка Java для обработки событий широко применяется анонимный класс, который не имеет имени, поскольку объект этого класса используется в конкретном контексте один раз, то есть, нет необходимости объявлять для него ссылку, так как этот объект более нигде не применяется. В профамме примера 10.5.4.2 в качестве аргумента используется не ссылка на объект класса приёмника как в примере 10.5.4.1, а описан особым образом анонимный класс. Конструкция параметра функции addMouseListener() вида


n e w MouseAdapter ( ) {. • •>

указывает компилятору о том, что имеет место анонимный класс. Компилятор создаёт объект анонимного класса MouseAdapter и передаёт его в качестве аргумента в функцию addMouseListener(). Как видим в примере 10.5.4.2, анонимные классы упрощают программу. Пример 10.5.4.2. Обработка событий мыши на Java, используя внутренний анонимный класс. /////////////// // J a v a и J # Вариант 8 importjava.awt.*; import java.awt.event.*;

// Класс прикладного окна class MainClass extends Frame { public MainClass ( ) { this.setSlze (400, 200); this,setTitfe ("Hello"); // Примененить в н у т р е н н и й а н о н и м н ы й класс this.addMouseListener ( n e w M o u s e A d a p t e r ( ) { public void mousePressed (MouseEvent mE)

{

>

});

>

System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE,getY( ));

public static void main ( ) // Главная функция {

>

MainClass mC= new MainClass ( ) ; mC.show ( ) ;

> /* Result: При каждом нажатии на клавишу мыши в прикладном окне выдаётся на консоль строка вида Mouse: x= 10 y= 55, где x, у - координаты носика мыши */

10.5.5. Обработка события закрытия окна Применим класс WindowAdapter адаптера для обработки собьггия закрытия окна. Дело в том, что иногда не удаётся закрыть окно, нажав на кнопку закрытия окна. Это возникает в тех случаях, когда собьггие закрытия окна не


обрабатывается. Два варианта программы, приведённЫе в примере I0.5.5.I, используют наш класс OurWindowAdapter адаптера окна, наследующий класс WindowAdapter адаптера, и анонимный класс. Пример 10.5.5.1. Обработка события закрытия окна. ///////////////

// J a v a и J # Вариант 9 import java.awt.*; import java.awt.event.*;

// Класс прикладного окна class MainClass extends Frame

{

public MalnClass ( ) { this.setSlze (400, 200); thls.setTltle ("Hello"); // Примененить внутренний анонимный класс thls.addMouseLlstener (new MouseAdapter ( ) { public void mousePressed (MouseEvent mE) {

»;

>

System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE.getY ( ));

thls.addWindowLlstener ( n e w O u r W i n d o w A d a p t e r ());

} public static vold maln ( ) || Главная функция {

>

>

MainClass mC= new MainClass ( ) ; mC.show ( ) ;

// Класс адаптера окна class O u r W i n d o w A d a p t e r extends W i n d o w A d a p t e r < public v o i d w i n d o w C l o s i n g {WindowEvent w E ) {

>

>

System.exit (О);

///////////////

IIJava и J# Вариант 10 Importjava.awt.*; Import java.awt.event.*; II Класс прикладного окна classMainClassextendsFrame

{


public MainClass ( ) { this.setSlze (400, 200); this.setTltle ("Hello"); // Примененить в н у т р е н н и й а н о н и м н ы й класс th!s.addMouseLlstener ( n e w M o u s e A d a p t e r ( ) { public v o i d m o u s e P r e s s e d ( M o u s e E v e n t mE) { System.out.println ( " M o u s e : x= " + mE.getX ( )

»;

>

Л. М Е ЯА»УГ тl II у.,— - Чт, iiii..g«i у \и,

// Примененить в н у т р е н н и й а н о н и м н ы й класс thls.addWindowListener ( n e w W i n d o w A d a p t e r ( ) { public v o i d w i n d o w C l o s i n g ( W i n d o w E v e n t w E ) {

»;

>

System.exlt(0);

public static void maln ( ) // Главная функция {

>

MainClass mC= new MainClass ( ) ; mC.show ( );

> /* Result: При каждом нажатии на клавишу мыши в прикладном окне выдаётся на консоль строка вида Mouse: x= 10 y= 55, где x, у - координаты носика мыши При н а ж а т и и к н о п к и з а к р ы т и я о к н а п р и к л а д н о е о к н о исчезает */

Для закрьгтия окна в варианте 9 программы примера 10.5.5.1 создан свой класс OurWindowAdapter адаптера, наследующий класс WindowAdapter адаптера окна. Объект класса OurWindowAdapter передан функции addWindowListener() для подписки на собьггие окна. Вариант 10 программы применяет анонимные класс WindowAdapter адаптера окна и класс MouseAdapter адаптера мыши.

10.6. Графика Рассмотрим классы .NET Framework, обеспечивающие рисование и вывод рисунков в область клиента окон (форм) и соответствующие им классы биб-


лиотеки языка Java. Множество этих классов представляют графический интерфейс устройства GDI+ + (Graphic Device Interface).

10.6.1. Графические объекты Графический интерфейс использует для рисования такие объекты как цвет (color), перья {pens), кисти (brushes), изображения (images) и шрифты (fonts). Прежде чем рисовать фигуру или линию, надо создать графический объект класса Graphics. Графический объект связан с конкретным окном (формой, фреймом) и его функции выполняют рисование в этом окне, включая при необходимости в качестве аргументов перья, кисти, шрифты и т.д. Таким образом, вспомогательные объекты типа Color, Pen, SolidBrush, Image и Font обеспечивают функционирование объектов класса Graphics, занимающих центральное место в создании графического интерфейса. Объекты вспомогательных типов хранят необходимую для рисования информацию, а графический объект её использует. При рисовании необходимо связать графический объект с контекстом устройства, на которое отображается информация. Обычно им является дисплей или принтер. Может выводиться не только текстовая информация, но и геометрические фигуры, залитые цветом, и изображения. Объект типа Graphics, например, на языках C# и C++/CLI связывается с устройством с помощью функции CreateGraphics(), а в языке Java(J#) - getGraphics().

Точка C# и C++/CLI. Наиболее часто употребляемый тип - это тип Point, используемый для создания объекта - одиночной точки, обычно в области клиента окна. Тип Point содержит открытые свойства - X и Y , определяющие координаты точки, конструктор, функцию Offset(dx, dy) смещения точки с приращениями dx и dy, операторы, позволяющие сравнивать объекты-точки и присваивать им значения. Тип Point имеет конструктор следующего формата: Point (int X, intY);

Нижеследующие примеры понятны без пояснений: C++/CLI. Point *pPl= new Point (1, 2); // Создать в неуправляемой куче // точку с pPl->X= 1 и pPl->Y= 2 p P l -> Offset (1, 1); // Сдвинуть точку p P l // pPl->X= 2 и pPl->Y= 3


Point

p2 (3, 4);

// Создать точку p2 в стеке // с p2.X= 3 и p2.Y= 4 p2.0ffset (5, 5); // Сдвинуть точку p2 // p2.X= 8 и p2.Y= 9 if (p2 != *pPl) p2= *pPl; // Неравные точки стали одинаковыми Point ^pP3= gcnew Polnt (10, 20); // Создать точку в управляемой куче // с pP3->X= 10 и pP3->Y= 20 If (p2 != *pP3) p2= *pP3; // Неравные точки стали одинаковыми C#. Point pl= n e w Pcint (1, 2); pl.Offset (1, 10); Point p2= new Point ( ); p2.X= 30; p2.Y=50;

// Создать точку р1 с pl.X= 1 и pl.Y= 2 // pl.X= 2 и pl.Y= 12 // Создать точку p2 с p2.X= 0 и p2.Y= 0

Java. Точки являются объектами класса Point. Конструктор класса Point имеет формат: Point (int X, intY);

Класс Point включает свойства x и у. Point p= new Point (10, 20 ); // Создать точку p2 с p2.X= 10 и p2.Y= 20 p.x= 30; p.y=50;

Прямоугольник C# и C++/CLI. Также часто употребляется тип Rectangle, используемый для создания объекта-прямоугольника с горизонтальными и вертикальными сторонами. Этот прямоугольник можно применять для рисования его пером (pen) или как область, подлежащую заливке некоторым цветом с помощью кисти (brush), или для указания области, занимаемой окном, и так далее. Тип Rectangle содержит одиннадцать свойств и много функций. Свойство Location, включающее координаты левого верхнего угла (X и Y или Left и Тор), определяет положение прямоугольника в окне или на экране, свойства Width и Height задают ширину и высоту прямоугольника, а свойства Right и Bottom - координаты правой нижней точки прямоугольника Из множества функций нам понадобятся следующие: конструктор Rectangle , функция Offset() смещения, и функция Contains() выявления вхождения точки в прямоугольник. Они понятны из нижеследующих примеров:

C++/CLI. Rectangle rl; // Создать прямоугольник в стеке rl.X= 1; rl,Y=2; rl.Width= 10; rl.Height= 20; Rectangle *r2= new Rectangle (21, 22, 210, 220); // Создать прямоугольник / / в управляемой куче

r2->Offset (100, 100);


Point ^p= gcnew Point (3, 4); if (rl.Contalns (*p)) rl=*r2;

// Создать точку p2 c p2.X= 3 и p2.Y= 4 // Если p2 в r l , то прямоугольник г1 // становится равным прямоугольнику r2 // rl.X= 121 rl.Y= 122 rl.Width= 210 rl.Height= 220;

C#.

Rectangle r l = new Rectangle (1, 2, 4, 5); // rl.X= 1, rl.Y= 2, // rl.Left= 1, rl.Top= 2, rl.Right= 5, rl.Bottom= 7, r l , Width = 4, r l . Height = 5 rl.Offset (2, 1); // Теперь rl.X= 3, rl.Y= 3, // rl.Left= 3, rl.Top= 3, rl.Right= 6, rl.Bottom= 8, r l . Width = 4, r l . Height = 5 Point p2= new Point (3, 4): // Создать точку p2 c p2.X= 3 и p2.Y= 4 if (rl.Contains (p2)) { что-либо делаем>; // Если p2 в r l , то что-либо делаем

Java. Прямоугольники представляются объектами класса Rectangle. Конструктор класса Rectangle имеет различные форматы : Rectangle (); Rectangle (lnt x, lnt у, int w, int h ); Rectangle (Point p, Dimension d ); где x, у - координаты верхнего левого угла прямоугольника; w, h - ширина и высота прямоугольника; d - ссылка на объект класса Dimension, задающий размеры прямоугольника.

Среди свойств класса Rectangle выделим свойства x и у, определяющие координаты левого верхнего угла прямоугольника, свойства width и height, определяющие ширину и высоту прямоугольника.

Цвет C# и C++/CLI. Типом Color представляется цвет. Среди структуры Color имеется конструктор

конструкторов

Color (int tr, int r, int g, int b);

который создаёт объект с указанной прозрачностью tr (transparency of а color) и основными составляющими цвета: красный r (red), зелёный g (green) и синий b (blue). Значения прозрачности и составляющих цвета лежат в границах от 0 до 255. Большее их значение означает более плотный цвет и меньшую прозрачность. Структура Color имеет огромный набор предопределённых цветов, представленных свойствами, например: Brown, Chocolate, Gold, Gray и т.д. Имеется статическая функция FromArgb(), позволяющая получить цвет по значениям трёх составляющих цвета и его интенсивности: static Color FromArgb (int tr, int r, int g, int b);

Она создаёт структуру Color из компонентов ARGB цвета: прозрачности tr и основных цветов - красного r, зелёного g и синего b.


А другая перегруженная статическая функция static Color FromArgb (lnt r, int g, int b);

создаёт структуру Color из компонентов ARGB цвета: прозрачности tr= 255 (непрозрачный цвет) и основных цветов - красного r, зелёного g и синего b. Примеры: C++/CLI. Color со! (System::Drawing::Co!or::Go!d); Color c o l l (Color::Gold); Color col2 (Color::FromArgb (0, 0, 255)); Color col3; col3= Color::FromArgb (0, 0, 255); Color col4; col4= Color::Gold; Color col5 (Color::FromArgb (100, 0, 0, 250));

// Золотой цвет // Золотой цвет // Синий непрозрачный цвет // Синий цвет // Золотой цвет // Синий полупрозрачный цвет

C#. Color col3; col3= Color.FromArgb (0, 0, 255); Color col4; col4= Color.Gold; Color col5= Color.Silver;

// Синий цвет // Золотой цвет // Серебренный цвет

Java. Цвет является объектом класса Color. Конструктор класса Color имеет формат: Color (int r, int g, int b); Color c= new Color (0, 200, 0); Color cl=c; Color c2= new Color (0, 0, 255); c2= c l ; c l = Color.yellow;

// с - синий цвет // с1 - тоже синий цвет // c2 - зелёный цвет // c2 - теперь синий цвет // c l - теперь жёлтый цвет

Класс Pen C# и C++/CLI. Управляемый класс Pen используется для создания объектовперьев (pens), определяющих стиль, толщину и цвет линий для рисования линий и кривых. Конструкторы класса Pen Pen Pen Pen Pen

(Color color); (Color color, float width)); ( Brush ^brush, float width, )); ( Brush brush, float width, ));

// C++/CLI и C# // C++/CLI и C# // C++/CLI // C#

создают перья с заданным цветом color, шириной width (по умолчанию ширина равна 1). Цвет coIor может быть задействован из кисти brush - объекта класса Brush (Brush - базовый класс для кистей).


Класс Pen имеет ряд свойств, например, Color для установки или получения цвета пера и Width для установки и получения ширины пера. Объекты класса Pen широко применяются в качестве аргументов функций рисования класса Graphics, например, в функции DrawEliipse(). Создать и модифицировать перо можно следующим образом: C++/CLI Pen ^ p P e n l = gcnew Pen (Color::Black); p P e n l -> Color= Color::Green; - П п „ 4 J j r C I I X

^ *^

Ш М И ч _ *VIUl*l~

С. h/,

// Создать чёрное перо с шириной 1 // Перо стало зелёным 11 iл r~ n n , n i . u A M lt " ^ ш..^.шыг1,

п э п и л й f/MUNV.1

С J

Pen ^pPen2= gcnew Pen (Color::Green, 5);// Создать зелёное перо с шириной 5 Pen ^pPen3= gcnew Pen (gcnew SolidBrush(Color::Green), 5); // Создать // зелёное перо с шириной 5 Color color= Color::Red; Pen ^pPen4= gcnew Pen (color, 5);

// Создать красное перо с шириной 5

C#.

Pen p e n l = new Реп (Color.Black); penl.Color= Color.Green; penl.Width= 5; Pen pen2= new Pen (Color.Green, 5);

// Создано чёрное перо с шириной 1 // Перо стало зелёным // и с шириной, равной 5 // Создать зелёное перо с шириной 5

Класс имеет много свойств и функций. Java. В языке Java перо не используется.

Класс SolidBrush C++/CLI и C#. Управляемый класс SolidBrush, порождённый из базового класса Brush, используется для создания объектов-кистей, применяемых при закрашивании фона окон или внутри фигур. Конструктор класса SolidBrush

(Color

colorJ;

имеет единственный параметр color типа Color, определяющий цвет созданной кисти. Класс SolidBrush имеет свойство Color, позволяющее установить и получить цвет кисти: C++/CLI. SolidBrush ^pBrush= gcnew SolidBrush (Color::Black); / / Создать чёрную кйсть pBrush -> Color= Color::Red; // и сделать красной Pen ^pPen5= gcnew Pen (pBrush, 5); // Создать красное nepo с шириной 5 C#. SolidBrush brush= new SolidBrush (Color.Black); // Создать чёрную кисть brush.Color= Color.Red; // и сделать красной Java. В языке Java кисть не используется.


Класс Font Объектами класса Font представляются шрифты с указанными названиями, размерами и стилями. Класс Font содержит перегруженные конструкторы и, в частности: C++/CLI. Font (String

^name, float size); Font (String ^name, float size, FontStyle style);

C#. Font (string name, float slze); Font (string name, float slze, FontStyle style);

Java. Font (String name, int style, int slze); где

name - название шрифта, size - размер шрифта, style - стиль шрифта ( Bold -жирный, Italic— курсив и др.)

Шрифт можно задать так:

C++/CLI. Font ^pFont= gcnew Font ("Verdana", 15);

C#. Font font— new Font ("Ariel", 20, FontStyle.Italic);

Java.

Font f l = new Font ("Courier", Font.BOLD, 50);

Замечание: Обратите внимание на отличия в списке аргументов конструкторов языков C# (C++/CLI) и Java.

Класс Graphics C++/CLI и C#. Объект класса Graphics является контекстом рисования, связанным с конкретным контекстом устройства и чаще всего с окном (формой). Класс Graphics предоставляет множество функций для рисования в этом окне строк (strings), прямоугольников (rectangles), эллипсов (ellipses) и т.д. Например: C++/CLI.

vold DrawString (String ^pS, Font ^pF, Brush Л рВ, float x, float у); void DrawRectangle (Pen ^pP, Rectangle r);


void FillE!lipse (Brush л р В , Rectangle r); C#. void DrawString (string s, Font f, Brush b, float x, float y); void DrawRectangle (Pen p, Rectangle r); void FIIIEIItpse (Brush b, Rectangle r);

Можно создать графический объект класса Graphics непосредственно, используя функцию CreateGraphics(). Graphics granhics= CreateGrapMcs ( };

Но чаще всего используют параметр функции перерисовки OnPaint(). Это параметр типа структуры PaintEventArgs, которая одним из своих элементов содержит свойство Graphics. C#. Graphics gr= this.CreateGraphics ( ); // Непосредственное использование gr.DrawStrlng ("Drawing", Font, new SolidBrush (Color.Blue), 10, 10);

C++/CLI.

protected: virtual void OnPaint (PaintEventArgs ^arg) override // Использование через // аргумент arg

{

>

Color col (Color::FromArgb (250, 0, 250, 250)); Drawing::Font ^pFont=gcnewDrawlng::Font("Verdana",20, FontStyle: :Italic); arg ->Graphics ->DrawStrlng ("Drawing", pFont, gcnew SolidBrush (col), 10, 30);

Java. Язык Java использует пакет AWT, который содержит множество графических классов и функций. Среди классов есть и класс Graphics, содержащий, например, такие функции рисования как void void void void void vold void void

drawLine (int x l , lnt y l , lnt x2, lnt y2); drawOval (lnt x, lnty, int w, int h); drawRect (int x, int у, lnt w, int h); drawString (String str, lnt x, int у); fillOval (int x, lnty, lnt w, int h); fillRect (int x, int у, int w, int h); setColor (Color color); setFont (Font font);

// Рисовать линию // Рисовать овал (эллипс) // Рисовать прямоугольник // Рисовать строку // Залить овал (эллипс) цветом // Залить прямоугольник цветом // Установить цвет // Установить шрифт

На языке Java также можно создать графический объект с помощью функции getGraphics() и затем воспользоваться функциями рисования, но рисование обычно осуществляется в теле предопределённой функции paint(), аргумент которой является ссылкой на объект графического класса Graphics. Java. public void paint (Graphics g)

{

// Использование через аргумент arg


>

g.setColor(Color.magenta); Font font— new Font ("Verdana", Font.rTALIC, 30); g.setFont(font); g.drawString ("Drawing", 10, 70);

Пример 10.6.1 иллюстрирует применение графики на языках C#, C++/CLI и Java(J#). В программах на языках C# и C++/CLI использовано пространство имён System.Drawing (System::Drawing), включающее графические классы. Прикладное окно программы представлено на рис. 10.6.1. Программа на языке Java(J#) применяет naKeTjava.awt. Пример 10.6.1. Использование графического объекта. //////////////////// //C# using System; using System.Drawing; using System,Windows.Forms;

class MainClass : Form

{

public MainClass ( ) { Text= "Drawing"; // Заголовок прикладного окна S h o w ( ); // Показать окно Graphics gr= this.CreateGraphlcs ( ) ; // Создать графический объект gr.DrawString ("Drawing", Font, // Нарисовать строку new SolidBrush (Color.Blue), 10, 50); gr.DrawString ("Drawing", new Font ("Arial", 25), // Нарис-ть строку new SolidBrush (Color.FromArgb (100, 255, 0, 0)), 5, 40); String drawStr = "Drawing"; // Создать строку Font drawFnt = new Font ("AriaF, 30);// Создать шрифт SolidBrush drawBr = new SolidBrush (Color.Green);// и кисть float x= 10.0F; // Координаты начала строки float y= 40.0F; gr.DrawString (drawStr, drawFnt, drawBr, x, у); // Нарис-ть строку

>

gr.DrawEllipse (new Pen(new SolidBrush (Color.DarkViolet), 2), 15, 15, 60, 70); // Нарисовать эллипс

static void Main ( ) {

>

>

Appllcation.Run (new MainClass ( ));

//////////////////// // C + + / C L I #include "stdafx.h" #using <System.Drawing.dll>

// Запустить программу


#using <System.Windows.Forms.dll> using namespace System; using namespace System::Drawing; using namespace System: :Windows:: Forms; ref class MalnClass : Form

{

public: MalnClass ( )

{

Text= "Drawing"; // Заголовок прикладного окна ShcVi ( ); // Пскэззть окно Graphics ^gr= this->CreateGraphlcs ( ); // Создать графический // объект // Нарисовать строки gr->DrawString ("Drawing", Font, gcnew SolidBrush (Color::Blue), 10, 50);

>;

>

vold maln {

>

gr->DrawString ("Drawing", gcnew Drawing::Font ("Arlal", 25), gcnew SolidBrush (Color::FromArgb (100, 255, 0, 0)), 5, 40); String ^drawStr = "Drawing"; Drawing::Font ^drawFnt = gcnew Drawing::Font ("Arial", 30); SolidBrush ^drawBr = gcnew SolidBrush (Coior::Green); float x= 10.0F; float y= 40.0F; gr->DrawString (drawStr, drawFnt, drawBr, x, y); // Нарисовать эллипс gr->DrawEI/ipse (gcnew Pen (gcnew SolidBrush (Color::DarkViolet), 2), 15, 5, 60, 70);

()

Appllcatlon::Run (gcnew MainClass ( ));

//////////////////// // J a v a и J # lmportjava.awt.*; class MainClass extends Frame { public MainClass ( ) { this.setTitle ("Drawing"); // Заголовок прикладного окна setSize (300, 200); show ( ); // Показать окно Graphics gr= this.getGraphics ( ); // Создать графический объект // Нарисовать строку gr.setColor (Color.blue); gr.drawString ("Drawing",10, 80); // Нарисовать ещё строку gr.setFont(new Font ("Arial", Font.BOLD,25)); gr.drawString ("Drawing", 5, 70);


// Нарисовать еще строку String drawStr = "Drawing"; // Создать строку gr.setFont(new Font ("Arlal", Font.BOLD, 30)); gr.setColor (Color.green); int x= 10; // Координаты начала строки int y= 40; gr.drawStrlng (drawStr, x, у); // Нарис-ть строку gr.setColor (Color.darkGray); gr.drawOval (15, 45, 60, 100); // Нарисовать эллипс

public static void main ( )

{

>

MainClass m= new MainClass ( );

Рис.10.6.1. Прикладное окно с графической информацией

10.7. Событие Paint и его обработчик Если прикладное окно программы примера 10.6.1 свернуть, а потом развернуть или же изменить его размер, то представленная в нём информация исчезнет. Дело в том, что операционная система Windows не заботится о восстановлении содержимого окна при его разрушении и возлагает его восстановление на само Windows-приложение, то есть на программиста. При всяком разрушении окна операционная CHCTeMaWindows посылает приложению событие Paint, и приложение реагирует на него предопределённой функцией-обработчиком OnPaint() в языках C# и C++/CLI или функциейобработчиком paint() в языке Java(J#), пустых по умолчанию, то есть ничего не отображающих в окне. Переопределив функцию OnPaint() или paint(), приложение будет рисовать в области клиента окна что-то в соответствии с её реализацией. В описание переопределённой функции на языке C# и C++/CLI надлежит поместить ключевое слово override. Функции Paint() и paint() относятся к виртуальным функциям, о которых подробно рассказано в разделе 13.1. Перерисовку окна может осуществить и программист, вызвав специальную функцию Invalidate() в языках C# и C++/CLI или функцию repaint() в языке Java(J#). Вызов этих функций генерирует собьггие Paint.


Итак, чтобы заставить перерисовываться область клиента окна, необходимо только переопределить функцию перерисовки, имеющую формат: C++/CLI. protected: virtual

C#.

protected

v o i d OnPaint (PaintEventArgs

А

е ) override;

override v o i d OnPalnt (PaintEventArgs e);

Функция реагирует на событие, определённое как C++/CLI.

C#.

public: event PaintEventHandler

^Paint;

public

Paint;

event PaintEventHandler

Событие Paint использует делегат: C++/CLI. public: delegate void PaintEventHandler (Object ^sender, PaintEventArgs

C#.

Л

е);

public d e l e g a t e void PaintEventHandler (object sender, PaintEventArgs e);

Для языка Java(J#) функция перерисовки имеет формат: Java(J#). public void paint (Graphics g);

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

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

Рис.10.7.1. Прикладное окно программы рисования ломаной линии

мыши. носика носика мыши. 10.7.1.


Ниже приводится программа; /////////////// //C# using System; using System.Drawing; using System.Windows.Forms; class CLine : Form

{

Point [ ] points; int len;

// Класс прикладного окна // Массив точек носика курсора мыши // Текущая длина массива points

public CLine( )

{

>

// Конструктор

// Привязать к событию MouseDown функцию OurMouseDown MouseDown += new MouseEventHandler (OurMouseDown); Text= "Line"; // Заголовок прикладного окна points= new Point [20]; // Создать массив polnts в куче len= 0; // Инициализировать len

// Перерисовать область клиента окна protected override void OnPaint (PaintEventArgs arg)

{

base.OnPaint (arg); for (int i=0; i < len-1; i++) // Перебрать массив точек arg.Graphics.DrawLine // Нарисовать отрезок линии (new Pen (Color.Blue), points[i], points[i+l]);

// Обработать событие MouseDown мыши void OurMouseDown (object sender, MouseEventArgs arg) { // Реагировать только на левую клавишу мыши if (arg.Button == MouseButtons.Right) return; If (len < 20) // { // points[len].X= arg.X; // points[len].Y= arg.Y; // len++;

>

Invalidate ( );

static void Main ( )

{

Если есть место в массиве polnts, то поместить в массив точку Координата X носика курсора мыши Координата Y носика курсора мыши

// Перерисовать

// Главная функция

Application.Run (new CLine ( ));

// Выполнить приложение


шишшшшш

// C + + / C L I *inciude "stdafx,h" #using <System.Drawing,dll> #using <System.Windows.Forms.dll> using namespace System; using namespace System::Drawing; using namespace System::Wlndows::Forms; ref class CLine : Form

// Класс прикладного окна

/

array<Point^> ^points; int len; public: CLine()

// Конструктор

{

>

// Массив точек носика курсора мыши // Текущая длина массива points

// Привязать к событию MouseDown функцию OurMouseDown MouseDown += gcnew MouseEventHandler (this, &CLine::OurMouseDown); Text= "Line"; // Заголовок прикладного окна points= gcnew array<Point^> (20); // Создать массив points в куче len= 0; // Инициализировать len

protected: // Перерисовать область клиента окна virtual void OnPaint (PaintEventArgs ^arg) override { Form: :OnPaint (arg); for (int i=0; i < len-1; i++) // Перебрать массив точек arg->Graphics->DrawLine // Нарисовать отрезок линии (gcnew Pen (Color::Blue), *points[i], *points[i+l]);

} // Обработать событие MouseDown мыши void OurMouseDown (Object ^sender, MouseEventArgs ^arg) { if (len < 20)

// Если есть место в массиве points, то

{

> >;

>

polnts[len]=gcnew Point (arg->X, arg->Y); // поместить в массив len++;

Invalidate ( );

void main ( ) { Application:: Run (gcnew CLine ( ));

} /////////////// // J a v a и J # import java.awt.*;

// Перерисовать


import java.awt.event,*; class CLine extends Frame

// Класс прикладного окна

{ Point [ ] points; int len;

// Массив точек носика курсора мыш» // Текущая длина массива polnts

public C U n e ( )

// Конструктор

{

setTitle ("Llne");

// Установить заголовок прикладного <

setSize (400, 200); // Установить размер окна //this.setBackground (Color.lightGray); points= new Point [20]; // Создать массив po/nts точек в куче len= 0; // Инициализировать len // Реализовать обработчик события мыши, применив // внутренний анонимный класс this.addMouseListener (new MouseAdapter ( )

{

public void mousePressed (MouseEvent mE) { | | Реагировать только на левую клавишу мыши if((mE.getModifiers()&Event.META_MASK)>0) return; if (len < 20) // Если есть место в массиве points { p o i n t s [ l e n ] = m E . g e t P o i n t (); / / Координаты U носика курсора мыши leri++;

»;

>

>

У repaint ( );

thls.addWindowListener (new OurWindowAdapter());

// Перерисовать область клиента окна public void paint (Graphics g) { g.setColor (Color.blue); for (int i=0; i < points.length-l; I++) / / Перебрать массив точек { g.drawLine( / / Нарисовать отрезок points[i].x, points[().y ,polnt5[i+l].x, points[i+l].y);

>

У

public static void main ( )

{

>

>

CLIne line= new CLIne ( ); line.show ( );

// Класс а д а п т е р а

окна

// Главная функция

линии


class OurWindowAdapter extends WindowAdapter

{

public void windowClosing (WindowEvent wE)

{

>

>

System.exit (0);

C#. Класс CLine прикладного окна, порождённый из базового класса Form, содержит закрытый массив точек points, соответствующих координатам носика мыши нажатых клавиш, и текущую длину ien этого массива. При создании объекта-окна конструктор инициализирует данные класса CLine, создав массив points из 20 точек в куче и присвоив его текущей длине len значение 0. Кроме этого конструктор подписывает обработчик OurMouseDown() на событие MouseDown клавиши мыши и посредством свойства Text формы определяет заголовок "Line" прикладного окна. Рассмотрим работу приложения. После запуска приложения появилось прикладное окно. При создании окна конструктор класса Cline, создав массив из 20 точек, присвоит длине массива len начальное значение и подпишет обработчик OurMouseDown() на событие MouseDown. При появлении окна creнерируется событие Paint и, как следствие, выполнится наш переопределённый обработчик OnPaint(), который в виду нулевого значения переменной len не рисует ломаную линию в области клиента, Таким образом, при появлении прикладного окна его область клиента пуста. Приложение ждёт события MouseDown от нажатия левой клавиши мыши и события от закрытия прикладного окна. При нажатии левой клавиши мыши выполнится обработчик OurMouseDown(). Обработчик OurMouseDown(), воспользовавшись параметром arg типа MouseEventArgs, извлекает значения координат носика курсора нажатоЙ клавиши мыши и присваивает их текущему элементу массива points точек. Затем продвигается индекс len массива. Если индекс len выходит за границы массива, то пополнения массива новыми точками не будет. Перед завершением обработчик OurMouseDown() вызывает функцию Invalidate(), которая генерирует событие Paint, обеспечивая тем самым выполнение функции OnPaint(). Ломаная линия перерисовывается в окне. Функция OnPaint() использует графический объект класса Graphics, извлекая его из структуры PaintEventArgs с помощью свойства Graphics и обратившись к функции DrawLine() рисования линии. В качестве аргумента функция DrawLine() использует объект-перо синего цвета. Ломаная линия рисуется циклически, при этом в каждом цикле рисуется только один отрезок линии синего цвета. C++/CLI. Программа на языке C++/CLI схожа с программой на языке C#, поскольку применяются те же классы библиотеки .NET Framework. Обращает на себя внимание иное описание и использование массива точек. В управляемом классе CLine применён управляемый массив points, который описан с применением слова array и является массивом дескрипторов на объекты то-


чек, размещенных в управляемой куче. При нажатии на клавишу мыши обработчик присваивает очередному дескриптору массива ссылку на созданный в куче объект точки, содержащей координаты носика мыши. А функция paint() перерисовки использует через дескриптор эти объекты при рисовании ломаной линии. Обратите внимание, что заголовок функции OnPaint() языка C++/CLI отличается от заголовка функции OnPaint() языка C#: используется ключевые слово virtual, а слово override стоит в конце заголовка. Java. Так же, как и в программе на C#, программа на языке Java использует графический класс Graphics в предопределённой функции перерисовки, которая называется paint(). Функция рисования линии drawLine() не содержит объект пера, как в соответствующей функции DrawLine() библиотеки .NET Framework. Цвет пера устанавливается с помощью свойства setColor(). Отлично от программы языка C# реализуется и обработка собьггия мыши, в соответствии с одним из вариантов, изложенных в разделе 10.5. Вместо функции Invalidate() языка C# применена функция repaint(). Эти функции инициируют появление события Paint. Использованию различных классов позволило их импортирование с помощью lmportjava.awt.*; import java.awt.event.*;

10.8. Управляющие элементы C# и C++/CLI. Управляющие элементы составляют основу при формировании пользовательского интерфейса приложения. Библиотека NET. Framework и пакеты языка Java включают десятки классов, используемых для создания объектов управления, а из них - элементов управления (controls). Элементы управления, такие как нажимаемые или селективные кнопки, панели списков, панели редактирования и другие, широко используются разработчиками оконных приложений. Они значительно облегчают интерфейс между пользователем и приложением. В библиотеке NET. Framework все классы управляющих элементов порождаются из общего базового класса Control и поэтому позволяют однообразное их использование. Имеется специальный класс ControlCollection, обеспечивающий объединение управляющих элементов в одной коллекции. Для связи управляющих элементов с конкретной коллекцией (т.е. объектом класса ControlCollection) используется свойство Controls управляющих элементов, наследуемое из класса Control. Получив посредством свойства Controls ссылку на коллекцию, можно воспользоваться его функциями (функциями класса ControlCollection), чтобы конкретный управляющий элемент добавить (Add) или удалить (Remove) из коллекции, или удалить все элементы (Clear) из коллекции, или же выявить (Contains) наличие указанного элемента в коллекции.


Из множества классов управляющих элементов библиотеки NET. Framework мы рассмотрим классы Button и TextBox. Из класса Button создаётся так называемая нажимаемая кнопка. Из класса TextBox создаётся панель редактирования, которая представляет собой простейший редактор с возможностью ввода и редактирования текста. Java. В языке Java классы управляющих интерфейсных элементов порождаются из класса Component, содержащего их общие свойства и функции. Кнопка и панель редактирования создаются из классов Button и TextFicld. Эти и другие управляющие элементы добавляются в контейнер окна посредством функции add(). Рис.10.8 1 и 10.8.2 иллюстрируют наследование базовых классов классами Button, TextBox и TextField.

I +TgxtBqy |

t>bTwffln;flaa&J

^

I +B"tton I fc| +ButtonBase 1 И •Control ;—Й+СдтндпвпД 1 *Oblect Й

l+MarshalflvRefObiBct К]

Рис. 10.8.1. Наследование базовых классов классами TextBox и Button в C# и C++/CLI

I +TextField~b—fcH+TextCnmnnnent I fcH+Conwonent I 1 «Button j

D>l +Qbiect 1

1

Рис. 10.8.2. Наследование базовых классов классами TextFieId и Button в Java Позднее будут рассмотрены ещё классы UserControl и Panel, используемые для получения интересного интерфейсного элемента, представляющего в прикладном окне полотно-окно с богатой функциональностью.

Класс Button C# и C++/CL1. Объект класса Button представляет нажимаемую кнопку. Объект создаётся конструктором: Button ();

В классе Control библиотеки .NET Framework среди более 50 собьггий объявлено событие Click:


C#. public e v e n t EventHandler Click;

C++.NET. public: e v e n t EventHandler^ Click;

На это событие мы будем подписывать обработчик, реагирующий на нажатие кнопки. При применении класса Button, кроме его собственных свойств, можно воспользоваться наследуемыми свойствами класса Controi. Наследуемое свойство Text, например, позволяет поместить текст в кнопку, а свойства Size и Location - определить размеры кнопки и координаты её верхнего левого угла. Обратите внимание, что делегатом события кнопки является делегат EventHandler, определённый в библиотеке .NET Framework для различных событий. Об этом делегате, рекомендованном фирмой Microsoft для упорядочивания применения собьггий, упоминалось в разделе 8.5, а в примере 8.5.6 иллюстрировалось его применение. Java. Добавление к кнопке предопределённого обработчика actionPerformedO класса ActionListener осуществляет функция addActionListener() класса Button. К кнопке можно применить множество свойств, наследуемых из класса Component, например, setLocation() и getLocation() при размещении кнопки и setSize() для установки размера.

Классы TextBox и TextField C# и C++/CLI. Объектом класса TextBox представляется поле ввода информации, являющееся простейшим редактором текста. Объект создаётся конструктором: TextBox ( );

При применении класса TextBox кроме его собственных свойств можно воспользоваться наследуемыми свойствами Text для помещения и извлечения текста, BackColor для установки и получения цвета фона, Multiline для указания многострочности, TextLength для получения длины текста и так далее. А свойства Size и Location, как и в случае кнопки, позволяют определить размеры панели редактирования и координаты её верхнего левого угла. Java. Для размещения редактора класса TextField в области клиента окна применяются свойства setLocation() и getLocation(), для установки размера setSize(). Имеется ещё много свойств, наследуемых из класса Component.


ПримерЮ.8.1. Программа с управляющими элементами на языке C#. Приложение имеет прикладное окно с кнопкой и редактором текста. В окне рисуется российский трехцветный флаг. Цвета флага - белый, синий и красный - применяются и при рисовании строки, введенной в редактор. Строка рисуется при нажатии на клавишу мыши, начиная с носика курсора мыши. Нажатие на кнопку меняет белый цвет строки циклически на синий, красный, а затем на белый цвет.. Прикладное окно изображено нарис. 10.8.3. //////////////////// nc# using System; using System.Drawing; using System.Windows.Forms; class CTextBox_Button : Form < TextBox ourTextBox; Button ourButton; Point point; Color [] colors; lnt n; public CTextBox_Button()

// Класс прикладного окна // // // // // //

Редактор Кнопка Начальные координаты строки Массив цветов флага Индекс массива цветов colors Конструктор

{

>

Text= "Russian fiag"; // Установить заголовок прикладного окна ourTextBox= new TextBox ( ) ; // Создать редактор ourTextBox.Location= new Polnt (10, 120); // Установить позицию ourTextBox.Size= new S i z e ( 1 5 0 , 2 0 ) ; //Установитьразмер ourTextBox.Text= "Russia"; // Установить текст в редакторе Controls.Add (ourTextBox); // Добавить редактор в форму ourButton= new Button ( ) ; // Создать кнопку ourButton.Location= new Point (170, 10); // Установить позицию ourButton.Slze= new Size (40, 20); // Установить размер ourButton.Text= "OK"; // Установить текст в кнопке Controls.Add (ourButton); // Добавить кнопку к форме // Подписать обработчик на событие Click кнопки ourButton.Cllck += new EventHandler (OurButtonClick); // Подписать обработчик на событие MouseDown мыши MouseDown += new MouseEventHandler (OurMouseDown); n= 0; point= new Polnt (100, 100); colors= new Color [3]; // Создать массив цветов colors [0]= Color.FromArgb (255, 255, 255); // Белый цвет colors [1]= Color.FromArgb (0, 0, 255); // Синий цвет colors [2]= Color.FromArgb (255, 0, 0); // Красный цвет

// Перерисовать область клиента окна protected override void OnPalnt (PaintEventArgs {

arg)

base.OnPaint (arg); Rectangle rect= new Rectangle (10, 10, 130, 30); for(int i= 0; i < 3; i++) // Нарисовать флаг


{

>

}

arg.Graphlcs.FIIIRectangle // Нарисовать прямоугольник (new SolidBrush (colors [i]), rect); rect.Offset (0, 30); // Переместить прямоугольник вниз

// Нарисовать строку от носика курсора мыши arg.Graphics.DrawString (ourTextBox.Text, new Font ("Arial", 16), new SolidBrush (colors [n]), point);

// Обработать событие MouseDown мыши void OurMouseDown (object sender, MouseEventArgs arg) {

>

point.X= arg.X; // Сохранить координату X носика курсора мыши point.Y= arg.Y; // Сохранить координату Y носика курсора мыши Invalidate ( ); // Перерисовать область клиента окна

// Обработать событие Click кнопки vold OurButtonClick (object sender, EventArgs arg) { п++; // К следующему цвету флага if (n >= 3) n= 0; Invalidate ( ) ; // Перерисовать область клиента окна

> static vold Main ( ) {

>

>

// Главная функция

Appllcation.Run (new CTextBox_Button ( )); // Выполнить // приложение

C#. Выполнение приложения начинает функция Main(), которая вызывает функцию Run(), передав ей в качестве аргумента объект класса CTextBox_Button прикладного окна. Прикладное окно с изображенным флагом, кнопкой и редактором появляется на экране. При создании объекта класса CTextBox_Button выполняется конструктор CTextBox_Button(). Вначале конструктор, используя свойство Text объекта окна, присваивает заголовку окна название Russian flag. Затем создаются редактор текста и кнопка. Свойства Location и Size редактора и кнопки размещают их в области клиента окна и устанавливают размеры. Функция Add() включает объекты редактора и кнопки в коллекцию прикладного окна. Если не сделать этого, то окно не будет считать редактор и кнопку своими объектами и не отобразит их в окне. Теперь осуществляется подписка обработчика OurMouseDown() на событие MouseDown мыши: создаётся объект делегата EventHandler и этот объект добавляется к объекту собьггия MouseDown. Затем на событие Click кнопки подписывается её обработчик OurButtonClick().


Перед завершением конструктор создаёт объект point точки для фиксации координат носика мыши, необходимых при рисовании строки функцией OnPaint(). Наконец, создается и инициализируется массив colors цветов. Обработчик OurMouseDown() собьггия MouseDown мыши прост. При каждом нажатии на клавишу мыши точке point присваиваются координаты носика мыши и вызывается функция Invalidate(), которая инициирует выполнение функции перерисовки OnPaint(). функция OnPaint() вначале изображает флаг, трижды рисуя со смешением прямоугольник rect с требуемыми цветами. Используемая при этом функция FillRectangle() закрашивает прямоугольник кистью нужного цвета. Затем функция DrawString() рисует строку, извлечённую из редактора с помощью свойства Text. Обработчик OurButtonClick() кнопки меняет цвет рисуемой строки, изменив индекс n элемента массива colors цветов и вызвав функцию Invalidate(). Функция Invalidate() инициирует перерисовку окна с объектом кисти очередного цвета. Этот объект кисти класса SolidBrush применяет функция DrawString() рисования строки.

номшв

Рис. 10.8.3. Прикладное окно приложения примера 10.8.1 суправляющими элементами Пример 10.8.2. Программа va. //////////////////// // Java и J #

с управляющими элементами на языке Ja-

lmportjava,awt.*; import java.awt,event.*;

class CTextBox_Button extends Frame { TextField ourTextBox; Button ourButton; Point point; Color [] colors; lnt n; public CTextBox_Button ( ) { setTltle ("Russian flag");

// Класс прикладного окна // // // // // //

Редактор Кнопка Начальные координаты строки Массив цветов флага Индекс массива цветов colors Конструктор

// Установить заголовок прикладного окна


setSize (400, 200); this.setBackground (Color.lightGray); setLayout (new FlowLayout ()); ourTextBox= new TextField (); // Создать редактор ourTextBox.setSlze (150, 20); // Установить размер ourTextBox.setText ("Russia"); // Установить текст в редакторе add (ourTextBox); // Добавить редактор в форму ourButton= new Button ("OK"); // Создать кнопку add (ourButton); //Добавить кнопку к форме // Подписать обработчик на событие кнопки чи t«лш АлНлп! icfnnar / \ u l ^l l ^ II r \ ^ U V I IUIJVSal 1^1 У J

<

public void actionPerformed (ActionEvent aE)

»;

>

n++; lf(n>=3)n=0; repaint ();

//Кследующемуцветуфлага

// Подписать обработчик на событие при нажатии на мышь this,addMouseLlstener (new MouseAdapter ( ) { public void mousePressed (MouseEvent mE) <

»;

>

System.out.println ("Mouse: x= " + mE.getX ( ) + " y= " + mE.getY ( )); point.x= mE.getX (); point.y= mE.getY (); repaint ( );

// Примененить внутренний анонимный классдля закрытия окна thls.addWindowListener (new WindowAdapter ( ) { public void wlndowCloslng (WlndowEvent wE) <

»;

>

>

System.exit (0);

n= 0; polnt= new Point ( ); point.x= 100; point.y= 100; colors= new Color [3]; colors [0]= Color.white; colors [1]= Color.blue; colors [2]= Color.red;

// Создать массив цветов // Серый цвет // Синий цвет // Красный цвет

// Перерисовать область клиента окна public vold paint (Graphics g) { Rectangle rect= new Rectangle (10, 40, 130, 30); for (lnt 1= 0; I < 3; i++) // Нарисовать флаг { g,setCoior (colors[i]);


g.flllRect // Нарисовать прямоугольник (rect.x, rect.y, rect.width, rect.height); rect.translate (0, 30); // Переместить прямоугольник вниз

>

// Нарисовать строку от носика курсора мыши g,setColor (colors[n]); g.drawString (ourTextBox.getText ( ), polnt.x, point.y);

public static void maln ( )

<

>

>

CTextBox_Button cT.show ( );

// Главная функция

cT= new CTextBox_Button ( ) ;

Для упрощения размещения управляющих элементов в прикладном окне на языке Java применяются объекты так называемых менеджеров компоновки классов FIowLayout, BorderLayout, CardLayout и GridLayout. Каждый менеджер компоновки располагает управляющие элементы, добавленные в контейнер окна с помощью функции add(), в прикладном окне в соответствии с принятыми в нём правилами размещения. Вызов в конструкторе функции setLayout (new FIowLayout ( ) ) ;

устанавливает менеджер FIowLayout компоновки, который размещает элементы "потоком" от центра, начиная с верхней части окна, что иллюстрирует рис. 10.8.4. Но можно разместить управляющие элементы, применив свойство setLocation(), в любом месте окна, отказавшись от менеджеров компоновки. Для этого в конструкторе вызывается setLayout (null);

Рис. 10.8.4. Прикладное окно приложения примера 10.8.2 с управляющими элементами на языке Java

Пример 10.8.3. C++/CLI. /////////////// // C + + / C L I #include "stdafx.h"

Программа

с управляющими элементами на языкс


#using <System.Drawing.dll> #using <System.Windows.Forms.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing; ref class CTextBox_Button: public Form // Класс прикладного окна { TextBox ^pTextBox; Button ^pButton; n ~ f ~ L

м л

| п 1 .

ruiiit ^uiiiv., array<Color ^> ^colors; int n; public: CTextBox Button ( )

{

>

// Дескриптор редактора // Дескриптор кнопки //

U 9 n 9 n L U L i a

VnAMnilUSVLI

r^nAI/IJ

// начальные координаты строки // Массив цветов // Индекс цвета // Конструктор

// Подписать обработчик на событие MouseDown мыши MouseDown += gcnew MouseEventHandler (this, &CTextBox_Button:: Ou rMouseDow n); Text= "Russian flag"; // Установить заголовок окна n= 0; BackColor= Color::Gold; colors= gcnew array<Color^> (3); //Создатьмассивструктур colors [0]= Color::White; colors [1]= Color::Blue; colors [2]= Color::Red; pTextBox= gcnew TextBox (); // Создать объект редактора pTextBox -> Location= Drawing::Point (10,120); // Разместить pTextBox-> Size= Drawing::Size (150, 20); // Размер pTextBox -> Text= "Russia"; // Поместить текст в редактор Controls -> Add (pTextBox); // Добавить редактор к форме pButton= gcnew Button; // Создать объект кнопки pButton -> Location= Point (170,10); // Разместить pButton -> Size= Drawing::Size (40, 20); // Размер pButton -> Text= "OK"; // Поместить текст в кнопку Controls -> Add (pButton); // Добавить кнопку к форме // Подписать обработчик на событие Click кнопки pButton -> Click += gcnew EventHandler (this, &CTextBox_Button::OurButtonClick);

protected: virtual void OnPaint (PaintEventArgs ^ arg) override {

// Перерисовать область

Form: :OnPalnt (arg); Rectangle rect (10, 10, 130, 30); // Прямоугольник третьей части флага for(int i=0; i < 3; i++) // Нарисовать флаг

< >

arg->Graphics->FillRectangle(gcnew SolidBrush (*colors[i]), rect.X, rect.Y, rect.Width, rect.Helght); rect.Offset (0, 30); // Сместить прямоугольник rect вниз

// Написать строку от координат носика курсора мыши arg -> Graphics -> DrawString (pTextBox ->Text, gcnew Drawing::Font ("Arial", 16),


gcnew SolidBrush (*colors [n]),

point);

void OurMouseDown (Object ^pSender, MouseEventArgs ^arg) {

>

polnt,X= arg - > X; polnt.Y= arg - > Y; Invalidate ( ) ;

/ / Сохранить координату X носика курсора мыши / / Сохранить координату Y курсора мыши / / Перерисовать область клиента окна

/ / Обработать щелчок на кнопке voiu OurButtonCiick (Object *-sender, EventAi'yS < n++; if (n > = 3) n= 0; Invalidate ( ) ;

>;

А

е)

/ / К следующему цвету массива цветов colors / / Перерисовать область клиента окна

>

vold main ( ) {

}

Application::Run (gcnew CTextBox_Button ( ));

/ / Выполнить

C++/CLI. В отличии от программы на языке Java в программе на языке C++/CLI появилась глобальная главная функция main(), операторы #using добавляют требуемые библиотеки. Трансформировалась грамматика - появилось слово ref перед управляемым классом, операторы new заменены на gcnew, появились дескрипторы и стрелки. Иначе получены объекты делегатов собьггий, аргументы конструкторов которых содержат ссылку this на объект-окно, содержащий обработчик, и ссылку на сам обработчик. В заголовке функции OnPaint() появились ключевые слова virtual и override. Пример 10.8.3 программы на языке C++/CLI иллюстрирует некоторые особенности этого языка, которые удивят программистов, знающих предшествующую версию C++,NET. Они, наверняка, обратят внимание на создание и использование массива цветов, присвоение значений свойствам кнопки и редактора, а также на неожиданное появление в заголовке функции OnPaint() после слова OnPaint ключевого слова override, что совершенно не согласуется с принятыми описаниями функций в предшествующем языке C++.NET.


Рис. 10.8.5. Прикладное окно приложения примера 10.8.3 с управляющими элементами на языке C++/CLI

Классы UserControl и Panel Объекты класса UserControl языка C# и объекты класса Panel языка Java своеобразны, поскольку представляют соответствующие управляющие элементы в окне в виде прямоугольных областей, каждая из которых обладает функциональностью окна. То есть в этой прямоугольной области можно выводить графическую информацию, применять свои потоки и собьггия. Так в прикладном окне могут появиться прямоугольные области, самостоятельно функционирующие. Эти области (точнее, эти управляющие элементы) размещаются в окне так же, как и любые другие управляющие элементы - кнопки, редакторы и т.д. Обычно на базе классов UserControl и Panel создаются порождённые классы, в которых, воспользовавшись свойствами управляющего элемента, легко получаются координаты его левой верхней точки, размер элемента и другие свойства, а также многие функции базовых классов UserControl и Panel, включая функции OnPaint() или paint() и Invalidate() или repaint(). Такое необычное применение в порождённом классе свойств, установленных вне управляющего элемента, иллюстрируют программы примеров 10.8.4,10.8.5 и 10.8.6, а также программа, поэтапно разрабатываемая в разделе 11. C# и C++/CLI. Объектом класса UserControl представляется поле, обладающее свойствами и функциональностью окна. Объект создаётся конструктором: UserControl ( );

При применении класса UserControl можно воспользоваться свойствами BackColor, ClientSize, Controls, Height, Location, Size, Visible, Width и други-


ми, наследуемыми из класса Control, событиями Click, MouseDown, MouseUp и множеством других, наследуемых из того же класса Control. Также наследуются известные нам функции OnPaint() и Invalidate(), и масса других. Как видим, управляющий элемент типа UserControl может существенно обогатить интерфейс пользователя с программой. Java. Также как и элемент типа UserControl, управляющий элемент типа Panel содержит сотни свойств и функций, наследуемых из базовых классов. T 3 t т TTt> TTTJ** vUJ^Wdt<iiU)

1 J О Г7 П 1 1 \ Л а ** ilUll^ftiV>v^,

*TOVIJO iutvriw

^ Л А Й Л Т П О WtiuriVIOtt}

Т rr*if iVUA

rtof^lv^«U Iя г А g W I ^ J i a p i l i V ^ y ^

л л * С 5 т л Л g v i ^ l £ . v y / )

getLocation(), и функции paint() и addMouseListener(). Сравните диаграммы классов рисунков 10.3.1.1 и 10.8.6. Сравнение показывает, что функциональность управляющего элемента типа UserControl и окна типа Form схожи. Что же касается управляющего элемента типа Panel и окна типа Frame, то здесь имеется отличие, как явствует из рисунков 10.3.1.2 и 10.8.7.

l+UserContrnl 1 fcH +ScroIIableContro71

D^j +Cnntrol I

I +Obiact kr]

DH+Comnnnent I

1 +MarshalBvRefObiect k ^ _

Рис. 10.8.6. Наследование базовых классов классом UserControl в C# и C++/CLI

I +Panel ~1 fcH+Container [

Й *Cnmnonent I

D>| *Qbiect I

Рис. 10.8.7. Наследование базовых классов классом Panel в Java Пример 10.8.4. Программа с управляющим элементом типа UserControl на языкс C# с рисованием в потоке. Приложение имеет прикладное окно с элементом типа UserControl. В области клиента этого управляющего элемента рабочий поток рисует эллипсы со случайными цветами и размерами, сменяющие друг друга (см. рис.10.8.8).


Рис. 10.8.8. Прикладное окно программы примера 10.8.4 /////////////// //C# using System; using System.Drawlng; using System.Threading; using System.Windows.Forms; // Класс управляющего элемента типа UserControl class User: UserControl { Thread t; bool life; Graphics g; Random rand;

// Ссылка на поток // Признак жизни потока // Ссылка на графический объект // Ссылка на случайное число

// Конструктор public User ( ) {

>

g= this.CreateGraphics (); // Создать граф.объект rand= new Random (); // Создать случайное число // Создать и запустить поток рисования life= true; // Пусть поток живёт t= new Thread (new ThreadStart (F)); // Создать объект t.Start ( ) ; // Запустить поток

// Выполнить рабочий поток private void F ( ) { while(life) { // Нарисовать случайный эллипс в области упр.элемента g.FillEllipse (new SolidBrush (Color.FromArgb (rand.Next(255), rand.Next(255), rand.Next(255))), rand.Next (this.Width), rand.Next (thls.Height), rand.Next (this.Width<this.Height?this.Width:this.Helght), rand.Next (this.Width<thls.Helght?this.Width:this.Height)); // Поспать Thread.Sleep (50);


>

>

// Класс прикладного окна class W: Form

{ User u;

// Ссылка на объект управляющего элемента

//Конструктор

_ . . u l : ~ ,v *u i 1 / ;\ puLfllu

{

>

u= new User( ); // Создать объект управляющего элемента u.Location= new Point (50, 40);// Разместить, u.Size =new Size (140, 130); // установить размер u.BackColor=Color.Coral; // и фон управляющего элемента Controls.Add (u); // Включить управляющий элемент в // коллекцию

// Выполнить основной поток static void Maln ( ) {

>

>

Application.Run (new WQ);

Пример 10.8.5. Программа с управляющим элементом типа UserControl на языке C# с применением потока и функции OnPaint() В области клиента управляющего элемента типа UserControl рабочий поток рисует совместно с функцией Paint() эллипсы со случайными цветами и размерами. Поскольку при рисовании каждого эллипса область клиента управляющего элемента перерисовывается заново, в отличие от примера 10.8.4, в котором текущий эллипс рисуется на предыдущих эллипсах, в примере 10.8.5 каждый рисуемый эллипс сменяет предыдущий.

/////////////// //c# using using using using

System; System.Drawing; System.Threading; System.Windows.Forms;

// Класс управляющего элемента типа UserControl class User: UserControl

{ Thread t; bool life; Graphics g; Random rand;

// Ссылка на поток // Признак жизни потока // Ссылка на графический объект // Ссылка на случайное число

// Конструктор


public User ( )

{

>

g= this.CreateGraphics (); // Создать граф.объект rand= new Random (); // Создать случайное число // Создать и запустить поток рисования llfe= true; // Пусть поток живёт t= new Thread (new ThreadStart(F)); //Создать объект потока t.Start(); //Запуститьпоток

// Выполнить рабочий поток private void F ( ) { while (life) {

>

>

// Перерисовать область клиента управляющего элемента Invalidate (); // Поспать Thread.Sleep (50);

protected override vold OnPaint (PaintEventArgs e) {

>

>

base,OnPaint (e); // Нарисовать случайный эллипс в области упр.элемента e.Graphlcs.FIIIEIIIpse (new SolidBrush(Color.FromArgb(rand.Next(255), rand.Next (255),rand.Next(255))), rand.Next (thls.Width), rand.Next (this.Height), rand.Next (this.Wldth<thls.Height?this.Width:thls.Helght), rand.Next (this.Width<this.Height?this.Width:thls.Height));

// Класс прикладного окна class W: Form { User u;

// Ссылка на объект управляющего элемента

//Конструктор public W ( ) { u= new User ( ) ; // Создать объект управляющего элемента u.Location= new Point (50, 40);// Разместить, u.Size =new Size (140, 130); // установить размер u.BackColor=Color.UghtGray; // и фон управляющего элемента Controls.Add (u); // Включить управляющий элемент в // коллекцию

} // Выполнить основной поток static void Main ( )

{


Appllcation.Run (new W());

Пример 10.8.6. Программа с управляющим элементом типа Panel на языке Java с применением потока и функции paint() Как и в программе примера 10.8.5 в данной программе рисование случайных эллипсов в пределах управляющего элемента типа Panel осуществляется функцией paint(), вызываемой посредством функции repaint() из потока. В области клиента управляющего элемента рисуются эллипсы со случайными координатами, размерами и цветами. /////////////// //J# importjava.awt.*; import java.awt.event.*; importjava.util.*; // Класс управляющего элемента типа UserControl class User extends Panel implements Runnable { Thread t; boolean life; Graphics g; Random rand;

// // // //

Ссылка на поток Признак жизни потока Ссылка на графический объект Ссылка на случайное число

// Конструктор public User ( ) { show(); this.setVisible (true); // Показать управляющий элемент g= this.getGraphics (); // Создать графический объект rand= new Random (); // Создать случайное число // Создать и запустить поток рисования life= true; // Пусть поток живёт t= new Thread (this); // Создать объект потока t.start ( ) ; // Запустить поток

// Завершить поток public void Finish ( ) { life= false; try

< >

>

t.join ( ) ;

catch (InterruptedException e) { >

// Выполнить рабочий поток public void run ( ) {


while (life) { System.out.println (" repaint ( ); // Поспать try

{

>

I'm working");

Thread.sleep (50);

catch (InterruptedException e) { >

> public

{

void paint (Graphics g) g.setColor (new Color (rand.nextInt(255), rand,nextInt(255), rand.nextInt(255))); // Нарисовать случайный эллипс в области упр.элемента g.fillOval ( rand,nextInt (this.getSlze().width), rand.nextInt (this.getSize().height), rand.nextInt (this.getSize().width<this.getSize().height? this.getSize().wldth:this.getSize().height), rand.nextInt (this.getSize().wldth<this.getSize().helght? thls.getSizeQ.width:this.getSizeQ.height));

// Класс прикладного окна class W extends Frame

{

User u;

// Ссылка на объект управляющего элемента

//Конструктор public W ( ) { setLayout(null); setSize (340, 230); // Установить размер u= new User(); // Создать объект управл. элемента u.setLocation(50, 40); // Разместить, u.setSize (140, 130); // установить размер u.setBackground (Color.lightGray); // и фон управляющего // элемента add(u); // Включить управляющий элемент в // коллекцию // Применить внутренний анонимный класс // для закрытия окна this.addWindowListener (new WindowAdapter ( ) { public vold windowClosing (WindowEvent wE)

{

System.exlt (0);


»;

show();

// Выполнить основной поток static public void main ( )

i

>

>

W w= new W ();

10.9. Дочерние окна При необходимости можно создать дополнительно к прикладному окну (основной форме или фрейму) другие окна, называемые дочерними окнами (дочерними формами или фреймами). На рисунках 10.9.1 и 10.9.2 изображены основное и дочернее окна приложений, программы которых приведены в примерах 10.9.1 и 10.9.2. Основное окно отображает российский флаг, а при нажатии на клавишу мыши в нем отображается слово, введённое в редактор, размещённый в дочернем окне. При нажатии на кнопку дочернего окна изменяется цвет выведенной строки. Примеры 10.9.1 и 10.9.2 включают только дополнения к программам примеров 10.8.1 и 10.8.2. В примере 10.9.3 программа реализована на языке C++/CLI.

Пример 10.9.1. Программа с дочерними окнами на языке C#.

//////////////////// //c#

// И з м е н е н и я ф а й л а п р и м е р а 1 0 . 8 . 1 p u b l i c c l a s s C T w o F o r m s : F o r m // Класс прикладного окна (основной формы)

{ Form childForm; TextBox ourTextBox; Button ourButton; Point point; Color [] coiors ; int n; public CTwoForms ( )

{

// Дочерняя форма // Редактор // Кнопка // Координаты начала строки // Массив цветов // Индекс массива colors // Конструктор

Text= "Two forms"; // Установить заголовок прикладного окна ourTextBox= new TextBox ( ) ; // Создать редактор и ourTextBox.Location= new Point (10, 120);// разместить его ourTextBox.Size= new Size (150, 20); // с размером ourTextBox.Text= "Russia"; // Установить текст в редакторе ourButton= new Button ( ) ; // Создать кнопку и ourButton.Location= new Point (170, 10); // разместить её ourButton.Size= new Size (40, 20); // с размером ourButton.Name= "cmdButton"; // Установить имя кнопки ourButton.Text= "OK"; // Установить текст в кнопке // Подписать обработчик на событие Click кнопки


ourButton.Click+= new System.EventHandler(cmdButton_Click); // Подписать обработчик на событие MouseDown мыши MouseDown += new MouseEventHandler (OurMouseDown); n= 0; polnt.X= 100; point.Y= 100; colors= new Color [3]; colors [0]= Color.FromArgb (220, colors [1]= Color.FromArgb (0, 0, colors [2]= Color.FromArgb (255,

// Установить координаты строки 220, 220); // Серый цвет 255); // Синий цвет 0, 0); // Красный цвет

c h ! l d F o r m = n e w F o r m ( ); // Создать дочернюю форму и c h i l d F o r m . L o c a t i o n = n e w P o i n t ( 2 5 0 , 10); // разместить c h i l d F o r m . S i z e = n e w Size ( 2 5 0 , 2 5 0 ) ; // с размером c h i l d F o r m . T e x t = " C h i l d F o r m " ; // Установить заголовок c h i I d F o r m . S h o w ( ); // Отобразить на экране childForm.Controls.Add (ourTextBox); // Добавить редактор childForm.Controls.Add (ourButton); // Добавить кнопку

} static void Main ( )

{

>

>

| | Главная функция

Application.Run(new C T w o F o r m s ( ));

| | Выполнить

C#. Программа примера 10.9.1 изменила приложение примера 10.8.1, дополнив его дочерним окном. В классе CTwoForms прикладного окна объявлена ссылка childForm на дочернее окно типа Form. Конструктор, используя оператор new и свойства Location, Size и Text, создает объект дочернего окна в управляющей куче и добавляет в коллекцию его управляющих элементов объект ourTextBox редактора и объект ourButton кнопки. Теперь текстовый редактор и кнопка появятся не в прикладном, а в дочернем окне.


Рис. 10.9.1. Прикладное окно приложения примера 10.9.1 сдвумя формами и с консолью на языке C# Пример 10.9.2. Программа

с

дочерними окнами на языке

Java(J#).

//////////////////// // J a v a и J # // И з м е н е н и я ф а й л а п р и м е р а 10.8.2 iiTipOrt jav'a.avvt. •) Import java.awt.event.*; public class CTwoFrames extends Frame // Класс прикладного окна

<

Frame c h l l d F r a m e ; TextFleld ourTextFleld; Button ourButton; Polnt polnt; Color [] colors; int n; public CTwoFrames()

// // // U // //

Дочерний фрейм Редактор Кнопка Начальные координаты строки Массив цветов флага Индекс массива цветов colors

// Конструктор

{ setTltle("Russlan flag"); // Установить заголовок прикладного окна setSize (new Dimension (400, 200)); this.setBackground(Color.llghtGray); ourTextFleld = new TextField ( );// Создать редактор ourTextFleld,setSize(150, 20); //Установитьпозицию ourTextField.setText ("Russia"); // Установить текст в редакторе ourButton= new Button ("OK"); // Создать кнопку // Подписать обработчик на событие кнопки ourButton.addActionLlstener (new ActionListener ( )

{

public void actionPerformed (ActionEvent aE) < n++; if(n >= 3) n= 0; repaint ( ) ;

»;

// К следующему цвету флага

>

// Подписать обработчик на событие MouseDown мыши this.addMouseListener (new MouseAdapter ( ) { public void mousePressed (MouseEvent mE) { point.x= mE.getX ( ) ; point.y= mE.getY ( ) ; repaint ( );

»;


// Примененить внутренний анонимный класс для закрытия окна this.addWindowListener (new WindowAdapter ( )

{

public void windowClosing (WindowEvent wE)

Ч

»;

>

System.exit (0);

point= new Point ( ); point.x= 100; point.y= 100; colors= new Color [3]; colors [0]= Color.white; colors [1]= Color.blue; colors [2]= Color.red;

>

// // // //

Создать массив цветов Белый цвет Синий цвет Красный цвет

c h i l d F r a m e = n e w F r a m e ( ); // Создать дочернюю форму и childFrame.setSize (200, 100); // с размером childFrame.setLayout ( n e w F l o w L a y o u t ( )); childFrame.add (ourTextField); // Добавить редактор c h i l d F r a m e . a d d (ourButton); // Добавить кнопку c h i l d F r a m e . s h o w ( );

// Перерисовать область клиента окна public void paint (Graphics g) { Rectangle rect= new Rectangle (10, 40, 130, 30); for(int i= 0; I < 3; i++) // Нарисовать флаг

{

>

>

g.setColor (colors[i]); g.flllRect // Нарисовать прямоугольник ( rect.x, rect.y, rect.width, rect.height); rect.translate (0, 30); // Переместить прямоугольник вниз

// Нарисовать строку от носика курсора мыши g.setColor (colors[n]); g.drawString (ourTextField.getText ( ), point.x, point.y);

public static void main ( )

{

>

>

// Главная функция

CTwoFrames cT= new CTwoFrames ( ); cT.show ( );

Java и C#. В на языке Java примера 10.9.2 в классе CTwoFrames прикладного окна объявлена ссылка childFrame на дочернее окно типа Frame. При создании дочернего окна в конструкторе CTwoFrames() применены оператор new и свойство setSize(). Добавленные с помощью функции add управляющие элементы ourTextField и ourButton размещает в области клиента дочернего окна


менеджер расстановки типа setLayout() прикладного окна. '5и»>»>Л'ч

'

'

FIowLayout,

»"

который указан

в свойстве

'Уя'й

Рис. 10.9.2. Прикладное окно приложения примера 10.9.2 с двумя фреймами на языке Java Пример 10.9.3. Программа с дочерними окнами на языке C++/CLI. /////////////// // C + + / C L I #include "stdafx.h" #uslng <System.Drawing.dll> #using <System.Windows.Forms.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing;

ref class CTwoForms : public Form // Класс прикладного окна

{ Form ^chlldForm; TextBox ^pTextBox; Button ^pButton; Point point; array<Color ^> ^colors; int n; public: CTwoForms ( )

// Дочерняя форма //Дескриптор редактора // Дескриптор кнопки // Начальные координаты строки // Массив цветов // Индекс цвета // Конструктор

{ || Подписать обработчик на событие MouseDown мыши MouseDown += gcnew MouseEventHandler (this, &CTwoForms: :OurMouseDown); Text= "Russian flag"; // Установить заголовок окна n= 0; colors= gcnew array<Color^> (3); // Создать массив структур colors[0]=Color::White; colors [1]= Color::Blue; colors [2]= Color::Red; pTextBox= gcnew TextBox(); // Создать объект редактора pTextBox -> Location= Drawing::Point (10,120); // Разместить pTextBox-> Size= Drawing::Size (150, 20); // Размер pTextBox -> Text= "Russia"; // Поместить текст в редактор Controls -> Add (pTextBox); // Добавить редактор к форме pButton= gcnew Button; // Создать объект кнопки pButton -> Location= Drawing::Point (170,10); // Разместить pButton -> Size= Drawing::Slze (40, 20); // Размер pButton -> Text= "OK"; II Поместить текст в кнопку


Controls -> Add (pButton); // Добавить кнопку к форме // Подписать обработчик на событие Click кнопки pButton -> Click += gcnew System:;EventHandler (this, &CTwoForms:: Ou rButtonClick); // Подписать обработчик на событие MouseDown мыши MouseDown += gcnew MouseEventHandler (this, &CTwoForms: :OurMouseDown); childForm= gcnew Form ( ) ; // Создать дочернюю форму и chlldForm->Location= *gcnew Polnt (250, 10); // разместить unldFonTi->Size= *ycnew Drawlng;:SiZ6 (250, 250); // с размером childForm->Text= "ChildForm"; // Установить заголовок childForm->Show ( ); // Отобразить на экране childForm->Controls->Add (pTextBox); // Добавить редактор childForm->Controls->Add (pButton); // Добавить кнопку

>

protected: virtual void OnPaint (PaintEventArgs {

Л

arg) override

// Перерисовать область

Form: :OnPaint (arg); Rectangle rect (10, 10, 130, 30); // Прямоугольник третьей части флага for(lnt 1=0; i < 3; i++) // Нарисовать флаг

{

>

>

arg->Graphics->FillRectangle(gcnew SolidBrush (*colors[i]), rect.X, rect.Y, rect.Wldth, rect.Height); rect.Offset (0, 30); // Сместить прямоугольник rect вниз

// Написать строку от координат носика курсора мыши arg -> Graphics -> DrawString (pTextBox ->Text, gcnew System::Drawing::Font ("Arial", 16), gcnew SolidBrush (*colors [nJ), point);

void OurMouseDown (Object ^pSender, MouseEventArgs ^arg) {

>

point.X= arg -> X; point.Y= arg -> Y; Invalidate ( ) ;

// Сохранить координату X носика курсора мыши // Сохранить координату Y курсора мыши // Перерисовать область клиента окна

// Обработать щелчок на кнопке void OurButtonClick (Object ^sender, EventArgs {

>;

>

n++; if (п >= 3) n= 0; Invalidate ( ) ;

Л

е)

// К следующему цвету массива цветов colors // Перерисовать область клиента окна

int maln ( ) < Application::Run (gcnew CTwoForms ()); return 0;

//Выполнить


C++/CLI. Программа примера 10.9.3 схожа с C# программой примера 10.9.1. Применительно к управляемому коду здесь применены операторы gcnew, дескрипторы, стрелки. В начале программы помещены операторы #using, подключающие требуемые библиотеки.

10.10. И с к л ю ч е н и я Исключения играют важную роль в программировании. Программа, осуществляющая ввод данных определённого формата и оперирующая с ресурсами, обязана, воспользовавшись исключениями, контролировать как корректность вводимых данных, так и доступность используемых ресурсов, иначе в программе произойдут сбои, и её работа, к сожалению пользователя и к стыду создателя, завершится аварийно с возможной выдачей непонятной для пользователя информации. Так что же такое исключение? Исключение (exception) - это объект, выдаваемой системой выполнения программы (CLR для C# или JVM для Java), когда при исполнении программы случилась неопределённая ситуация, система не знает что делать и выбрасывает (throw) специальный объект (exception), содержащий информацию о случившемся. Если программист предусмотрел возможность появления этого объекта, схватил (catch) его и среагировал надлежащим образом, то аварийное завершение не случится - программа продолжит нормально функционировать. Иначе, к глубокому сожалению, выполнение программы завершится аварийно. Пример 10.10.1 иллюстрирует аварийное завершение программы при делении на нуль. Пример 10.10.1. Исключение не обрабатывается. //////////////////// // J a v a и л и J # package cjLecExc_l; public class Exc { public static void main ( ) {

> /*

>

int x, у, z; x= 5; y= 0; z = x / y ; System.out.println ("z= "+z);

Result: На консоль вьщаётся информация об аварийном завершении программы: Unhandled Exception: System.DivldeByZeroExceptlon: Attempted to divide by zero, at cjLecExc_l.Exc.main() in c:\VS_Programs\cjLecExc_l\Classl.jsl


:line 9 Press any key to continue */

//////////////////// // C# using System; class Exc {

static void Main < ^ { int x, y, z; x= 5; y= 0; z = x / y ; Console.WriteLine ("z= "+z);

> > /*

Result: На консоль вьщаётся информация об аварийном завершении программы: Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. at C l a s s l . M a l n ( ) in c:\vs_programs\cslecexc_l\classl.cs:line 8 Press any key to continue */

До выброса исключения функция Main() и main() присвоили значения переменным x и у. При делении x на нулевое значение переменной у выброшено исключение, работа программы прерывается, и, заметьте, функции WriteLine() и println() не выполняются: на консоль не вьщаётся значение переменной z. Как видно из результата работы программы, на консоль выдана информация о необработанном программистом исключении (unhandled exception) типа DivideByZeroException (попытка разделить на нуль). Для обработки исключения применяется try-catch-finally конструкция: try {

>

контролируемый программный код

c a t c h (тип-исключения объект-исключения ) {

>

обработчик исключения для объекта типа

[ finally <

>]

обработка перед выходом из try-блока

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


мещают последовательность операторов, реагирующих на это исключение. Последовательно можно разместить несколько catch-блоков, каждый из которых обрабатывал бы своё исключение. Но catch-блок более общего исключения (базового исключения, или суперкласса) должен быть размещён ниже, иначе оно перехватит следующие (порождённые из него) исключения. finally-блок может отсутствовать. Последовательность его операторов выполняется перед возвратом из try-блока. Пример 10.10.2 иллюстрирует обработку DivideByZeroException исключения. Пример 10.10.2. Обрабатка DivideByZeroException исключения. шишинншш || Java или J# package cjLecExc_2b; public class Exc { public static void main(String[] args) { int x, у, z; try {

> > I*

>

x= 5; y= 0; z= x/y; System.out.println("z= "+z);

catch(ArithmeticException e) {System.out.println("Exception is catched");}

Result: Exception is catched Press any key to continue */

llllllllllllllllllll И C#

using System;

class Exc { static void Main(string[] args) < int x, y, z; try {

>

x= 5; y= 0; z= x/y; Console.WriteLlne("z= "+z);

catch(ArithmeticException e) {Console.WriteLine("HcKflK>4eHMe перехвачено");>


>

/*

Result: Исключение перехвачено Press any key to continue */

В примере 10.10.2 потенциально опасное деление на нуль охвачено tryблоком, а catch-блок перехватил выброшенное исключение типа ArithmeticException и выдал на консоль сообщение. Программа завершилась не аварийно. Поскольку с одним try-блоком может быть связано много catch-блоков и, более того, try-блок может включать по мере необходимости в надлежащих местах другие try-блок со своими catch-блоками, то необходимо быть внимательным и помнить, что после выброса исключения схватывается ближайший catch-блок. Будучи размещённая в функции, try-catch-конетрукция после перехвата исключения, выполнив свою работу, завершит выполнение этой функции. Обратите внимание на результат работы программы примера 10.10.3, в котором вызываются две функции.

Пример 10.10.3. Вложение исключений. //////////////////// // J a v a и л и J # package cjLecExc_3a; public class Exc { public static void main ( ) < try {

>

>

First ( );

catch ( A r i t h m e t i c E x c e p t i o n e ) {System.out.println ( e + " First");>

static void First ( ) {

>

Second ( ) ;

static void Second ( ) { int x, y, z; try { x= 5; y= 0; z= x/y;


System.out.println ("z= " + z ) ;

>

c a t c h ( A r i t h m e t i c E x c e p t i o n e) {System.out.println (e+" Second");>

> /* Result: java.lang.ArithmeticException: / by zero Second Press any key to continue */

Программа примера 10.10.3 при выбросе исключения перехватила ближайший catch-блок функции Second(). Но если закомментировать try-catchконструкцию в функции Second(), то сработает try-catch-конструкция, размещенная в функции First(). Результаты работы этой программы показывают, что при наличии соответствующего блока catch исключение не завершает аварийно всю программу, а завершает аварийно только функцию, в которой оно выброшено. Пример 10.10,4 иллюстрирует необходимость применения исключения для выявления правильности вводимых данных в текстовый редактор. Программа проверяет, какое число было введено в редактор: положительное или отрицательное. После ввода числа и нажатия на кнопку в редакторе появляется слово positive или negative. Если введено не число, то в редакторе добавляется строка Error! Enter the correct number. Пример 10.10.4. Обработчик киопки использует исключение. //////////////////// // J a v a и J # importjava.awt.*; importjava.awt.event.*; public class CTextBox extends Frame {

// Класс прикладного окна

TextField tB; Button b;

// Редактор // Кнопка

public CTextBox() {

// Конструктор

setTitle ("TestNumber"); // Установить заголовок прикладного окна setSlze (300, 200); thls.setBackground (Color.lightGray); setLayout (null); tB= new TextField ( ) ; // Создать редактор tB.setLocatlon (10, 30); // Разместить редактор tB.setSlze (250, 20); // Установить размер tB.setText ("Enter the number"); // Установить текст в редакторе add (tB); // Добавить редактор в фрейм b= new Button ("OK"); Ц Создать кнопку b.setLocatlon (10, 50); // Разместить кнопку b.setSize (40, 30); // Установить размер кнопки


add ( b ) ; / / Добавить кнопку к фрейму // Подписать слушателя на событие кнопки b.addActionLlstener (new ActionListener ( )

<

public void actionPerformed (ActionEvent aE) { try

{

>

int n=Integer.parseInt (tB.getText()); lf(n<0) tB.setText ("negative"); else tB.setText ("pos!tive");

catch ( N u m b e r F o r m a t E x c e p t l o n e) {

});

>

>

tB.setText (tB.getText () + " Error! Enter the correct number");

this.addWindowListener (new WindowAdapter ( ) { public void windowClosing (WindowEvent wE) {

>

»;

>

System.exlt (0);

public static void main ( ) // Главная функция

< >

>

CTextBox cT= new CTextBox ( ); cT.show ( );

//////////////////// C# using System; using System.Drawing; using System.Windows.Forms; class CTextBox : Form

// Класс прикладного окна

{ TextBox tB; Button b;

// Редактор // Кнопка

public CTextBox()

// Конструктор

{

Text="TestNumber";

// Установить заголовок прикладного окна

tB= new TextBox ( ) ; // Создать редактор tB.Location= new Point (10, 30);// Разместить tB.Size= new Size (250, 20); // Установить размер tB.Text= "Enter the number"; // Установить текст в редакторе


this.Controls.Add (tB); b= new Button (); b.Text= "OK"; b.Location= new Point (10, 50); b.Size= new Size (40, 30); Controls.Add (b); b.Click += new EventHandler (E

// Добавить редактор в форму // Создать кнопку // Установить название кнопки // Разместить кнопку // Установить размер кнопки // Добавить кнопку в форму it); // Подписать обработчик

private void But (Object obj, EventArgs а)

r

\

string st= tB.Text; try { int n=System.Convert.ToInt32 (st); if(n<0) tB.Text= "negative"; else tB.Text= "positive";

}

//catch (Exception e) catch (FormatException e) {

>

tB.Text= tB.Text + " Error! Enter the correct number";

static void Main ( ) // Главная функция {

>

CTextBox cT= new CTextBox ( ); Application.Run (cT);

/////////////// // C+ + / C L I #include "stdafx.h" #using <System.Drawing.dll> #using <System.Windows.Forms.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing; ref class CTextBox : Form

{

TextBox ^tB; Button ^b;

public: CTextBox( )

{

Text="TestNumber";

// Класс прикладного окна // Редактор // Кнопка // Конструктор // Установить заголовок прикладного окна

tB= gcnew TextBox ( ); // Создать редактор tB->Location= *gcnew Point (10, 30); // Разместить tB->Size= *gcnew Drawing::Size (250, 20); // Установить размер


tB->Text= "Enter the number"; // Установить текст в редакторе thls->Controls->Add (tB); // Добавить редактор в форму b= gcnew Button (); // Создать кнопку b->Text= "OK"; // Установить название кнопки b->Location= *gcnew Drawing::Point (10, 50); // Разместить кнопку b->Size= *gcnew Drawing::Size (40, 30); // Установить размер Controls->Add (b); // Добавить кнопку в форму // Подписать обработчик b->Click += gcnew EventHandler (this, &CTextBox::But);

> private: void But (Object ^obj, EventArgs

i

>

>

а)

String ^st= tB->Text; try { int n=System::Convert::ToInt32 (st); if(n<0) tB->Text= "negative"; else tB->Text= "positive";

//catch (Exception e) catch (FormatException <

>;

л

>

л

е)

tB->Text= tB->Text + " Error! Enter the correct number";

void main ( ) {

>

Application::Run (gcnew CTextBox ( ));

// Выполнить

C# и C++/CLI. Обработчик But() кнопки извлекает строку из редактора текста, применив свойство Text редактора. В блоке try-catch осуществляется преобразование извлечённой строки в целое число с помощью статической функции ToInt32() класса Convert. Если символы преобразуемой строки не являются цифрами, то во время преобразования будет выброшено исключение типа FormatException и в текстовом редакторе появится строка Error! Enter the correct number. Иначе положительное число в поле редактора заменится на слово positive, а отрицательное на negative. Java и J#. На событие кнопки с помощью функции addActionListener() подписан объект анонимного класса ActionListener адаптера с обработчиком actionPerformed(). Обработчик в блоке try-catch извлекает из объекта tB редактора строку, используя статическую функцию parseInt() класса Integer и свой-


ство getText() редактора. Результат работы программы совпадает с результатом работы программы на языке C#. Обратите внимание, что в теле конструктора CTextBox( ) на языке Java аргумент функции setLayout (nuII) установки менеджера расстановки равен null, поэтому размещение редактора и кнопки в окне осуществляется не менеджером, а программистом с помощью свойств setLocation(), setSize() и sctText().

Рис. 10.10.4. Прикладное окно приложения примера 10.10.4 после ввода положительного числа и нажатия на кнопку


П.Разработка программы LorryEndWarehouse Разработаем программу, в прикладном окне которой размещены два склада с грузом, который перевозится грузовиками. Склады представлены прямоугольниками, часть которых закрашена желтым цветом, указывающим заполненность склада. Грузовики, представленные в виде полых или заполненных кругов с номерами, горизонтально перемещаются от склада к складу, заезжая и задерживаясь в зоне контроля, представленной вертикальным зелёным прямоугольником. Прикладное окно программы csLorryAndWarehouse изображено нарис. 11.4.3.1. Среди объектов есть объекты с потоками, использующими разделяемые объекты (разделяемые ресурсы). Например, ресурс "склад с товарами" (warehouse) используется последовательно, то есть разделяется между грузовиками (lorry), которые друг за другом разгружают в него груз или загружаются грузом. Каждый объект грузовика перемещается с помощью потока. Когда несколько грузовиков подъезжают к складу, то только одному из них дозволено загрузиться или выгрузиться на складе, то есть только один поток из потоков грузовиков должен завладеть объектом склада, а другие должны встать в очередь и ждать, когда ресурс-склад освободится. Таким образом, применительно к разделяемому ресурсу необходимо синхронизировать параллельно выполняющиеся потоки, использующие его. Что и будет сделано в программе. В программе кроме разделяемого ресурса-склада применен другой ресурс - зона контроля (control region), при въезде в которую грузовики будут обслуживаться по очереди. Это второй разделяемый ресурс. Поскольку в программе создаётся много грузовиков и часто приходится выполнять над их объектами одну и ту же операцию, то применяется список - объект класса ArrayList. По мере создания объекты грузовиков включаются в этот список. При необходимости элементы списка (объекты грузовиков) циклически извлекаются, и с каждым из них выполняется требуемая операция. Для фиксации полного заполнения или полного опустошения складов применим событие или уведомление. На это событие (уведомление) среагирует обработчик, который сменит направление перевозки груза между складами, сообщив об этом каждому грузовику. Классы склада, зоны контроля и грузовика разместим в отдельных библиотеках. О создании библиотек в средах Visual Studio .NET и Eclipse рассказано в приложении 3 книги в конце книги. В приложениях 1 и 2 приведены версии этой программы без библиотек, включающие все классы в одном приложении.


11.1. Программа с точки зрения пользователя Между двумя складами (warehouses) грузовики (lorries) перевозят грузы (loads). Перевозкой управляет центр (center), увеличивая или уменьшая количество грузовиков. Пользователь программы, который и представляет ценгр, может добавить новый грузовик или удалить указанный, остановить все гузовики или возобновить их движение. В прикладном окне программы LorryEndWarehouse представлены два склада в виде прямоугольников, динамически отображающих изменение груза, хранящегося на каждом из них (желтым цветом отражена хранящаяся на складе часть груза). Грузовики же изображены в виде окружностей, движущихся от склада к складу. Каждому грузовику приписан его уникальный номер. Этот номер используется при удалении грузовика. В прикладном окне находятся кнопка Добавить добавления грузовика и кнопка Удалить удаления грузовика. Перед нажатием на кнопку Удалить необходимо выбрать номер удаляемого грузовика в элементе списка, представленном в прикладном окне. Кнопки Stop и Run позволяют приостанавливать или возобновлять движение грузовиков. При запуске программы показывается прикладное окно с управляющими элементами и двумя складами - один склад пустой, а другой полный груза.

Рис. 11.1.1. Диаграмма вариантов использования программы LorryEndWarehouse После нажатия на кнопку Добавить появляется движущийся грузовик, которому приписан номер 1. При добавлении очередного грузовика его номер на единицу превысит номер предыдущего. Грузовики перевозят грузы из полного левого склада к пустому правому. Когда пустой склад заполнится,


движение грузовиков будет обратным. Так что сначала наблюдается перевозка груза с левого склада на правый склад, а потом с правого на левый, и наоборот. Между складами находится зона контроля, изображённая в виде зелёного прямоугольника, вытянутого вертикально. Каждый из грузовиков, войдя в эту зону, задерживается на 2 секунды для проверки его исправности и ремонта. Диаграмма вариантов использования программы представлена на рис.

11.2. Объектное представление программы Проанализируем нашу программу с точки зрения составляющих её объектов и назовём некоторые из них для облегчения дальнейшей ссылки к ним. Разумеется, вырисовывающийся вариант построения будущей программы не является лучшим. Возможны и другие, более интересные. Здесь сделан акцент на его простоту и предпочтительное употребление потоков, их синхронизацию, использование собьггий и разнообразных управляющих элементов, включая кнопки, редактор, список и управляющие элементы типа UserControl и Panel. Исходя из предыдущего раздела, основными объектами программы являются объект прикладного окна, объекты управляющих элементов (кнопки и управляющий элемент списка), объект зоны контроля, два объекта склада и объекты грузовиков. Объект window прикладного окна LorryEndWarehouse владеет объектами двух складов (leftWH и rightWH), объектом region зоны контроля и объектом aL списка типа ArrayList, который включает объекты-грузовики. Прикладное окно должно перерисовывать изображения перемещающихся грузовиков, для чего можно воспользоваться специальным потоком thread. Панели же, представляющие склады и область контроля, поскольку они наследуют классы UserControl и Panel, рисуются самостоятельно. Объект-грузовик (lorry) является объектом класса Lorry, содержит собственный поток, обеспечивающий его перемещение от склада к складу, загрузку и выгрузку груза, и приостановку движения в зоне контроля. В зоне контроля может только один грузовик пребывать определенное время. Поскольку разные объекты грузовиков не могут одновременно войти в зону контроля, здесь необходима синхронизация потоков грузовиков. Объект-склад (leftWH или rightWH) является объектом класса Warehouse, динамически показывающий изменение груза на складе путём закрашивания части прямоугольника, которым представлен склад в окне window. Поскольку склад одновременно может понадобиться нескольким грузовикам, выгрузка и загрузка грузовиков синхронизирована. О переполнении склада или опустошении склада фиксируется специальной булевской


переменной full, которая меняет поведение грузовика, заставляя его обеспечить перевоку груза в надлежащем направлении. На рис. 11.2.1 изображена диаграмма объектов программы LorryEndWarehouse. На диаграмме представлено четыре объекта грузовиков класса Lorry, но, заметим, в программе их количество определяется пользователем программы, может увеличиваться и уменьшаться.

Рис. 11.2.1. Диаграмма объектов nporpaMMbiLorryEndWarehouse

11.3. События, потоки и их синхронизация Итак, на основании анализа задачи выявлены основные объекты и их взаимосвязи. Программа реализует модель, состоящую из множества взаимосвязанных объектов. Функционирование программы обеспечивают множество потоков. Основной поток, реализованный функцией Main() или main(), создаст объект прикладного окна. При создании объектов грузовиков в основном потоке запускаются потоки грузовиков - модель начинает функционировать. Для простоты грузовик перевозит груз в одном направлении, пытаясь загрузиться грузом или выгрузиться в складе. Если какой-либо склад будет опустошён, то грузовик меняет направление перевозки груза. Операции объектов класса Lorry, связанные со складами и с объектом зоны контроля, должны быть синхронизированы. Поскольку загрузка груза из склада и выгрузка из грузовика определяются средствами склада, то синхронизация при этом параллельных потоков объектов грузовиков должна осуществляться складом. Для этого можно воспользоваться критическими секциями, применёнными в теле функций Get() и Put() класса Warehouse склада, которые соответственно осуществляют загрузку и выгрузку грузовиков. Что же касается захода грузовиков в зону контроля, то время пребывания в этой зоне на обследование и ремонт определяется только грузовиком. Поэтому объект region зоны контроля рассматривается как разделяемый ресурс. Каждый объект грузовика должен захватывать, использовать в течение заданного промежутка времени и освобождать этот ресурс. При своём удалении объект грузовика обязан освободить (dispose) разделяемый ресурс, если он его использует, чтобы другие грузовики могли им воспользоваться. Инте-


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

11.4. Поэтапная разработка программы В результате объектного анализа решаемой задачи выявилась необходимость разработки следующих классов: - класс LorryEndWarehouse - класс объекта, представляющего функционирование реализуемой модели в окне; - класс Warehouse, объектами которого являются склады; - класс Lorry, объектами которого являются грузовики; - класс ContrlRegion, объектом которого является зона контроля. Начнём разработку программы с разработки класса Warehouse. Затем перейдём к разработке класса ContrlRegion и класса Lorry, использующего объекты классов Warehouse и ContrlRegion. На последнем третьем этапе завершим разработку созданием приложения с управляющими элементами. Конечно, при желании можно увеличить количество этапов, упростив их. Например, реализацию каждой функции или части функций класса осуществлять в отдельном этапе. Объём книги не позволяет это сделать, но настоятельно рекомендуется при реализации этой программы самостоятельно включить больше этапов, что позволит не только лучше понять функционирование этой программы, но и приобрести полезный опыт поэтапной разработки. Если читателю эта программа покажется интересной, то очень желательно было бы её модифицировать с применением абстрактных классов.

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


щих свойств Location (getLocayion() и setLocation()), Size (getSize() и setSize()), BackColor (getBackground() и setBackground()) и др. Те же самые свойства могут применяться как к объектам класса Warehouse, так и внутри самого класса Warehouse для выявления расположения его управляющего элемента в прикладном окне, получения его ширины и высоты и др. Заметим, в следующем разделе будет применён базовый класс UserControl (или Panel) при создании класса ContrlRegion. На рис. 11.4.1.1 представлен класс Warehouse (для C#). •Warehouse -left: bool - a l l l o a d : int -partLoad; int -timePeriod: int +evFromWarehousB: +Warehousa() +Put() +Get() #Change() #OnPaint()

>UserCnntrol +WarehnusaEtfantArqs

DelEvFromWarehouse

+Full: property +Left: property -left: bool -full: bool

^. +EventAras

+Warflhangp.FvantArflftfr

<<.uie>> . . . ^ «-Monitor

L55.У?.е.>?....> +DelEvFromWarehouse

+Enter() 4Cxrt()

Рис. 11.4.1.1. Диаграмма классов склада Warehouse (для C#) C#. В класс входит собьггие, сигнализирующее грузовикам, движущимся к этому складу с грузом о необходимости смены направления движения. Данные собьггия содержаться в специальном объекте класса WarehouseEventArgs и включают свойства Full и Lefl, определяющие, возникло ли переполнение или опустошение склада (Full), и в каком складе - левом или правом (Left). Делегат события определим так: public delegate void D e l E v F r o m W a r e h o u s e (object sender, WarehouseEventArgs args);

А собьггие как: public event DelEvFromWarehouse

evFromWarehouse;

Для простоты реализации фиксируется максимальная вместимость allLoad склада числом 100. Груз выгружается из грузовика в склад или загружается в грузовик из склада порциями по 10 единиц (partLoad) в течение 100 мс. (timePeriod). Поскольку в течение этого времени должен обслуживаться только один грузовик, функция Put() выгрузки и функция Get() загрузки должны содержать критические секции, реализованные с помощью монитора


или оператора lock. Потокам других грузовиков, пытающимся выгрузиться или загрузиться, придётся встать в очередь и дождаться, когда функции Put() или Get() объекта склада, вызванные текущим грузовиком, выйдут из критической секции. Функции Put() и Get() используют общую функцию Change(), непосредственно добавляющую или убавляющую груз в складе В примере 11.4.1.1 приведён класс Warehouse, написанный на языках C# и Java. Класс Warchousc на языкс C# наследует класс LJserControl, а на языке Java - класс Panel, каждый из которых позволяет перерисовывать собственную область соответствующего управляющего элемента. Пример11.4.1.1. ^accWarehouse. /////////////// // C# using using using using

File csWarehouse.dll класс W a r e h o u s e System; System.Drawlng; System.Threading; System,Wlndows.Forms;

namespace csWarehouse { // Делегат события о переполнении и опустошении склада public delegate void DelEvFromWarehouse (object sender, WarehouseEventArgs args); // Класс данных события evFromWarehouse public class WarehouseEventArgs: EventArgs

{ private bool left; // Признак идентификации склада private bool full; // Признак заполнения склада // Конструктор public WarehouseEventArgs (bool left, bool full) {

>

this.left= left; this,full= full;

// Свойство заполнения склада public bool Full {

>

get {return full;>

// Свойство идентификации склада public bool Left {

>

>

get {return left;>

// Класс склада


public class Warehouse: UserControl { bool left; // Признак идентификации склада int allLoad= 100; // Текущий размер груза в складе lnt PartLoad= 10; // Размер груза грузовика int partLoad; // Выгружаемая или загружаемая порция int timePeriod= 100; // Время выгрузки или загрузки груза public event DelEvFromWarehouse evFromWarehouse; // Ссылка // на объект события о переполнении и опустошении склада // Конструктор public Warehouse (bool left, bool full) {

>

this.left= left; if (lfull) allLoad=0;

// Поместить груз в склад public bool Put (bool full) { lf(!full) return false; // Пустой грузовик не выгружать if(allLoad >= 100) return false; // Склад заполнен partLoad= PartLoad; // Определить выгрузку Change ( ); // Выгрузить return true;

} // Получить груз из склада public bool Get (bool full) { if(full) return false; // Полный грузовик не загружать if(((allLoad) < PartLoad) && (evFromWarehouse 1= null)) { // Недостаточно груза в складе для загрузки грузовика WarehouseEventArgs wH= new WarehouseEventArgs (left, false); evFromWarehouse (this, wH); // Сгенерировать событие

>

>

return false;

// Загрузить грузовик и уменьшить груз склада partLoad= -PartLoad; // Определить выгрузку Change (); // Выгрузить return true;

// Поместить или получить груз в склад protected void Change ( ) { // Выполнить операцию только с одним грузовиком //lock(this)

//{ Monitor.Enter (this); allLoad += partLoad;

// Начать критическую секцию


Invalidate ( ) ; Thread.Sleep (timePeriod); Monitor.Exit (this);

>

// Завершить критическую секцию

//>

// Перерисовать размещение груза в складе protected override void OnPaint (PaintEventArgs e) { base.OnPalnt (e); e.Graphics.FIIIRectangle (new SolidBrush (Color.Yellow), 0, 100 - allLoad, CllentSize.Width, ClientSize.Helght); e.Graphlcs.DrawString (allLoad,ToString ( ) , Font, new System.Drawing.SolidBrush (Color.Blue), 5, 101;

)

>

}

///////////////

// J a v a и J # F l l e cjWarehouse.dll

класс Warehouse

package cjWarehouse; importjava.awt.*; importjava,utll.*; // Класс данных события public class WarehouseEventArgs

{

private boolean left; // Признак идентификации склада private boolean full; // Признак заполнения склада // Конструктор public WarehouseEventArgs (boolean left, boolean full) {

>

this.left= left; this.full= full;

// Свойство заполнения склада public boolean getFull ( ) {return full;>

>

// Свойство идентификации склада public boolean getLeft ( ) {return left;}

// Класс наблюдаемого объекта class beWatched extends Observable

<

// Конструктор public beWatched ( ) { > void notifyObs (WarehouseEventArgs arg) { setChanged ( ); notifyObservers (arg);>


// Класс склада public class Warehouse extends Panel

{ boolean left; int W= 40; lnt H= 100; lnt allLoad= 100; int PartLoad= 10; int partLoad; inttimePeriod=lou; public beWatched bW; // объект

// Признак идентификации склада //Ширина // Высота // Текущий размер груза в складе // Размер груза грузовика // Получаемая или помещаемая порция груза //времяполученияилипомещения груза // Ссылка на вспомогательный наблюдаемый

// Конструктор public Warehouse (boolean left, boolean full) { this.left= left; if (lfull) allLoad= 0; // Склад пуст bW= new beWatched (); // Наблюдаемый объект склада

// Поместить груз в склад synchronized public boolean Put (boolean full) {

>

lf(!full) return false; if allLoad >= 100) return false; // Выгрузить грузовик и увеличить груз склада partLoad= PartLoad; // Определить выгрузку change (); return true;

// Получить груз из склада synchronized public boolean Get (boolean full ) { if (full) return false; if (((allLoad) < PartLoad)) {

>

>

// Недостаточно груза в складе для загрузки грузовика WarehouseEventArgs wA= new WarehouseEventArgs (left, false); bW.notifyObs (wA); // Уведомить обозревателя return false;

// Загрузить грузовик и уменьшить груз склада partLoad= -PartLoad; // Определить выгрузку change (); return true;

// Поместить или получить груз public void change ( ) {


// Выполнить операцию только с одним грузовиком synchronized (thls) // Критическая секция

{

allLoad += partLoad; try {

>

Thread.sleep (timePeriod);

catch (InterruptedExceptlon e) { >

>

>

repaint ( ) ;

// Перерисовать размещение груза в складе public void palnt (Graphics g) { g.setColor (Color.yellow); g.drawRect (0, 0, W, H); g.flllRect ( 0, 100 - allLoad, W, H); g.setColor (Color,black); g.drawStrlng (Integer.toString (allLoad), 5, 30);

>

}

///////////////

//C++/CLIFile

ccWarehouse.dll

KnaccWarehouse

#include "stdafx.h" #using <System.Drawing.dll> #uslng <System.Windows.Forms.dll> using namespace System; using namespace System::Windows:;Forms; using namespace System::Drawing; using namespace System::Threading; using namespace System:;Coiiections; namespace ccWarehouse { // Класс данных события evFromWarehouse public refclassWarehouseEventArgs: EventArgs { private: bool left; // Признак идентификации склада bool full; // Признак заполненности склада public: WarehouseEventArgs (bool left, bool full) {

>

this->left= left; this->full= full;

// Свойство заполнения склада property bool Full {


bool get ( ) {return full;>

// Свойство идентификации склада property bool Left {

>;

>

bool get ( ) {return left;>

//' ДсЛсГа! СОиЬииЯ О ПсрспОЛНеНИИ И Ori'yViCLuCIiy'iVi склада public delegate vold DelEvFromWarehouse (Object ^sender, WarehouseEventArgs ^args); // Класс склада public ref class Warehouse; UserControl

{ bool left; lnt allLoad; lnt PartLoad; lnt partLoad; lnt tlmePeriod;

// Признак идентификации склада II Текущий размер груза в складе // Размер груза грузовика Ц Выгружаемая или загружаемая порция // Время выгрузки или загрузки груза

public: event DelEvFromWarehouse ^evFromWarehouse; // Ссылка // на объект события о переполнении и опустошении склада // Конструктор Warehouse (bool left, bool full) {

>

allLoad= 100; PartLoad= 10; tlmePeriod= 100; this->left= left; lf(!full)allLoad=0; // Складпуст

// Поместить груз в склад bool Put (bool full ) { lf(!full) return false; // Пустой грузовик не выгружать if(allLoad >= 100) return false; // Склад заполнен // Выгрузить грузовик и увеличить груз склада partLoad= PartLoad; // Определить выгрузку Change (); // Выгрузить return true;

// Получить груз из склада bool Get (bool full) < if(full) return false; // Полный грузовик не загружать !f((allLoad < PartLoad) && (evFromWarehouse 1= null)) { || Недостаточно груза в складе для загрузки грузовика


>

>

WarehouseEventArgs ^wH= gcnew WarehouseEventArgs (left, false); evFromWarehouse (thls, wH); // Сгенерировать событие return false;

// Загрузить грузовик и уменьшить груз склада partLoad= -PartLoad; // Определить выгрузку Change ( ) ; // Выгрузить return true;

// Поместить или получить груз порциями и с временной задержкой protected: void Change ( )

{

>

// Выполнить операцию только с одним грузовиком Monitor::Enter (thls); // Начать критическую секцию allLoad += partLoad; Thread::Sleep (tlmePerlod); Invalidate ( ) ; Monitor:: Exlt (this); // Завершить критическую секцию

// Перерисовать размещение груза в складе protected: virtual void OnPalnt (PaintEventArgs ^e) override

{

>; >

>

UserControl ::OnPaint (e); e->Graphics->FillRectangie (gcnew SolidBrush (Color::Yellow), 0, 100 - allLoad, CllentSlze.Width, ClientSize.Height); e->Graphlcs->DrawString (allLoad.ToStrlng ( ) , Font, gcnew System::Drawing::SolidBrush(Color::Blue),5,10);

C#. Класс поместим в библиотеку csWarehouse.dll с пространством имён csWarehouse. Для чего при создании проекта на вкладке Templates необходимо выделить Class Library. О создании C# библиотеки см. в приложении 3 "Использование сред разработки" этой книги. Рассмотрим подробнее класс Warehouse. Поскольку в нашем приложении будут применены два объекта этого класса, то для идентификации объектов использован не целочисленный номер, а булевская переменная left, значение true которой определяет левый склад на прикладном окне, а значение false - правый. Булевская переменная full принимает значение true, когда склад заполнен полностью грузом и значение false иначе. Эти данные о складе и его заполнении передаются собьггием evFromWarehouse, генерируемым объектом склада в момент возникновения полного опустошения склада. Описание делегата DelEvFromWarehouse собьггия определяет в качестве списка параметров обработчиков ссылку к объекту класса WarehouseEventArgs, поэтому обработчик события, воспользовавшись этой ссылкой и используя


свойства Left и Full ссылаемого объекта, узнает, какой склад оказался опустошённым и подлежит заполнению. Класс Warehouse наследует класс UserControl с изобилием различных свойств и функций, позволяющих сделать из него интересный управляющий элемент, размещённый в окне. Нам понадобится только его свойства Location, Size, BackColor да возможность перерисовывать его область клиента с помощью функции перерисовки OnPaint(). Параметры конструктора Warehouse устанавливают местоположение и O o r m / W P U U A i ^ T L JUI ^j/lVVlillWWlW

Л Г П в TlO w i w l u ^ u .

Интерфейсные функции Put() и Get() выгружают или загружают грузовик, используя для этого внутреннюю функцию Change(). При этом груз allLoad, содержащийся в складе, либо увеличивается, либо уменьшается на порцию partLoad. В случае, когда склад не дозволяет выгрузить груз из грузовика в виду переполнения склада или не имеет возможность загрузить грузовик изза отсутствия груза, функции Put() и Get() возвращают значение false. Также значение false возвращается при попытке выгрузиться пустому грузовику и загрузится уже заполненному грузовику. Хотя последние случаи маловероятны, для надёжности программы необходимо учитывать все возможные ситуации - программа должна функционировать и при неправильных исходных данных. При полном опустошении склада генерируется событие evFromWarehouse, извещающее о необходимости смены направления перевозки груза из складов. Обработчик, подписавшийся на это событие, обязан уведомить все грузовики о необходимости смены направления перевозки груза. Для этого применяется булевская переменная leftRight и свойство LeftRight каждого грузовика. Их значение, равное true, определяет движение слева направо, а значение false - справа налево. Чтобы сделать выгрузку или загрузку груза доступной только для одного грузовика, функция Change() реализована как критическая секция с применением функций Enter() и Exit() монитора класса Monitor (или оператора lock). Функция Invalidate(), вызываемая в теле функции Change(), инициирует перерисовку области клиента склада после каждого добавления или удаления порции partLoad груза. При этом закрашенная желтым цветом часть прямоугольника склада увеличивается или уменьшается, динамически показывая текущую загруженность склада грузом. Java и J#. Класс Warehouse поместим в пакет cjWarehouse.dll. Для чего при создании проекта в системе разработки Eclipse необходимо создать класс без функции main(). О создании такого пакета см. в приложении 3 "Использование сред разработки" в конце книги. Программа на языке Java отличается, прежде всего, появлением класса beWatched, наследующего интерфейс Observable. Наследование интерфейса Observable указывает, что объекты класса beWatched являются наблюдаемыми объектами, то есть объектами, которые уведомляют о чём-либо другие объекты - обозреватели. Необходимость в создании наблюдаемого (уведомляющего) объекта этого класса в объекте склада возникла по простой причи-


не: класс Warehouse склада уже наследует класс Panel и поэтому не может наследовать второй класс Observable, чтобы уведомить об опустошении склада. По этой причине в классе Warehouse создаётся объект класса beWatched, и на него будет подписан обозреватель, содержащий обработчик, реагирующий на уведомление от этого объекта. Для генерирования уведомления достаточно вызвать функцию notifyObs() этого объекта, которая вызывает, в свою очередь, стандартные функции setChanged() и notifyObservers(arg), уведомляющие подписавшегося обозревателя. При этом обработчику обозревателя передаётся объект arg типа WarehcussEvsntArgs содержащий идентификатор склада и информацию о заполнении склада. В функциях Put() и Get() на языке Java вызов делегата языка C# заменён вызовом функции notifyObs() наблюдаемого объекта, содержащей в качестве аргумента объект wA типа WarehouseEventArgs, включающий данные о складе. Критическая секция функции change() реализована с помощью оператора synchronized. Выполнение функции change() изменяет содержимое склада и, вызывая функцию repaint(), заставляет перерисовываться панель, которой представлен склад в прикладном окне. Приложение, представленное в примере 11.4.1.2, иллюстрирует работу двух объектов класса Warehouse. Специальный поток thr приложения осуществляет загрузку в склады и выгрузку из складов груза, используя интерфейсные функции Get() и Put() класса Warehouse и признак направленности leftRight, значение которого меняются событиями из объектов класса Warehouse. Пример 11.4.1.2. Приложения csTestWarehouse и cjTestWarehouse. /////////////// // C# File csTestWarehouse // Подключить системные библиотеки System.Drawing.dll и // System.Windows.Forms.dll // Подключить разработанную библиотеку csWarehouse using System; using System.Drawing; using System.Threading; using System.Windows.Forms; using c s W a r e h o u s e ; namespace csTestWarehouse { class TestWarehouse: Form { Warehouse leftWH, rlghtWH; // Ссылки на объекты // компонента Warehouse bool leftRlght; // Признак направления перемещения грузовиков bool llfe; // Признак жизни потока управления загрузкой // и выгрузкой складов Thread thr; // Ссылка на поток управления загрузкой // и выгрузкой складов


public TestWarehouse ( )

{ // Создать объект левого склада leftWH= new Warehouse (true, true); leftWH.Location= new Polnt (10, 10); leftWH.SIze= new Slze (30, 100); leftWH.BackColor= Color.White; leftWH.evFromWarehouse+ = new DelEvFromWarehouse (WarehouseHandler); t'nis,Controis.Add (leftWH); leftWH.Show ( ); // Создать объект правого склада rlghtWH= new Warehouse (false, false); rightWH.Location= new Point (100, 10); rlghtWH.SIze= new Slze (30, 100); rightWH,BackColor= Color.White; rightWH.evFromWarehouse+= new DelEvFromWarehouse (WarehouseHandler); thls.Controls.Add (rightWH); rightWH,Show ( );

}

// Создать и запустить поток управления загрузкой // и выгрузкой складов leftRight= true; life= true; thr= new Thread(new ThreadStart (Go)); thr.Start ( );

// Обработать событие evFromWarehouse vold WarehouseHandler (object ob, WarehouseEventArgs arg) < // Выдать информацию о направлении перемещения и // о складе, объект которого сгенерировал событие Console.Write("event evFromWarehouse leftRight=" + leftRight); if (arg,Left) Console.Write("CKnafl leftWH "); else Console.Wrlte(" Склад rightWH "); if(arg.Full) Console.WriteLine(" полный"); else Console.WriteLine(" пустой"); Console.WriteLine( ); // Изменить направление перемещения грузовиков if(arg.Left && arg.Full) leftRight= true; if (larg.Left &&arg.Full)


leftRight= false;

// Обеспечить загрузку груза в склады и // выгрузку груза иэ складов vold Go ( ) { while (life) { lf (leftRight) {leftWH.Get (false ); rightWH.Put(true);} else

>

{leftWH.Put (true ); rightWH.Get (false );>

>

// Завершить поток управления загрузкой // и выгрузкой складов vold Finish ( ) {

>

life= false; thrJo!n ( );

static void Main ( ) {

>

>

// Создать объект прикладного окна TestWarehouse testWH= new TestWarehouse ( ) ; // Запустить приложение с прикладным окном Appllcation.Run (testWH); // Завершить поток управления загрузкой // и выгрузкой складов testWH.Finish ( ) ;

> /* Result: event evFromWarehouse event evFromWarehouse event evFromWarehouse event evFromWarehouse

leftRight= leftRight= leftRight= leftRight=

True False False True

Склад Склад Склад Склад

leftWH rlghtWH leftWH leftWH

пустой полный полный пустой

*/"

///////////////

// Java и J # . File cjTestWarehouse package cjTestWarehouse; // Класс Warehouse и отладка importjava.awt.*; importjava.util.*; import cjWarehouse.* class TestWarehouse extends Frame implements Runnable, Observer


Warehouse leftWH, rightWH; // Ссылки на объекты // компонента Warehouse boolean leftRight; // Признак направления перемещения грузовиков boolean llfe; // Признак жизни потока управления загрузкой // и выгрузкой складов Thread thr; // Ссылка на поток управления загрузкой // и выгрузкой складов public TestWarehouse ( )

i this.setLayout(null); // Менеджер расстановки не используем this.setSlze(400,300); // Создать объект левого склада leftWH= new Warehouse (true, true); leftWH.setLocation(10, 30); leftWH.setSize (30, 100); leftWH.setForeground (Color.white); leftWH.bW.addObserver (this); // Подписать обозреватель leftWH.show ( ); this.add(leftWH); // Создать объект правого склада rightWH= new Warehouse (false, false); rightWH.setLocation(350, 30); rightWH.setSize(30, 100); rightWH.setForeground(Color.whlte); rlghtWH.show ( ); rightWH.bW.addObserver(this); // Подписать обозреватель thls.add(rightWH);

>

// Создать и запустить поток управления загрузкой // и выгрузкой складов leftRight= true; life= true; thr= new Thread(thls,"thread"); thr.start ();

// Обработать событие evFromWarehouse public void update (Observable obs, Object args) { // Выдать информацию о направлении перемещения и // о складе, объект которого сгенерировал событие WarehouseEventArgs arg= (WarehouseEventArgs) args; if (arg.getLeftQ) System.out.print("— leftWH "); else System.out.print("— rightWH "); If (arg.getFull()) System.out.println(" full"); else


System.out.println(" empty"); // Изменить направление перемещения грузовиков if(larg.getLeft() && larg.getFull()) leftRight= true;

>

if (arg.getLeft() && larg.getFull()) leftRight= false;

// Обеспечить загрузку груза в склады и // выгрузку груза из складов public void run ( ) { while (life) { if (leftRight)

{leftWH.Get (false ); rightWH.Put (true );>

else try

{leftWH.Put (true ); rightWH.Get (false );>

{

>

>

>

Thread.sleep (100);

catch (InterruptedException e) { }

// Завершить поток управления загрузкой // и выгрузкой складов vold Finish ( ) <

>

life= false;

public static vold main ( )

{

>

// Создать объект прикладного окна TestWarehouse testWH= new TestWarehouse ( ) ; testWH.show();

> /* Result: — leftWH empty — rightWH empty — leftWH empty — rlghtWH empty */'

C#. Приложение csTestWarehouse применяет библиотеку csWarehouse.dll с классом Warehouse и пространством имён csWarehouse. Перед компиляцией приложения необходимо подключить эту библиотеку к приложению, воспользовавшись меню Project/AddReference системы разработки Visual Studio.


Класс TestWarehouse прикладного окна содержит признак leftRight направленности перемещения грузовиков, объекты leftWH и rightWH класса Warehouse и поток thr, потоковая функция Go() которого осуществляет циклическую загрузку в склады и выгрузку из складов груза, используя интерфейсные функции Get() и Put() объекта класса Warehouse и признак направленности leftRight. Значение признака leftRight меняется обработчиком WarehouseHandler() собьггия evFromWarehouse, генерируемого объектами leftWH и rightWH. В прикладном окне, представленным на рис. li.4.1.1, один из Складов будет заполняться грузом (желтая часть его прямоугольника увеличивается), а другой склад разгружаться (желтая часть его прямоугольника уменьшается). Когда склады опустошаться полностью, начнётся обратный процесс - заполненный прямоугольник будет разгружаться, а другой загружаться, и т.д. C# и Java(J#). Приложение cjTestWarehouse применяет ранее созданный пакет cjWarehouse с классом Warehouse. Класс TestWarehouse прикладного окна содержит признак leftRight направленности перемещения грузовиков, объекты leftWH и rightWH класса Warehouse и специальный поток, потоковая функция run() которого осуществляет циклическую загрузку в склады и выгрузку из складов груза, используя интерфейсные функции Get() и Put() класса Warehouse и признак направленности leftRight. Значение признака leftRight меняется обработчиком уведомления, генерируемого объектами leftWH и rightWH. В прикладном окне, представленным на рис. 11.4.1.1, один из складов будет заполняться грузом (желтая часть его прямоугольника увеличивается), а другой склад разгружаться (желтая часть его прямоугольника уменьшается). Когда склады опустошаться полностью, начнётся обратный процесс - заполненный прямоугольник будет разгружаться, а другой загружаться, и т.д.

Рис. 11.4.1.1. Результатработы приложения csTestWarehouse.


11.4.2. Второй этап. Разработка классов ContrlRegion и Lorry Объектами класса Lorry являются грузовики, перевозящие груз из склада в склад, Класс Lorry содержит поток, учитывающий все особенности поведения грузовика: из одного склада загружается груз, затем грузовик перемещается к другому складу и, встретив зону контроля, приостанавливается на некоторое время, дабы обследоваться на исправность и выполнить, при необходимости, ремонт, потом грузовик подъезжает к складу и встаёт в очередь, если уже подлежат разгрузке или погрузке другие предшествующие грузовики, и, наконец, разгружается. К потокам грузовиков, выполняющим все эти действия, применяется механизм синхронизации на этапах загрузки и выгрузки, а также и при прохождении зоны контроля. Разрабатывая класс Warehouse склада, мы возложили синхронизацию загрузки и выгрузки грузовиков на объекты класса Warehouse (введя критическую секцию в функцию Change() или change(), вызываемую в интерфейсных функциях Get() и Put()), считая, что время обслуживания грузовиков и очерёдность обслуживания определяется складом. Что же касается прохождения зоны контроля, то её представим в виде разделяемого ресурса, получив который объект компонента Lorry обязан освободить после его использования. Здесь время контроля может определяться грузовиком, но в данной программе оно одно и тоже для всех грузовиков. Интерфейс объекта класса Lorry должен включать функции приостановки и возобновления движения грузовика, завершения выполнения потока и, обязательно, функцию Dispose() освобождения разделяемого ресурса. Ссылка на объект разделяемого ресурса может быть передана при создании объекта класса Lorry через аргументы его конструктора. Разделяемый ресурс является объектом класса ContrlRegion. Чтобы разнообразить разработку программы и упростить представление этого разделяемого ресурса - зоны контроля - в прикладном окне, наследуем класс ContrlRegion из класса UserContro или Panel, а для ограничения доступа к нему объектов класса Lorry (грузовиков) в каждом из этих о&ьектов захватим этот ресурс с помощью монитора или оператора synchronized. Объект грузовика, захватив ресурс, выполнит некоторую работу длительность 2 сек., вызвав функцию Work().

+CnntrtRegion

+Work()

~

4>Uu>erCantral] . <<y?e >>. ->| +Thread |

Рис. 11.4.2.1. Диаграмма классов компонента ContrlRegion (для C#) Класс ContrlRegion прост. Его диаграмма классов изображена на рис. 11.4.2.1, а сам класс представлен в примере 11.4.2.1. Класс ContrlRegion язы-


ка C# разместим в библиотеке csContrlRegion с пространством имён csContrlRegionDll. Класс ContrlRegion языка Java поместим в пакет cjContrlRegion. Пример 11.4.2.1. Класс ContrlRegion. ///////////////

// C # using using using

Flle csContrlRegion к л а с с C o n t r l R e g i o n System; System.Threadlng; System.Windows.Forms;

namespace csContrlRegionDll { // Класс области контроля public class ContrlRegion; U s e r C o n t r o l

<

>

>

public void Work ( ) {

// Выполнить работу

Thread,Sleep (2000);

>

///////////////

// J a v a и J # File cjContrlRegion к л а с с C o n t r l R e g i o n lmportjava.awt.*; lmportjava.utiL*; Importjava.awt.event.*; // Класс ContrlRegion public class ContrlRegion extends P a n e l < / / Проконтролировать грузовик public void Work ( ) { try {

>

>

>

Thread.sleep (1000);

catch (InterruptedException e) { >

///////////////

// C + + / C L I File ccContrlRegion #include "stdafx.h" #using <System.Wlndows.Forms,dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Threadlng; namespace ccContrlReglonDII { | | Компонент ContrlRegion


public r e f c l a s s C o n t r l R e g i o n : U s e r C o n t r o l { public: void Work ( ) // {

>

>;

>

Выполнить работу

Thread::Sleep (2000);

Наиболее сложным классом нашей программы является класс Lorry, диаграмма классов которого изображена на рис. 11.4.2.2. С каждым объектом класса Lorry связан уникальный номер number, присваиваемый при создании этого объекта. Объект класса Lorry представлен в прикладном окне в виде круга с координатами в точке p, перемещающегося по горизонтали с приращением dX в каждом цикле потоковой функции Moving() собственного потока thr объекта этого класса. При создании объекта класса Lorry его конструктор присваивает требуемые при выполнении потоковой функции Moving() значения признака направления перемещения груза leftRight, значения ссылок leftWH и rightWH к объе к т а м складов, значение ссылки region к объекту зоны контроля.

*lorry -leftWH: Warehouse -rightWH: Warehouse -region: ContrlRegion -thr: Thread -leftRight: bool

-number int

-p: Point -dX:int -inContrl: bool -life: bool -run: bool -timePeriod: int +Number property +Point: property +LeftRight:propertv

^>| •Warehouse I

« у§£ »

^r^^———:—| >hCwrtriRwiffn I

+LorryQ

+Run() +Stop() +Finish() -Movinon

<<.ysfi. >>

Ц

+Thread |

<<-BW.>>..-..?>| *Mnnttnr I

г

Рис. 11.4.2.2. Диаграмма классов компонента Lorry Класс Lorry размещён в библиотеке csLorry с пространством имён csLortyDIl.


Класс Lorry представлен в примере 11.4.2.2. Он требует подключения библиотек csWarehouse и csContrlRegion, содержащих классы Warehouse и ContrlRegion. После создания объекта класса Lorry, при котором создаётся и объект thr потока с потоковой функцией Moving(), его функция Start() должна запустить поток. Функции Stop() и Run() приостанавливают или возобновляют выполнение потока, а функция Finish() его завершает. Цикл while продвигает координату x объекта на dX через период времени timePeriod, обеспечивая при достижении складов выгрузку или погрузку груза посредством интерфейсных функций Get() и Put() класса Warehouse. Для фиксации вхождения объекта грузовика в зону контроля region используется признак inContrl. При вхождении в зону region осуществляется захват разделяемого ресурса, а при выходе - его освобождение. Захват области контроля осуществляет статическая функция Enter() класса Monitor, а освобождение - статическая функция Exit(). Пример11.4.2.2. ^iaccLorry. /////////////// // C # using using using using using using

File csLorry к л а с с L o r r y System; System.Threading; System.Drawing; System.Wlndows.Forms; csWarehouseDI!; csContrlRegionDII;

namespace csLorryDII { // Класс грузовика public class Lorry { int number; Point p; lnt dX; bool leftRight; ContrlRegion region; bool inContrl; bool life; bool run; Thread thr; int timePeriod= 100; Warehouse leftWH; Warehouse rlghtWH; bool full;

// Номер объекта грузовикаа // Координаты объекта грузовика // Приращение координаты X // Направление перемещения груза // Ссылка на зону контроля // Признак нахождения в зоне контроля // Признак жизни потока // Признак выполнения потока // Ссылка на объект потока // Временной интервал перемещения // Ссылка на левый склад // Ссылка на правый склад

// Конструктор public Lorry (int Number, int Y, int DX, Warehouse LeftWH, Warehouse RlghtWH, ContrlRegion Region)

{ number= Number; dX= -DX; leftRight= true; leftWH= LeftWH; rightWH= RlghtWH; full= false;


region= Region; inContrl= false; p= new Point (); p.X= leftWH.Location.X+leftWH.Width + 50; p.Y= Y;

>

// Создать объект потока run= true; llfe= true; thr= new Thread (new ThreadStart (Movlng)); thr.Start ( ) ;

// Свойство Number public int Number {get {return number;>> // Свойство Point public Point Point {get {return p ; » // Свойство LeftRight public bool LeftRight {

>

set {leftRlght= value;> get {return leftRight;>

// Завершить поток public vold Finish ( ) {llfe= false; thr.Join ( ) ; } // Приостановить поток public void Stop ( ) {

>

run= false;

// Возобновить поток public void Run ( ) { lock (this)

{

>

// Создать синхронизируемый блок

run= true; Monitor.Pulse (this); // Разблокировать поток

> // Потоковая функция компонента private void Moving ( ) { while (life) { lock (this)

{

// Создать синхронизируемый блок

if (I run) Monltor.Wait (this); // Ожидать // разблокировки


// Объект грузовика достиг справа // объекта левого склада if (p.X <= leftWH.Location.X+leftWH.Width) {

>

if (leftRight) // Пересылка слева направо {if(leftWH.Get (fuli)) full= true;> // Загрузить груз else // Пересылка справа налево {lf(leftWH.Put (full)) full= false;> // Выгрузить груз dX= -dX;

/'/ ОбЪек! I ру^ОВИКЗа ДОС|ИГ СЛсБа // объекта правого склада if(p.X >= rightWH.Location.X) {

>

if (leftRight) // Пересылка слева направо {if(rightWH.Put (ful!))full= false;}// Выгрузить груз else // Пересылка справа налево {if(rightWH.Get (fuil)) full= true;>// Загрузить груз dX= -dX;

// Обслужить в зоне контроля Point pR= new Point (region.Location.X, region.Location.Y); Rectangle rect= new Rectangle (pR.X, pR.Y, region.Width, region.Height); // Вошли в зону контроля if (rect.Contains (p) Ш !inContrl) // Вошли в зону { Console.WriteLine ("Вошли в зону"); inContrl= true;

>

// Захватить разделяемый ресурс Monitor.Enter (region); region.Work ( ) ; // Освободить разделяемый ресурс Monitor.Exit (region);

if (!rect.Contains (p) && inContrl) // Вышли из зоны {

> > ///////////////

Console.WriteLine ("Вышли из зоны"); inContrl= false;

Thread.Sleep (timePeriod); p.X += dX;

// J a v a и J # F i l e cjLorry Importjava.awt.*; import java.util.*;

класс

Lorry


import java.awt.event.*; import cjWarehouse; import cjContrlRegion; // Класс Lorry public class Lorry implements Runnable { public int number; public Point p; int dX; public boolean leftRight; ContrlRegion region; boolean inContrl; boolean life; boolean running; Thread thr; int tlmePeriod= 100; Warehouse leftWH; Warehouse rightWH; Rectangle rect; boolean full;

// Номер грузовика // Координаты объекта грузовика // Приращение координаты X // Направление перемещения груза // Ссылка на зону контроля // Признак нахождения в зоне контроля // Признак жизни потока // Признак выполнения потока // Ссылка на объект потока // Временной интервал перемещения // Ссылка на левый склад // Ссылка на правый склад

// Конструктор public Lorry (int Number, lnt Y, int DX, Warehouse LeftWH, Warehouse RightWH, ContrlRegion Region)

{

>

number= Number; dX= -DX; leftRight= true; leftWH= LeftWH; rightWH= RightWH; full= false; region= Region; inContrl= false; p= new Point ( ); p.x= leftWH.getLocatlon().x+leftWH.WIDTH + 50; p.y= Y; rect= new Rectangle (reglon.getLocation().x, region.getLocation().y, region.getSize().width, reglon.getSize().height); running= true; life= true; // Создать объект потока thr= new Thread (this); thr.start ( );

// Свойство Number public lntgetNumber ( ) {return number;> // Свойство Point public Point getPolnt ( ) {return p;} // Свойство LeftRight public boolean getLeftRight ( ){return leftRight;> public void setLeftRight (boolean LeftRight) {thls.leftRight= LeftRight;} // Свойство Full public boolean getFull (){return full;} // Завершить поток


public void Finish ( ) {llfe= false;>; public void Stop ( )

{

>

// Приостановить поток

running= false;

synchronized public void Run ( ) // Возобновить поток <

>

running= true; notify();

// Потоковая функция public void run ( )

{

while (life) { try { synchronized (this) {

>

>

if(!runnlng) walt ( ) ;

// Подождать

catch(InterruptedException e) { } // Грузовик достиг справа левый склад lf(p.x <= leftWH.getLocation().x+leftWH.WIDTH) { System.out.println ("the left warehouse");

}

if (leftRight) // Пересылка слева направо {if(leftWH.Get(full)) full= true;> // Загрузить груз else {if(leftWH.Put(full)) full= false;} // Выгрузить груз dX= -dX;

// Грузовик достиг слева правый склад if(p.x >= rightWH.getLocation().x) { //System.out.println ("the right warehouse");

}

if (leftRight) // Пересылка слева направо <if(rightWH.Put(full)) full= false;} // Выгрузить груз else {if(rightWH.Get(full)) full= true;} // Загрузить груз dX= -dX;

// Обслужить в зоне контроля // Вошли в зону контроля


if (rect.contains (p) && !inContrl) // Вошли в зону

{ //System.out.println ("enter in the region"); inContrl= true; synchronized(reglon)

{

>

>

region.Work ( );

if (!rect.contains (p) && inContrl) // Вышли из зоны {

>

//System.out.println ("leave the region"); inContrl= false;

try

{

>

>

>

>

Thread.sleep (timePeriod);

catch (InterruptedException e){} p.x += dX;

///////////////

// C + + / C L I File ccLorry класс L o r r y #include "stdafx.h" #using <System.Drawing.dll> #using <System.Wlndows.Forms.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing; using namespace System::Threading; using namespace System::Collections; using namespace ccWarehouseDII; using namespace ccContrlRegionDII; namespace ccLorryDII { // Класс Lorry public ref class Lorry { int number; Point ^p; int dX; bool leftRight; ContrlRegion ^region; bool inContrl;

// Номер объекта компонента // Координаты объекта компонента // Приращение координаты X // Направление перемещения груза // Ссылка на зону контроля // Признак нахождения в зоне контроля

bool life; bool run; Thread ^thr; int timePeriod; Warehouse ^leftWH;

// Признак жизни потока // Признак выполнения потока // Ссылка на объект потока // Временной интервал перемещения // Ссылка на левый склад


Warehouse ^rightWH;

// Ссылка на правый склад

// Конструктор public: Lorry (int Number, lnt Y, int DX, Warehouse ^LeftWH, Warehouse ^RightWH, ContrlRegion ^Reglon)

{

number= Number; dX= DX; leftRlght= true; leftWH= LeftWH; rightWH= RightWH; timePerlod= 100; region= Region; inContrl= false; p= gcnew Point ( ); p->X= leftWH->Location.X+leftWH->Width + 1; p->Y= Y;

>

// Создать объект потока run= true; llfe= true; thr= gcnew Thread (gcnewThreadStart (this, &Lorry::Moving)); thr->Start ( );

// Свойство Number property lnt Number { lnt get() {return number;» // Свойство Point property Polnt^ P {Polnt^ get() {return p ; » // Свойство LeftRight property bool LeftRight {

>

void set (bool value) {leftRlght= value;> bool get () {return leftRight;}

// Освободить ресурс vold DelResource ( ) {

>

disposed= true;

// Завершить поток void Rnish ( ) {life= false; thr->3oin ( ) ; } // Приостановить поток void Stop ( ) { run= false;

} // Возобновить поток void Run ( ) { run= true; Monitor::Pulse (this); // Разблокировать поток

} // Потоковая функция компонента


private: vold Moving ( ) { while (life)

i

if (I run) Monitor::Walt (this); // Ожидать разблокировки // Объект компонента достиг справа // объекта левого склада lf((p->X) <= (leftWH->Location.x+leftWH->Width)j { Console::WrlteLine ("Левая граница");

>;

if (leftRight) // Пересылка слева направо leftWH->Get ( ); // Загрузить груз else leftWH->Put ( ); // Выгрузить груз dX= -dX;

// Объект компонента достиг слева // объекта правого склада if(p->X >= rightWH->Location.X) { Console::WrlteLine ("Правая граница");

>

if (leftRight) // Пересылка слева направо rightWH->Put ( ) ; // Выгрузить груз else rightWH->Get ( ); // Загрузить груз dX= -dX;

// Обслужить в зоне контроля Point ^pR= gcnew Point (region->Location.X, region->Location.Y); Rectangle ^rect= gcnew Rectangle (pR->X, pR->Y, reglon->Width, region->Height); if(!disposed) { // Вошли в зону контроля if (rect~>Contains (*p) && !inContrl) { // Вошли в зону контроля Console::WriteLine ("Вошли в зону"); inContrl= true;

>

// Захватить разделяемый ресурс Monitor::Enter (region); region->Work ( ) ; // Контролируемся // Освободить разделяемый ресурс Monltor::Exit (region);

if (!rect->Contains ( ж р) && inContrl)

{


// Вышли из зоны

>

>

Console::WriteLine ("Вышли из зоны"); inContrl= false;

Thread::Sleep (tlmePeriod); p->X += dX;

>; Проверить функционирование компонента Lorry позволит приложение csTestLorry, приведённое в примере 12.4.2.3. К приложению надо подсоединить библиотеки csLorry, csWarehouse и csContrlRegion, содержащие компоненты Lorry, Warehouse и ContrlRegion. Пример 12.4.2.3. Приложение csTestLorry. /////////////// / / C # File csTestLorry using System; using System.Threadlng; using System.Drawing; using System.ComponentModel; using System.Wlndows.Forms; using csWarehouseDII; u s i n g csContrlRegionDII; u s i n g csLorryDII; class TestLorry: Form { Lorry lorry; // Ссылки на объект грузовика Warehouse leftWH; // Ссылки на объект левого склада Warehouse rlghtWH; // Ссылки на объект правого склада ContrlRegion region; // Ссылка на объект зоны контроля Thread thread; // Ссылка на объект потока перерисовки bool life; // Признак жизни потока перерисовки Button butRun, butStop; // Ссылки на интерфейсные кнопки Button butAdd, butDel; // Ссылки на интерфейсные кнопки ListBox listBox; // Список номеров грузовиков int numLorry= 0; // Номер грузовика int Y; // Координата Y пути грузовика Random rand; // Ссылка на случайное число ArrayList aL; // Ссылка на список объектов грузовиков bool leftRight; // Признак направления перемещения грузовиков // Конструктор LorryAndWarhouses ( )

<

this.Text= "Test"; thls.CllentSlze= new Size (350, 130); aL= new ArrayList ( ) ;


rand= new Random ( ); leftRight= true; // Создать кнопки butRun= new Button ( ) ; // Создать кнопку пуска butRun.Location= new Point (240,5); butRun.Size= new Slze (45, 20); butRun.Text= "Run"; butRun.BackColor= Color.LightGray; butRun.Click += new EventHandler(But_Run); this.Controls.Add (butRun); // butStop= new Button ( ) ; // Создать кнопку останова butStop.Location= new Point (290,5); butStop.Size= new Slze (45, 20); butStop.Text= "Stop"; but5top.BackColor= Color.LightGray; butStop.Click += new EventHandler (But_Stop); this.Controls.Add (butStop); // butAdd= new Button ( ) ; // Создать кнопку добавления butAdd.Location= new Point (240,30); butAdd.Size= new Size (65, 20); butAdd.Text= "Добавить"; butAdd.BackColor= Color.UghtGray; butAdd.Click += new EventHandler (But_Add); this.Controls.Add (butAdd); butDel= new Button ( ) ; // Создать кнопку удаления butDel.Location= new Point (240,60); butDel,Size= new Size (65, 20); butDel.Text= "Удалить"; butDel. BackColor= Color. Lig htGray; butDel.Click += new EventHandler (But_Del); this.Controls.Add (butDei); listBox= new ListBox ( ) ; // Создать элемент списка listBox.Location= new Point (240, 90); listBox.Size= new System.Drawing.Slze (60, 20); this.Controls.Add (listBox); // Создать склады // Создать объект левого склада leftWH= new Warehouse (true, true); leftWH.Location= new Polnt (10, 10); leftWH.Size= new Size (30, 100); leftWH.BackColor= Color.White; leftWH.evFromWarehouse+= new DelEvFromWarehouse (this. EvFromWarehouseHandler); this.Controls.Add (leftWH); leftWH.Show ( ) ; // Создать объект правого склада rlghtWH= new Warehouse (false, false); rightWH.Location= new Point (200, 10); rightWH.Size= new Size (30, 100); rig htWH. BackColor= Color.White; this.Controls.Add (rightWH); rightWH.evFromWarehouse+= new DelEvFromWarehouse


>

(this.EvFromWarehouseHandler); rightWH.Show ( ) ; // Создать область контроля reglon= new ContrlRegion ( ); region.Location= new Point (100, 0); region.Size= new Size (40, ClientSize.Height); reglon,BackColor= Color.green; this.Controls.Add (region); region,Show ( ); // Создать поток перерисовки и запустить его iife= true; thread= new Thread (new ThreadStart (ThrPaint)); thread.Start();

// Обработать нажатие кнопки пуска private void But_Run (object о, EventArgs e) { IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ) ) {

>

>

Lorry lorry= (Lorry) inum.Current; lorry.Run ( );

// Обработать нажатие кнопки останова private void But Stop (object о, EventArgs e ) { IEnumerator lnum= aL.GetEnumerator (); while (inum.MoveNext ( ) ) {

>

>

Lorry lorry= (Lorry) inum.Current; lorry.Stop ( ) ;

// Обработать нажатие кнопки добавления грузовика private void But_Add (object о, EventArgs e) {

>

numLorry++; Y += 20; lnt dX = rand.Next (5,10); if (leftRight) dX= dX; else dX= -dX; lorry= new Lorry (numLorry, Y, dX, leftWH, rightWH, region); aL.Add (lorry); llstBox.Items.Add (lorry.Number);

// Обработать нажатие кнопки удаления грузовика private vold But_Del (object о, EventArgs e ) {


if (listBox.SelectedIndex == -1)

{

>

MessageBox.Show ("Выберете номер удаляемого грузовика в" + "\пэлементе списка перед нажатием кнопки");

else < int numSel= (int) listBox.SelectedItem; llstBox.Items,Remove (numSel); IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( )) { Lorry lorry= (Lorry) inum.Current; if (lorry.Number == numSel) {

>

>

>

>

lorry.Finish(); aL.Remove (lorry); listBox.Items.Remove (lorry.Number); return;

// Потоковая функция перерисовки private void ThrPaint ( ) { while (life) {

>

>

Invalidate ( ) ; Thread.Sleep (150);

// Завершить поток перерисовки public void Finish ( ) {

>

life= false; thread.Join ( ) ;

// Обработать кнопку закрытия окна protected override vold OnClosed (EventArgs e) { base.OnClosed (e); IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ) )

{

>

Lorry lorry= (Lorry) inum.Current; lorry.Finish (); lorry= null;

Finish ( );


// Перерисовать область клиента прикладного окна protected override void OnPaint (PalntEventArgs e) { base.OnPaint (e); IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( )) { Lorry lorry= (Lorry) inum.Current; e.Graphics.DrawEllipse (new Pen(Color.Blue, 2), lorry.Polnt.X - 10, lorry,Point.Y - 10, 20, 20); if(lorry.full) //1 e.Graphics.FillEllipse (new SolidBrush (Color.Blue), lorry.Polnt.X - 10, lorry.Polnt.Y - 10, 20, 20);

>

>

e.Graphics.DrawString (lorry.Number.ToString ( ), Font, new SolidBrush (lorry.LeftRlght?Color.Red:Color.Green), lorry.Point.X + 10, lorry.Polnt.Y + 10); if(leftRight) e.Graphlcs.FIIIRectangle (new SolidBrush (Color.Red), 240, 106, 100, 20); else e.Graphics.FillRectangle (new SolidBrush (Color.Green), 240, 106, 100, 20);

// обработать событие склада vold EvFromWarehouseHandler (object ob, WarehouseEventArgs arg) { // Изменить направление перемещения грузовиков lf(arg.Left && !arg.Full) leftRight= false; if (!arg.Left && !arg.Full) leftRight= true; IEnumerator inum= aL.GetEnumerator ( ) ; while (inum.MoveNext ( ) )

{

>

Lorry lorry= (Lorry) inum.Current; lorry.LeftRight= leftRight;

// Выдать информацию о направлении перемещения и // о складе, объект которого сгенерировал событие Console.Write("eventevFromWarehouse leftRight=" + leftRight); if (arg.Left) Console.Write("CKnafl leftWH "); else Console.Wrtte(" Склад rightWH "); if (arg.Full) Console.WriteLine(" полный");


else

>

Console.WriteLine(" пустой");

// Основная функция static void Maln ( )

{

>

>

Application.Run (new LorryAndWarhouses ());

Класс TestLorry прикладного окна содержит объекты leftWH и rightWH складов, объект region зоны контроля и объекты lorryl и lorry2 компонента Lorry. Конструктор класса прикладного окна создаёт объекты складов, объект зоны контроля, а также два объекта компонента Lorry и запускает последние, применив функцию Start(). Также создаётся и запускается специальный поток thr, потоковая функция Go() которого инициирует перерисовку объектов класса с их номерами в области клиента прикладного окна. Обработчик WarehouseHandler() события evFromWarehouse складов меняет значение признака leftRight направления перемещения грузовиков при обнулении или переполнении складов и передаёт это значение объектам lorryl и lorry2 компонента Lorry. При этом объекты компонента Lorry начнут перемещаться в противоположном направлении. На рис. 11.4.2.1 изображено прикладное и консольное окно приложения csTestLorry. 0B -<. VA ^,^|*.|w !fi-.tb'lv'.Ml^4't>l:jy^--.'l Cll/<.^-

-|nj xj

Рис. 11.4.2.1. Прикладноеиконсольноеокнапрограммы csTestLorry.

11.4.3. Третий этап. Разработка приложения csLorryAndWarehouse Наконец, мы приступили к разработке csLorryAndWarehouse приложения, которое должно удовлетворить пожеланиям пользователя этой программы, высказанным в разделе 11.1. Прикладное окно должно включать требуемые управляющие элементы, а программа - список грузовиков, позволяю-


щий включить произвольное количество объектов типа Lorry и выполнить над этими объектами необходимые операции. Диаграмма классов приложения csLorryAndWarehouse изображена на рис. 11.4.3.1. *LorrvAndWarhouse Form -lorry: Lorry -leftWH:Warehouse -rightWH: Warehouse •^ *Warehnu8e1 -region: ContrlRegion -thread: Thread ^l +ContrlReqion 1 -butRun,butStop: Button -butAdd, butDel: Button ^ *Button I -listBox: ListBox -aL: ArrayList *j *LlstBox~| -leftRight: bool -rand: Random -life: bool $\ +ArravList~^~ -numlorry: int -Y: int ^l +Random I 4iorryAndWa rho uses() « use » -But_Run() >UAnnlicatlon I -But_Stop() -But Add() -BufDel() -ThrPaint() #OnPeint() +EvFromWarehouseHandlerfl) 4Main() +Finish() #OnClos8d()

^o

~^i +Lorrv^

Рнс. 11.4.3.1. Диаграмма классов приложения csLorryAndWarehousc Класс LorryAndWarehouse прикладного окна кроме объектов leftWH и rightWH складов, объекта region зоны контроля, объекта thread потока, присутствующих в классе TestLorry прикладного окна приложения csTestLorry предыдущего раздела 11.4.2, содержит объекты butRun, butStop, butAdd, butDel кнопок, объект listBox списка номеров грузовиков и объект aL списка объектов грузовиков. При нажатии на кнопку butAdd создаётся очередной объект типа Lorry, он включается в список aL типа ArrayList, а его номер включается в список listBox типа ListBox. Затем запускается поток созданного объекта, обеспечивающий его перемещение. Если выбрать в списке listBox, представленном в прикладном окне, номер грузовика и нажать на кнопку butDel, то этот объект будет удалён из aL. и listBox и уничтожен. Нажатие на кнопки butStop и butRun вызывает соответственно приостановку движения всех грузовиков или возобновление их движения. В классе LorryAndWarehouse прикладного окна описан обработчик EvFromWarehouseHandler() собьггия склада. Реагируя на переполнение или


опустошение складов, обработчик меняет направление перевозки rpys

пользовавшись переменной leftRight и свойством LeftRight объектов Lorry. Программа приложения csLorryAndWarehouse представлена в ре 11.4.3.1.

Пример 11.4.3.1. ПриложениесяЬоггуА^УУагеЬонзе. /////////////// /7 C #

F i l e csLorryAndWarehouse к л а с с

LorryAndWarhouses

using System;; using System.Col!ections; using System.Drawing; using System.Threading; using System.Windows.Forms; using csLorryDff; using csWarehouseDH; using csContrlRegionDll; n a m e s p a c e csLorryAndWarehouse { class LorryAndWarhouses: Form { Lorry lorry; / / С с ы л к и на о б ъ е к т грузовика Warehouse leftWH; / / Ссылки на объектлевого склада Warehouse rightWH; / / Ссылки на о б ъ е к т правого склада ContrlRegion region; // Ссылка на объектзоны контроля Thread thread; / / Ссылка на объект потока перерисовки bool life; // Признак жизни потока перерисовки Button butRun, butStop; / / Ссылки на интерфейсные кнопки Button butAdd, butDel; // Ссылки на интерфейсные кнопки ListBox iistBox; / / Список номеров грузовиков int numLorry= 0; / / Номер грузовика int Y; / / Координата Y пути грузовика Random rand; // Ссылка на случайное число ArrayUst aL; // Ссылка на список объектов грузовиков bool leftRight; Ц Признак направления перемещения грузовиков П Конструктор LorryAndWarhouses ( )

{

this.Text= "Test"; this.ClientSize= new Size (350, 130); aL= new ArrayList ( ); rand= new Random ( ); leftRlght= true; // Создать кнопки butRun= new Button ( ); // Создать кнопку butRun.Location= new Point (240,5); butRun.Size= new Size (45, 20); butRun.Text= "Run"; butRun.BackColor= Color.LightGray;

пуска


butRun.Click += new EventHandler (But_Run); this.Controls.Add (butRun); //-— butStop= new Button ( ); // Создать кнопку останова butStop.Locatlon= new Polnt (290,5); butStop.Size= new Size (45, 20); butStop.Text= "Stop"; butStop. BackColor= Color. Llg htGray; butStop.Click += new EventHandler (But_Stop); this.Controls.Add (butStop); //-— butAdd= new Button ( ); // Создать кнопку добавления butAdd.Location= new Point (240,30); butAdd.Slze= new Size (65, 20); butAdd.Text= "Добавить"; butAdd.BackColor= Color.LightGray; butAdd.Click += new EventHandler (But_Add); this.Controls.Add (butAdd); butDel= new Button ( ) ; // Создать кнопку удаления butDel.Location= new Point (240,60); butDel.Size= new Size (65, 20); butDel.Text= "Удалить"; butDel. BackColor= Color. Lig htGray; butDel.Cllck += new EventHandler (But_Del); this.Controls.Add (butDel); listBox= new ListBox ( ) ; // Создать элемент списка listBox.Location= new Point (240, 90); listBox.Size= new System.Drawing.Size (60, 20); this.Controls.Add (llstBox); // Создать склады // Создать объект левого склада leftWH= new Warehouse (true, true); leftWH,Location= new Point (10, 10); leftWH.Size= new Size (30, 100); leftWH,BackColor= Color.White; leftWH.evFromWarehouse+= new DelEvFromWarehouse (EvFromWarehouseHandler); this.Controls.Add (leftWH); leftWH.Show ( ); // Создать объект правого склада rlghtWH= new Warehouse (false, false); rightWH.Location= new Point (200, 10); rightWH.SIze= new Size (30, 100); rightWH. BackColor= Color.White; this.Controls.Add (rightWH); rightWH.evFromWarehouse+= new DelEvFromWarehouse (EvFromWarehouseHandler); rlghtWH.Show (); // Создать область контроля reglon= new ContrlRegion (); region.Location= new Point (100, 0); reglon.Size= new Slze (40, CllentSize.Height); region.BackColor= Color.green; this.Controls.Add (region);


>

region.Show ( ) ; // Создать поток перерисовки и запустить его life= true; thread= new Thread (new ThreadStart (ThrPaint)); thread.Start();

// Обработать нажатие кнопки пуска private void But_Run (object о, EventArgs e) { IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ) )

<

>

>

Lorry lorry= (Lorry) inum.Current; lorry.Run ( );

// Обработать нажатие кнопки останова private void But_Stop (object о, EventArgs e ) { IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ) )

i

>

>

Lorry lorry= (Lorry) inum.Current; lorry.Stop ( );

// Обработать нажатие кнопки добавления грузовика private void But_Add (object о, EventArgs e) {

>

numLorry++; Y += 20; int dX = rand.Next (5, 10); if (leftRight) dX= dX; else dX= -dX; lorry= new Lorry (numLorry, Y, dX, leftWH, rlghtWH, region); aL.Add (lorry); listBox.Items.Add (lorry.Number);

// Обработать нажатие кнопки удаления грузовика private void But_Del (object о, EventArgs e ) < if(listBox.SelectedIndex == - l )

{

>

else {

MessageBox.Show ("Выберете номер удаляемого грузовика в" + "\пэлементе списка перед нажатием кнопки");


int numSel= (int) listBox.SelectedItem; listBox.Items.Remove (numSel); IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ))

t

>

>

>

Lorry lorry= (Lorry) inum.Current; if (lorry.Number == numSel) {

>

lorry.Flnish(); aL.Remove (lorry); listBox.Items.Remove (iorry.NumDer); return;

// Потоковая функция перерисовки private void ThrPaint ( ) { while (llfe) {

>

>

Invalidate ( ); Thread.Sleep (150);

// Завершить поток перерисовки public void Finish ( ) {

>

life= false; thread.Join ( );

// Обработать кнопку закрытия окна protected override void OnClosed (EventArgs e) { base.OnClosed (e); IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ))

{

> >

Lorry lorry= (Lorry) inum.Current; lorry.Finish ( ); lorry= null;

Finish ( );

// Перерисовать область клиента прикладного окна protected override void OnPaint (PaintEventArgs e) { base.OnPaint (e); IEnumerator inum= aL.GetEnumerator ( ) ; while (inum.MoveNext ( )) {


Lorry lorry= (Lorry) Inum.Current; e.Graphics.DrawElllpse (new Pen(Color.Blue, 2), lorry.Point.X - 10, lorry.Point.Y - 10, 20, 20);

>

>

e.Graphlcs.DrawStrlng (lorry.Number.ToString ( ), Font, new SolidBrush (Color.Blue), lorry.Point.X + 10, iorry.Point.Y + 10);

// обработать событие склада public void EvFromWarehouseHandler (object sender, WarehouseEventArgs args)

{

leftRight= !leftRight; IEnumerator inum= aL.GetEnumerator ();

while (inum.MoveNext ( )) {

>

>

Lorry lorry= (Lorry) inum.Current; lorry.LeftRlght= leftRight;

// Основная функция static void Main ( )

{

}

>

>

Application.Run (new LorryAndWarhouses ());

///////////////

// Java и C# Flle cjLorryAndWarehouse класс LorryAndWarhouses importjava.awt.*; importjava.utll.*; import java,awt.event.*; import cJContrlReglon_DII.*; import <jWarehouse_DII.* ;

import cjLorry.*;

class LorryAndWarehouses extends Frame implements Runnable, Observer

{

Lorry lorry; // Ссылка на объект грузовика Warehouse leftWH; // Ссылка на объект левого склада Warehouse rlghtWH; // Ссылка на объект правого склада ContrlRegion region; // Ссылка на объект зоны контроля Button butRun, butStop; // Ссылки на интерфейсные кнопки Button butAdd, butDel; // Ссылки на интерфейсные кнопки Choice choice; // Ссылка на элемент списока Thread thread; // Ссылка но объект потока перерисовки boolean life; // Признак жизни потока перерисовки int numLorry= 0; // Номер грузовика boolean leftRight; // Признак направления перемещения грузовиков int Y= 20; // Координата Y пути грузовика


Random rand; ArrayList aL;

// Ссылка на случайное число // Ссылка на список объектов грузовиков

// Конструктор LorryAndWarehouses ( ) { this.setTltle ("Test"); // Установить заголовок окна this.setSlze (350, 180); // Установить размер окна this.setLayout (null); // Установить отсутствие // менеджера расстановки thls.setBackground(Color.lightGray); aL= new ArrayList ( ) ; // Создать объект списка грузовиков rand= new Random ( ); // Создать случайное число leftRight= true; // Установить направление "слева направо" // Создать кнопки // Создать кнопку "Add" добавления шара butAdd= new Button ("Add"); // Создать объект кнопки butAdd.setLocation (240,85); // Разместить в окне butAdd.setSlze (65, 20); // Установить размер кнопки butAdd.setBackground (Color.lightGray); this.add (butAdd); // Добавить кнопку в коллекцию окна // Подписать обработчик на событие кнопки ""Add" butAdd.addActionListener (new ActionListener ( )

{

public void actionPerformed (ActionEvent aE) { numLorry++; Y += 20; lnt dX = rand.nextInt (5) + 5; If (leftRight) dX= dX; else dX= -dX; // Создать объект грузовика lorry= new Lorry (numLorry, Y, dX, leftWH, rightWH, region); aL.add (lorry); // Включить объект в список грузовиков //lorry.Start (); // Стартовать поток грузовика // Добавить номер грузовика в элемент списка номеров choice.addItem (Integer.toStrlng(numLorry));

});

>

// Создать кнопку "Del" удаления butDel= new Button ("Del"); // Создать объект кнопки butDel.setLocation (240,115); // Разместить в окне butDel.setSlze (65, 20); // Установить размер кнопки butDel,setBackground (Color.llghtGray); thls.add (butDel); // Добавить кнопку в коллекцию окна // Подписать обработчик на событие кнопки "Del" butDei.addActionListener (new ActionListener ( ) { public void actionPerformed (ActionEvent aE) {


// Если есть элементы в списке, то удалить грузовик с // наименьшим номером или выбранным номером из // списка lf(choice.getItemCount()!=0)

{

String s= choice.getSelectedItem(); int num= Integer.parseInt(s); for (int i= 0; i<aL.size(); i++) < Lorry lorry= (Lorry) aL.get(i); lf(lorry.number== num) { lorry.Finish ( ) ;

»;

>

>

>

>

aL.remove (i); choice.remove(s); return;

// Создать кнопку "Run" пуска butRun= new Button ("Run"); // Создать объект кнопки butRun.setLocatlon (240,25); // Разместить в окне butRun.setSize (45, 20); // Установить размер кнопки butRun.setBackground (Color,lightGray); thls.add (butRun); // Добавить кнопку в коллекцию окна // Подписать обработчик на событие кнопки "Run" butRun.addActionLlstener (new ActionListener ( ) { public void actlonPerformed (ActionEvent aE)

{

»;

>

for (int 1= 0; l<aL,size(); i++) {

>

Lorry lorry= (Lorry) aL.get(i); lorry.Run ();

Ц Создать кнопку "Stop" останова butStop= new Button ("Stop"); // Создать объект кнопки butStop.setLocation (240,55); // Разместить в окне butStop.setSize (45, 20); // Установить размер кнопки butStop.setBackground (Color.llghtGray); thls.add (butStop); // Добавить кнопку в коллекцию окна // Подписать обработчик на событие кнопки "Stop" butStop.addActionListener (new ActionListener ( ) { public void actlonPerformed (ActionEvent aE) {


for (int i= 0; l<aL.slze(); i++) {

»;

>

>

Lorry lorry= (Lorry) aL.get(l); lorry-Stop ( ) ;

// Создать элемент списка номеров грузовиков choice= new Choice ( ) ; // Создать объект элемента списка cholce.setLocatlon (240, 145); Ц Разместить в окне cholce.setSize (60, 20); // Установить размер элемента thls.add (choice);

// Добавить элемент в коллекцию окна

// Создать склады // Создать объект leftWH левого склада leftWH= new Warehouse (true, true); // Создать объект склада leftWH.setLocation (10, 10); // Разместить в окне leftWH.setSlze (30, 100); // Установить размер склада leftWH,setBackground (Color.white); leftWH, bW,addObserver(this); thls.add (leftWH); // Добавить склад в коллекцию окна leftWH,show ( ); // Создать объект rightWH правого склада rlghtWH= new Warehouse (false, false); // Создать объект склада rlghtWH.setLocatIon (200, 10); // Разместить в окне rightWH.setSize (30, 100); // Установить размер склада rightWH.setBackground (Color.white); rightWH.bW.addObserver(thls); this.add (rightWH); // Добавить склад в коллекцию окна rightWH.show ( ) ; // Создать область region контроля region= new ContrlRegion ( ); // Создать объект области контроля region.setLocation (100, 0); // Разместить в окне region.setSize (30, 200); // Установить размер области region.setBackground (Color.green); this.add (region); // Добавить область в коллекцию окна // Создать поток thread перерисовки и запустить его life= true; thread= new Thread (this,"controlThread"); thread.start();

// Потоковая функция run перерисовки public void run ( ) { while (life) { repaint ( ); try

// Перерисовать окно


{

>

>

>

Thread.sleep (150);

catch (InterruptedException e){}

// Завершить поток перерисовки public vold Flnlsh ( ) {

>

fife— 1114.—

falrA> i u u w ,

// Завершить все потоки при закрытии прикладного скна public boolean handleEvent (Event e) { If (e,ld == Event,WINDOW_DESTROY)

{

// Завершить потоки грузовиков for (lnt 1= 0; l<aL,size(); !++) {

>

> >

Lorry lorry= (Lorry) aL.get(l); lorry,Finish ( ) ;

// Завершить поток перерисовки Finish (); // Завершить основной поток System.exit(0);

return (super.handleEvent(e));

// Перерисовать область клиента прикладного окна public vold paint (Graphics g) { // Нарисовать каждый грузовик с номером for (int 1= 0; l<aL.size(); i++) { Lorry lorry= (Lorry) aL.get(i); g.setColor (Color.blue); g.drawOval (lorry.p.x - 10, iorry.p.y - 10, 20, 20); if(lorry.getFull()) g.fillOval (lorry.p.x - 10, lorry.p.y - 10, 20, 20); if(lorry.getLeftRight()) g.setColor(Color.red); else g.setColor(Color.green); g.drawStrlng (Integer.toStrlng(lorry.number), lorry.p.x + 10, lorry.p.y + 10);

>

}

// Обработать уведомление склада public void update (Observable obs, Object args) { // Изменить направлеие грузовиков


leftRight= lleftRight; for (lnt i= 0; i<aL.size(); i++) {

>

Lorry lorry= (Lorry) aL.get(l); lorry.leftRlght= this.leftRight;

WarehouseEventArgs arg=(WarehouseEventArgs) args; // Изменить направление перемещения грузовиков if(arg.getLeft() && larg.getFull()) leftRight= false; if (larg.getLeft() && larg.getFull()) leftRight= true; // Завершить потоки грузовиков for(inti= 0; i<aL.slze(); i++) <

>

Lorry lorry= (Lorry) aL.get(i); lorry.setLeftRight(leftRight);

// Выдать информацию о направлении перемещения и // о складе, объект которого сгенерировал событие If (arg.getLeft()) System.out.prlnt("CKflafl leftWH "); else System.out.print(" Склад rightWH "); if (arg.getFull()) System.out.println(" полный"); else System.out.println(" пустой");

> // Выполнить основной поток приложения public static void main ( ) {

>

>

LorryAndWarehouses laWH= new LorryAndWarehouses ( ); laWH.show ( ) ;

///////////////

// C + + / C L I File ccLorryAndWarehouse класс LorryAndWarhouses #include "stdafx.h" #using <System.Drawing.dll> #using <System.Wlndows.Forms.dll> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing; using namespace System::Threading;


using namespace System::Collections; using namespace cJLorryDII; using namespace pjWarehouseDII; using namespace pjContrlRegionDII; refclassLorryAndWarehouses: Form { Lorry ^lorry; // Ссылки на объект грузовика Warehouse ^leftWH; // Ссылки на объектлевого склада Warehouse ^rlghtWH; // Ссылки на объект правого склада ContrlRegion ^rsQiOnj // Ссыякз нз объсктзоны контроля Thread ^thread; // Ссылка но объект потока перерисовки bool llfe; // Признак жизни потока перерисовки Button ^butRun, ^butStop;// Ссылки на интерфейсные кнопки Button ^butAdd, ^butDel; // Ссылки на интерфейсные кнопки ListBox ^listBox; // Список номеров грузовиков int numLorry; // Номер грузовика lnt Y; // Координата Y пути грузовика Random ^rand; // Ссылка на случайное число ArrayList ^aL; ; // Ссылка на список номеров грузовиков bool leftRight; // Признак направления перемещения грузовиков // Конструктор public: LorryAndWarehouses ( ) { thls->Text= "Test"; thls->ClientSlze= Drawing::Size (350, 130); numLorry= 0; aL= gcnew ArrayList (); // Создать список номеров грузовиков rand= gcnew Random (); // Создать случайное число leftRlght= true; // Двигаться слева направо // Создать кнопки butRun= gcnew Button ( ); // Создать кнопку пуска butRun-> Location= Point (240,5); butRun-> Size= Drawlng::Size (45, 20); butRun-> Text= "Run"; butRun-> BackColor= Color::LightGray; butRun-> Click += gcnew EventHandler (thls, &LorryAndWarehouses::But_Run); this-> Controls-> Add (butRun); // butStop= gcnew Button (); // Создать кнопку останова butStop-> Location= Point (290,5); butStop-> Size= Drawlng::Size (45, 20); butStop-> Text= "Stop"; butStop-> BackColor= Color::LightGray; butStop-> Click += gcnew EventHandler (this, &LorryAndWarehouses::But_Stop); this-> Controls-> Add (butStop); //-— butAdd= gcnew Button ( ) ; // Создать кнопку добавления butAdd-> Location= Point (240,30); butAdd-> Slze= Drawing::Slze (65, 20);


butAdd-> Text= "Добавить"; butAdd-> BackColor= Color::LightGray; butAdd-> Click += gcnew EventHandler (thls, &LorryAndWarehouses::But__Add); this-> Controls-> Add (butAdd); butDel= gcnew Button ( ) ; // Создать кнопку удаления butDel-> Location= Point (240,60); butDel->Size= Drawing::Size (65, 20); butDel->Text= "Удалить"; butDel-> BackColor= Color::LightGray; butDei-> Ciick += gcnew EventHanaier (this, &LorryAndWarehouses::But_Del); this-> Controls-> Add (butDel); listBox= gcnew ListBox ( ) ; // Создать элемент списка listBox-> Location= Point (240, 90); listBox-> Size= Drawing::Slze (60, 20); this-> Controls-> Add (listBox); // Создать склады // Создать объект левого склада leftWH= gcnew Warehouse (true, true); leftWH-> Location= Point (10, 10); leftWH-> Slze= Drawing::Size (30, 100); leftWH-> BackColor= Color::White; leftWH-> evFromWarehouse+= gcnew DelEvFromWarehouse (thls, &LorryAndWarehouses::EvFromWarehouseHandler); this-> Controls-> Add (leftWH); leftWH-> Show ( ) ; // Создать объект правого склада rightWH= gcnew Warehouse (false, false); rightWH-> Location= Point (200, 10); rightWH-> Size= Drawing::Size (30, 100); rightWH-> BackColor= Color::White; this-> Controls-> Add (rightWH); rightWH->evFromWarehouse+= gcnew DelEvFromWarehouse (this, &LorryAndWarehouses::EvFromWarehouseHandler); rightWH-> Show ( ) ; // Создать область контроля region= gcnew ContrlRegion ( ); region-> Location= Point (100, 0); region-> Size= Drawing::Size (40, CiientSlze.Height); region-> BackColor= Coior::LightSkyBlue; this-> Controls-> Add (region); region-> Show ( ) ; // Создать поток перерисовки и запустить его life= true; thread= gcnew Thread (gcnew ThreadStart (this, &LorryAndWarehouses::ThrPaint)); thread-> Start ( );

// Обработать нажатие кнопки пуска private: void But_Run (Object Л о , EventArgs л е ) {


IEnumerator ^lnum= aL-> GetEnumerator ( ); while (inum-> MoveNext ( ) ) {

>

>

Lorry ^lorry= (Lorry^) lnum-> Current; lorry-> Run ( );

// Обработать нажатие кнопки останова void But_Stop (Object л о , EventArgs ^e ) < IEnumerator ^lnum= aL->GetEnumerator ( ) ; while (lnum-> MoveNext ( ) ) {

>

>

Lorry ^lorry= (Lorry^) inum->Current; lorry-> Stop ( );

// Обработать нажатие кнопки добавления грузовика void But_Add (Object ^o, EventArgs ^e) { numLorry++; Y += 20; int dX = rand-> Next (5, 10); If (leftRight) dX= dX; else dX= -dX; lorry= gcnew Lorry (numLorry, Y, dX, leftWH, rightWH,

>

region);

aL-> Add (lorry); listBox-> Items-> Add (lorry-> Number);

// Обработать нажатие кнопки удаления грузовика void But_Del (Object ^o, EventArgs ^e ) { if (listBox-> SelectedIndex == -1) {

>

MessageBox::Show ("Выберете номер удаляемого грузовика в" + "\пэлементе списка перед нажатием кнопки");

else { int numSel= (int) llstBox-> SelectedItem; listBox-> Items-> Remove (numSel); IEnumerator ^inum= aL->GetEnumerator ( ); while (inum->MoveNext ( )) { Lorry ^lorry= (Lorry^) inum-> Current; if (lorry-> Number == numSel)

{ //if(!lorry-> Runnlng)lorry-> Run ( ); // с м . д а л ь ш е //lorry-> DelResource ( ); // см. д а л ь ш е


>

>

>

>

lorry-> Finish(); aL-> Remove (lorry); listBox-> Items->Remove (lorry-> Number); return;

// Потоковая функция перерисовки vold ThrPaint ( ) { while (life) {

>

>

Invalidate ( ); Thread: :Sleep (150);

// Завершить поток перерисовки public: void Finish ( ) {

>

life= false; thread-> Join ( ) ;

// Обработать кнопку закрытия окна protected: virtual void OnClosed (EventArgs л е ) override { Form::OnClosed (e); IEnumerator ^inum= aL-> GetEnumerator ( ); while (inum-> MoveNext ( ) ) {

> >

Lorry ^lorry= (Lorry^) inum-> Current; lorry-> Finish ( ); lorry= nullptr;

Finish ( ) ;

// Перерисовать область клиента прикладного окна protected: virtual void OnPaint (PaintEventArgs ^e) override { Form::OnPaint (e); IEnumerator ^lnum= aL-> GetEnumerator ( ) ; while (inum-> MoveNext ( ) ) { Lorry ^lorry= (Lorry^) inum-> Current; e->Graphics-> DrawEllipse (gcnew Pen(Color::Blue, 2), lorry-> P-> X - 10, lorry-> P-> Y - 10, 20, 20); e->Graphics->DrawString (lorry-> Number.ToString ( ), Font,


gcnew SolidBrush (Color::Blue), lorry-> P-> X + 10, lorry-> P-> Y + 10);

> // обработать событие склада public: void EvFromWarehouseHandler (Object ^sender, WarehouseEventArgs ^args)

i

leftRight= !leftRight; IEnumerator ^inum= aL-> GetEnumerator (); while (inum-> MoveNext ( ) )

{

>

>

Lorry ^lorry= (Lorry^) lnum-> Current; lorry-> LeftRight= leftRight;

>; void main ( ) {

>

Application::Run( gcnew LorryAndWarehouses ());

В программе приложения csLorryAndWarehouse, представленной в примере 11.4.3.1, описан класс LorryAndWarhouses прикладного окна. Класс LorryAndWarhouses прикладного окна приложения включает интерфейсные элементы пользователя, объекты leftWH и rightWH складов, объект region зоны контроля, объект aL списка грузовиков и объект thread потока перерисовки области клиента этого окна. Потоковая функция ThrPaint() циклически через 150 мс инициирует перерисовку объектов списка aL в виде окружностей с номерами. Перерисовка осуществляется функцией OnPaint(), извлекающей необходимую информацию из списка. Может возникнуть вопрос, а почему бы не убрать этот поток thread перерисовки и осуществлять перерисовку при изменении приращения координат каждым объектом компонента Lorry? Для этого в класс Lorry пришлось бы ввести специальное собьггие, генерируемое в каждом цикле потоковой функции Moving() объекта этого класса. Ведь каждый объект класса Lorry может в своей потоковой функции Moving() иметь и разные значения аргумента timePeriod функции Sleep. Перерисовка при каждом цикле потоковой функции ThrPaint() каждого объекта компонента Lorry, осуществляемая по вышеуказанному специальному событию в классе LorryAndWarhouses, более точно бы отобразила перемещение грузовиков в прикладном окне этого класса. Так можно сделать, но это бы вызвало частую необоснованную перерисовку окна, неосязаемую нашим глазом. Вспомним, что отображение кадров в кино и на телевизоре осуществляется с достаточно низкой частотой,


вполне удовлетворяющей человечество. И здесь, в программе приложения csLorryAndWarehouse, решено поступить аналогичным образом. Конечно, в потоковой функции ThrPaint() можно указать и другое время цикла перерисовки, отличное от 150 мс. Читатель, заинтересовавшись в этом, может поэкспериментировать. Но всегда желательно не перегружать функции перерисовки излишней работой и вызывать эти функции тогда, когда это требуется. При закрытии окна выполняется функция OnClosed(), которая завершает выполнение потока каждого объект компонента Lorry и потока thread перерисовки. Прикладное окно программы csLorryAndWarchousc изображено на рис. 11.4.3.1.

Рис. 11.4.3.1. Прикладное и консольное окна программы csLorryAndWarehouse

11.4.4. Четвертый этап. Удаление ресурса В отладке этой программы создавалось разное количество грузовиков, их движение приостанавливолось и возобновлялось, грузовики удалялись и добавлялись. И возникли неприятные ситуации, когда удаляемые грузовики не удалялись, и при этом появлялось аварийное завершение программы. Как правило, грузовики не удалялись при нажатии на кнопку "Del", когда они перед этим приостанавливались кнопкой "Stop" или находились в приостановленном состоянии в области контроля. В программе что-то не хватало в параллельном взаимодействии потоков грузовиков. В частности, когда приостановившиеся грузовики пытались захватить ресурс и выполнить функцию Work(). Применение параллельно выполняющихся потоков и, в особенности, применение при этом потоков, использующих разделяемые ресурсы, требует тщательного анализа их совместной работы. Здесь возможны дедлоки (deadlock) - ^пиковые или безысходные ситуации, когда один поток захватил один ресурс и не может выполняться, поскольку ему нужен ресурс, захваченный вторым потоком, а второй поток не может выполняться, поскольку ему нужен ресурс, захваченный первым потоком. И эти потоки никогда не дождутся освобождения ресурсов - программа встанет


Чтобы исключить такую ситуацию, прежде всего, необходимо придерживаться правила, чтобы каждый поток освобождал pecyp тотчас, как он ему более не нужен. Желательно, чтобы перед захватом ресурса поток проверил, свободин ли он? И если ресурс занят, то поток начал бы выполнять другую нужную работу, а позднее ещё сделал бы попьггку завладения этим ресурсом. Грубейшей ошибкой является удаление объекта, не освободившего используемые им ресурсы. В нашей программе удалялись (уничтожались) объекты грузовиков, не освободившие используемый ими ресурс region. После выбора номера грузовика и нажатия на кнопку "Del", выполняется обработчик But_DeI(), который прекращает поток, вызвав функцию Finish(). Но поток-то может стоять в очереди к ресурсу! Положение усугубляется, когда все грузовики остановлены нажатием кнопки "Stop" и после этого они удаляются выбором номера грузовика и нажатием на кнопку "Del". Грузовики вне области контроля обычно удаляются успешно. Но грузовики, захватившие ресурс и посему приостановленные дважды (их потоки приостановлены, поскольку они в очереди к ресурсу, и еще приостановлены выполнением функции Stop()) не удаляются, при этом возникает аварийная ситуация. Если грузовик приостановлен, то вызов функции Stop() для прекращения потока неразумен, так как стоящий в очереди грузовик продолжит стоять, посколько функция Finish() присвоила переменной life значение false, и поток не возобновится для выполнения функции Exit() монитора, освобождающей ресурс. Значит для прекращения выполнения потока надлежит запустить грузовик и вызвать функцию Finish(), а до вызова этой функции освободить ресурс. В примере 11.4.3.2 приведён модифицированный класс Lorry с функцией Dispose(), осуществляющей освобождение ресурса.. Пример 11.4.3.2. Класс Lorry с функцией DisposeO. /////////////// // C# using using using using using using

File csLorry класс Lorry System; System.Threading; System.Drawing; System.Windows.Forms; csWarehouseDII; csContrlRegionDll;

namespace csLorryDII { // Класс грузовика public class Lorry { int number; Polnt p; int dX; bool leftRight; ContrlRegion region; bool lnContri;

// Номер объекта компонента // Координаты объекта компонента // Приращение координаты X // Направление перемещения груза П Ссылка на зону контроля // Признак нахождения в зоне контроля


Random rand; bool life; bool run; Thread thr; int tlmePeriod= 100; Warehouse leftWH; Warehouse rightWH; bool disposed= false;

// Ссылка на объект случайного числа // Признак жизни потока // Признак выполнения потока // Ссылка на объект потока // Временной интервал перемещения // Ссылка на левый склад // Ссылка на правый склад И Признак освобождения ресурса

// Конструктор public Lorry (int Number, intY, int DX, Warehouse LeftWII, Warehouse RightWH, ContrlRegion Region)

{

>

number= Number; dX= DX; leftRight= true; leftWH= LeftWH; rightWH= RightWH; region= Region; inContrl= false; p= new Point (); p.X= leftWH. Location.X+leftWH.Width + 1; p.Y= Y; rand= new Random ( ); // Создать объект потока run= true; life= true; thr= new Thread (new ThreadStart (Moving)); thr.Start ();

// Свойство Number public lnt Number {get {return number;» // Свойство Point {get {return p ; » public Point Point // Свойство LeftRight public bool LeftRight {

>

set {leftRlght= value;> get {return leftRight;}

// Свойство Running public bool Running {get {return r u n ; »

|| Освободить ресурс public void Dispose ( ) {

>

disposed= true;

// Завершить поток public void Finish ( ) {llfe= false; thrJoin ( );} // Приостановить поток public void Stop ( ) { run= false;


// Возобновить поток public void Run ( ) { lock (this)

{

>

// Создать синхронизируемый блок

run= true; Monltor.Pulse (this); // Разблокировать поток

> // Потоковая функция компонента private void Moving ( ) { while (life)

<

lock (this)

{

>

// Создать синхронизируемый блок

if (I run) Monitor.Wait (this); // Ожидать // разблокировки

// Объект компонента достиг справа // объекта левого склада If (p,X <= leftWH.Locatlon.X+leftWH,Wldth) { Console.WriteLine ("Левая граница");

>

if (leftRight) // Пересылка слева направо leftWH.Get ( ); // Загрузить груз else leftWH.Put ( ); // Выгрузить груз dX= -dX;

// Объект компонента достиг слева // объекта правого склада if(p.X >= rightWH.Location.X) { Console.WriteLine ("Правая граница");

>

if (leftRight) // Пересылка слева направо rightWH.Put ( ); // Выгрузить груз else rlghtWH.Get (); //Загрузитьгруз dX= -dX;

// Обслужить в зоне контроля Point pR= new Point (region.Locatlon.X, region.Locatlon.Y); Rectangle rect= new Rectangle (pR.X, pR.Y, reglon.Width,


region.Height); if(!disposed) { // Вошли в зону контроля if (rect.Contalns (p) && !inContrl) // Вошли в зону { Console.WriteLine ("Вошли в зону"); inContrl= true; // Захватить разделяемый ресурс

>

Monitor.Enter (region); reglon,Work (); // Освободить разделяемый ресурс Monitor.Exit (reglon);

if (lrect,Contains (p) && inContrl)// Вышли из зоны { Console.WriteLine ("Вышли из зоны"); inContrl= false;

>

>

>

>

}

Thread.Sleep (timePeriod); p,X += dX;

В классе Lorry с функцией Dispose() примера 11.4.3.2 введена специальная переменная disposed, предотвращающая повторное освобождение ресурса. При создании объекта грузовика эта переменная принимает значение false, указывая, что ресурс ещё не освобождался функцией Dispose(). Вызов функции Dispose() присваивает переменной disposed значение true. Значение true изменяет функционирование грузовика. Теперь объект типа Lorry, возможно дождавшись освобождения уже захваченного им ресурса или ещё не захватив ресурс, не будет захватывать ресурс. Вышеизложенные размышления и введение функции Dispose() привело к изменению в обработчике кнопки "Del". Изменённая функция обработчика приведенав примере 11.4.3.3.. Свойство Running объекта типа Lorry позволяет выявить, перемещается ли грузовик (выполняется ли поток). Для завершения приостановленного потока, имеющего ложное значение свойства Running объекта, обработчик кнопки "Del" возобновляет поток, вызвав функцию Run(), а затем его завершает, вызвав функцию Finish(). Разумеется, до вызова функции Finish() вызывается функция Dispose() для освобождения ресурса.


Пример 11.4.3.3. Удаление объекта класса Lorry с функцией Dispose(). // Обработать нажатие кнопки удаления грузовика с функцией Dlspose() private void But_Dei (object о, EventArgs e )// { if(listBox.SelectedIndex == -1) {

>

else {

MessageBox. Show ("Выберете номер удаляемого грузовика в" + "\пэлементе списка перед нажатием кнопки");

// Получить выбранный номер из управляющего элемента int numSei= (int) listBox.SelectedItem; // Удалить номер из списка номеров грузовиков listBox.Items.Remove (numSel); // Найти объект грузовика с номером numSel IEnumerator inum= aL.GetEnumerator ( ); while (inum.MoveNext ( ) )

{

Lorry lorry= (Lorry) inum.Current; if (lorry.Number == numSel) { // Возобновить поток грузовика if(!lorry.Running)lorry.Run ( ); // Освободить ресурс lorry.Dispose ( ); // Завершить поток грузовика lorry.Finish(); // Удалить объект грузовика aL.Remove (lorry); // Удалить номер грузовика listBox.Items.Remove (lorry.Number); return;

>

>

>

>

В приложениях 1 и 2 книги приведены варианты разработанных программ на языках C# и J# (Java), содержащие все классы в одном соответствующем приложении.


12. Зачетные задачи Приведённые в данном разделе задачи позволят не только самостоятельно проверить уровень усвоения изложенного в книге материала, но также способствует попрактиковаться в разработке простых программ, в которых акцентировано внимание на наиболее трудно усвояемых языковых средствах языков C# и Java, связанных с событиями, уведомлениями и потоками. Для каждого задания приводится вариант результативной программы на языках C# и Java. Для облегчения разработки программ, каждое задание снабжено примечанием, которое может быть учтено только при возникновении трудностей. Игнорирование примечания способствует проявлению творчества;, созданию своих версий программ, которые можно будет сравнить с приведёнными версиями.

12.1. Задание 1 Программа содержит два объекта, представленных окнами и связанных друг с другом с помощью собьггий (уведомлений). Каждое окно содержит редактор текста. Прикладное окно содержит поток, обеспечивающий посредством событий (уведомлений) обмен содержимого редакторов текста через 1000 мс. Примечание: Поток прикладного окна циклически через 1000 мс посыпает событие (уведомление) другому окну, передав ему текст из своего редактора. Обработчик другого окна принимает этот текст и помещает его в свой редактор и отправляет событие (уведомление) прикладному окну с текстом своего редактора. Теперь обработчик прикладного окна, приняв этот текст, присваивает его редактору этого прикладного окна.

// Java Обмен текста между редакторами разных окон Importjava.awt.*; Import Java.awt.event.*; Importjava.util.*; // Класс наблюдаемого объекта class Obs extends Observable < public void performObs (String s) { setChanged ( ); notifyObservers (s); //или (new String(s));

}


class First extends Frame implements Observer

{

private TextField tBf; public Obs obs; // Конструктор public First ( ) <

// Ссылка на объект редактора текста // Ссылка на наблюдаемый объект

obs= n e w Obs ( ); // Создать наблюдаемый объект this.setTitle ("First"); setSize (140, 130); this.setVisible (true); this.setLayout (null); this.setBackground (Color,lightGray);

>

// Создать редактор текста tBf= new TextField ( ); tBf.setText ("first"); tBf.setLocation (10, 50); tBf.setSize (100, 30); this.add (tBf);

// Обработать уведомление от другого окна public void update (Observable ob, Object obj) {

>

>

// Запомнить текст редактора данного окна String st=tBf.getText (); // Получить текст редактора другого окна и String s=( (String)obj) .toString(); // поместить его в редактор данного окна tBf.setText (s); //Передать текст редактора данного окна в другое окно //(послать уведомление другому окну) obs.performObs (st);

// Класс прикладного окна class Second extends Frame implements Observer, Runnable { public Obs obs; private TextField tBs; private Thread t; private boolean life;

// Ссылка на наблюдаемый объект // Ссылка на объект редактора текста

//Конструктор public Second ( ) { this.setTitle ("Second"); setSize (240, 130); obs= n e w Obs ( ); // Создать наблюдаемый объект this.setLayout (null); this.setBackground (Color.lightGray);


this.setVlsible (true); // Создать редактор текста tBs= new TextField (); tBs.setLocation (10, 50); tBs.setText ("second"); tBs.setSlze (100, 30); thls.add (tBs); // Примененить внутренний анонимный класс для закрытия окна this,auuvVindowLlsiener (new VVindowAdapier ( ) { public vold wlndowCloslng (WindowEvent wE) { life= false; try {

>

»;

>

>

t.join ();

catch (InterruptedExceptlon e) { > System.exit (0);

// Создать и запустить поток life= true; t= new Thread (this); t.start ();

// Обработать уведомление от другого окна public void update(Observable obs, Object obj) {

>

// Получить текст редактора другого окна String s = ((String)obj).toString(); // Поместить полученный текст в редактор данного окна tBs.setText (s);

public void run ( ) { while(llfe)

{

// Получить текст из редактора данного окна String s= tBs.getText ( ); // Передать извлечённый текст в другое окно obs.performObs (s); // или (new String(s)); try {

>

Thread.sleep (1000);

catch(InterruptedExceptlon e) { >


dass Exchange < // Выполнить основной поток static public void main ( ) <

}

>

Second s= new Second ( ); // Создать объект прикладного окна // Создать объект другого окна First f= new First (); s.obs.addObserver (f); // Подписать окно-обозреватель f на // уведомление скна наблюдателя s f.obs.addObserver (s); // Подписать окно-обозреватель s на // уведомление окна наблюдателя f

/////////////// // C# using using using using

Обмен текста между редакторами разных окон System; System.Drawing; System.Threading; System.Windows.Forms;

delegate void Del (string s);

// Делегат события

class First: Form

<

TextBox tBf; public event Del ev;

// Ссылка на событие

// Конструктор public First ( ) {

>

thls.Text="First"; this.Vlslble=true; Slze= newSlze (140, 130); tBf= new TextBox (); tBf.Text="first"; tBf.Locatlon= new Point (10, 50); tBf.Size= new Size (100, 30); this.Controls.Add (tBf);

// Обработчик события public void Hand (string s) { // Запомнить текст из редактора текста данного окна string st=tBf.Text; // Поместить текст из редактора текста другого окна // в редактор данного окна tBf.Text= s; // Послать запомненный текст в редактор другого окна if (ev != null) ev (st);


// Класс прикладного окна class Second : Form { public event Del ev; TextBox tBs; Thread t; bool life;

// Ссылка на событие

~ . // /!\fi\unbl p^Klup

public Second ( ) { this.Text="Second"; Size= new Size (240, 130); tBs= new TextBox ( ); tBs.Text="second"; tBs.Location= new Point (10, 50); tBs.Size= new Size (100, 30); this.Controls.Add (tBs);

>

// Создать и стартовать поток life= true; t= new Thread (new ThreadStart(Run)); t.Start ( );

private vold Run ( ) { while(life) {

>

>

// Получить текст редактора данного окна string s= tBs.Text; // Передать ссылку на этот текст другому окну if (ev 1= null) this.ev (s); Thread.Sleep (1000);

// Обработчик события public void Hand (string s) <

>

// Поместить текст из редактора другого окна // в редактор данного окна tBs.Text= s;

protected override void OnClosed(EventArgs e) { life= false; tJoin ();


class Exchange { // Выполнить основной поток static void Main ( )

{

Second s= new Second (); First f= new First(); s.ev+= n e w Del (f.Hand); f.ev+= n e w Del (s.Hand);

> >

// Создать прикладное окно // Создать другое окно // Подписать обработчик окна f // на событие окна s // Подписать обработчик окна s // на событие окна f

Application.Run (s);

12.2. Задание 2 Программа содержит два объекта, представленных окнами и связанных друг с другом с помощью событий (уведомлений). В каждом окне перемещается горизонтально шар, отталкиваясь от сторон окна. Если расстояние между координатами x шаров меньше 20 пиксель, то шары окрашиваются в красный цвет. Иначе шары окрашены в синий цвет. Примечание. Шар является объектом класса, содержащего поток, перемещающий этот шар. Объект шара создаётся в каждом окне. При создании объекта шара ему передается ширина области клиента окна. В каждом цикле поток шара генерируетсобытие, передающее шару другого окна координату x данного шара, чтобы шар другого окна, сравнивая координаты, мог выявить их близость. Также собьггие шара используется при перерисовке его в собственном окне.

// Java Фиксация близости координат x шаров разных окон importjava.awt.*; importjava.awt.event,*; importjava.util.*; class Ball extends Observable implements Runnable, Observer

{

private private private private private private private

Thread t; boolean life; int w; int x; int x l = 0; Random r; Color c;

// Свойство Color public Color getColor ( ) {return c;} // Свойство X publicintgetX(){returnx;}

// Конструктор


public Ball (int W) { w= W; c= Color.blue; r= new Random ( ); x= r.nextInt(w-15) + 5; c= Color.blue; life= true; t= new Thread (this); t.start ( );

public void run ( ) { int dx= r.nextInt(10) + 5; whlle(life) { x+= dx; if(x<0 11 x>w) dx= -dx; c= Math.abs (x - x l ) < 20? Color.red: Color.blue; setChanged ( ); notifyObservers(new Integer (x)); try

{

>

>

>

Thread.sleep (1000);

catch (InterruptedException e) { >

public void update (Observable obs, Object obj)

i

>

// Получить координатышара другого окна x l = ((Integer)obJ).intVaiue();;

class Wind extends Frame implements Observer { public Ball b; private int x; // Конструктор public Wind ( ) { this.setTitle ("Wind"); setSize (140, 130); this.setVisible (true); b= new Ball (this.getSlze().wldth); b.addObserver (this); // Подписать окно-обозреватель // на уведомление шара-наблюдателя этого окна для // перерисовки после каждого цикла потоковой функции


// Обработать уведомление от шара для его перерисовки public void update (Observable ob, Object obj) { repaint ( );

} public void paint (Graphics g)

<

>

>

g.setColor (b.getColor()); g.drawOval (b.getX(), 50, 20, 20);

class Exchange { // Выполнить основной поток static public void main ( ) { Wind w l = new Wlnd ( ); // Создать объект прикладного окна Wlnd w2= new Wlnd ( ); // Создать объект другого окна w l . b . a d d O b s e r v e r (w2.b); // Подписать шар-обозреватель окна w2 // на уведомление шара-наблюдателя окна w l

>

>

w2.b.addObserver ( w l . b ) ; // Подписать шар-обозреватель окна w l // на уведомление шара-наблюдателя окна w2

/////////////// // C# using using using using

Фиксация близости координат x шаров разных окон System; System.Drawing; System.Threading; System.Windows.Forms;

delegate void Del (int x); // Делегат события class Ball { public event Del ev; Thread t; bool life; int w; int x; int x l = 0; Random r; Color с; // Свойство Color public Color Color { get {return с;>


// Конструктор public Ball (int W) {

>

w= W; r= new Random ( ); x= r.Next (1, w-15); c= Color.Blue; life= true; t= newThread (newThreadStart(Move)); t.Start ( );

private void Move ( )

{

>

int dx= r.Next(5, 20); while(life) {

>

Console.WriteLine ("--"); x+= dx; if(x<0 || x>w) dx= -dx; c= Math.Abs(x - x l ) < 20? Color.Red: Color.Blue; if (ev != null) this.ev (x); Thread,Sleep (1000);

public void BallHandler (int X) {

>

>

x l = X;

class Wind : Form

{ public Ball b; int x; // Конструктор public Wind ( ) { this.Text="Wind"; this.Visible=true; Size= new Size (140, 130); b= new Ball(this.ClientSize.Width); b.ev+= n e w Del (Hand);

} // Обработчик события public void Hand (lnt X) { x= X; Invalidate ( );


protected override void OnPalnt(PalntEventArgs e) {

>

>

base.OnPaint (e); e.Graphics.DrawEllipse(new Pen(b.Color,2), x, 50, 20, 20);

class Near { // Выполнить основной поток static void Main ( ) <

>

>

Wind w l = new Wind ( ); // Создать первое окно Wind w2= new Wind ( ); // Создать второе окно w l . b . e v + = n e w Del(w2.b.BaIIHandler); w2.b.ev+= n e w DeI(wl.b.BaliHandler); Application.Run (wl);

12.3. З а д а н и е 3 Программа содержит прикладное окно с кнопкой, при каждом нажатии на которую появляется движущийся шар голубого цвета. Шары отталкиваются от границ окна. При отталкивании от границы каждого шара вспыхивает специальный желтый шар, неподвижно размещённый в окне. Примечание. Объекты шаров создаются из класса, содержащего поток, перемещающий эти шары. Также этот поток генерирует собьггия (уведомления), обработчики которых осуществляют перерисовку окна и "поджигают" желтый шар. Удобно хранить объекты шаров в списке, что облегчает их совместную обработку.

///////////////

// Java Шары поджигают специальный объект importjava.awt.*; import java.awt.event.*; importjava.util.*; // Класс потокового объекта class Ball extends Observable implements Runnable

{

private int x, y; public int w, h; public int dx, dy; private Thread t; private boolean life;

// Координаты шара // Ширина и высота области клиента // Приращения координат шара // Ссылка на поток // Признак существования потока

// Свойство X public int getX ( ) {return x;} // Свойство public int getYY ( ) {return у;>


// Конструктор public Ball (lnt X, int Y, int Dx, lnt Dy, int W, int H) { w= w; h= H;

>

x= X; y= Y; dx= Dx; dy= Dy;

// Создать и стартовать поток life= true; t= new Thread (this); t.start ();

public void Finish ( )

{

life= false; try {

>

>

t.joln ( );

catch(InterruptedException e) { >

public void run ( ) { while (llfe) { x += dx; у += dy; if (x>w || x<0) {

>

dx= -dx; // Уведомить o6 отскакивании шара setChanged ( ); notifyObservers ( n e w Boolean (true));

if(y>h || y<0) {

>

dy= -dy; // Уведомить об отскакивании шара setChanged ( ); notifyObservers ( n e w Boolean (true));

// Уведомить о перерисовке окна setChanged ( ); notifyObservers ( n e w Boolean (false)); try {

>

>

>

>

Thread.sleep (100);

catch (InterruptedException e) { >

// Класс специального объекта class SpecialBall implements Runnable, Observer


int x, у; intd=l; Thread t; boolean running;

// Координаты шара // Ширина пера // Ссылка на поток // Признак выполнения потока

// Свойство X public int getX ( ) {return x;} // Свойство Y pubiic int getY ( ) {return у;> // Свойство D (толщина спей, шара для перерисовки) public int getD ( ) {return d;} // Конструктор public SpecialBall (int X, int Y) {

>

running= false; x= X; y= Y;

// Обработать уведомление от шаров public void update (Observable obs, Object obj) {

>

boolean bool= ((Boolean) obj).booleanValue(); if(bool) Start ();

// Выполнить поток для заполнения овала шара желтым цветом public void run ( ) { d= 4; // Овал шара заполнить желтым цветом try {

>

>

Thread.sleep (200);

catch (InterruptedException e) { > d= 1; // Овал шара нарисовать пером желтого цвета running= false;

// Создать и стартовать поток для заполнения овала шара цветом public vold Start ( ) { if(lrunnlng) { running= true; t= new Thread (this); t.start ();


// Класс прикладного окна class Wlnd extends Frame implements Observer

{

public SpecialBall specialBall; private Button butAdd; private ArrayList aL; private Random r; Object obj;

// // // //

Ссылка Ссылка Ссылка Ссылка

на на на на

спец.объект кнопку список объект случайного числа

// Конструктор pubiic wind ( ) { setSize (300, 200); setVisible (true); specialBall= new SpecialBall (30, 30); // Создать спец.объект aL= new ArrayList ( ) ; // Создать список r= new Random ( ) ; // Создать объект случайного числа butAdd= new Button("Add"); // Создать кнопку butAdd.setLocatlon (100, 24); // Разместить кнопку // Задать размер кнопки в области клиента окна butAdd.setSize (32, 24); add(butAdd); obj=this; // Подписать обработчик actionPerformed на событие кнопки butAdd.addActionLlstener (new ActionListener()

{

public void actionPerformed (ActionEvent aE) { int х— r.nextInt(50)+10; int y= r.nextInt(50)+10; int dx= r.nextInt(10)+5; int dy= r.nextInt(10)+5; Ball b= new Ball( x, у, dx, dy, 300, 200);

»;

>

b.addObserver ( ( W i n d ) obj); b.addObserver (specialBall); aL.add (b);

// Подписать обработчик windowClosing на событие закрытия окна this.addWindowListener(new WindowAdapter ( ) { public void windowClosing (WindowEvent wE) { for(lnt i= 0; i< aL.size(); I++) {

>

Ball b= (Ball) aL.get(i); b.Flnish ( );

System.exlt (0);


}); // Перерисовать public void paint (Graphics g) { for (int i= 0; i < aL.size(); i++) { Ball b= (Bail) aL.get(i); g.setColor (Co!or.blue); g.drawOval (b.getX(), b.getY(), 20, 20);

>

g.setColor (Color.yellow); g.drawOval (specialBall.getX(), specialBall.getY(), 20, 20); if(specialBall.getD() == 4) g.flllOval(specialBall.getX(), specialBall.getY(), 20, 20);

>

// Обработать уведомление перерисовки от шара public void update (Observable obs, Object obj) {

>

repaint ( ) ;

// Выполнить основной поток public static void main ()

{

>

Wind w= new Wind ();

/////////////// // C# using using using using using

Шары "поджигают" специальный объект System; System.Drawing; System.Windows.Forms; System.Threading; System.Collections;

delegate void delEv ( );

// Делегат события

// Класс потокового объекта class Ball { public public private private private private private

event delEv ev; // Объявление события event delEv evSpecial; // Объявление специального события int x, у; // Координаты шара int w, h; // Ширина и высота области клиента int dx, dy; // Приращения координат шара Thread t; // Ссылка на поток bool life; // Признак существования потока


// Свойство X public int X {get{return х ; » // Свойство Y public int Y {get{return у ; » // Конструктор pubiic Baii (int X, int Y, int Dx, int Dy, int w, int H) { w= W; b= H; x= X; y= Y; dx= Dx; dy= Dy;

>

// Создать и стартовать поток life= true; t= new Thread (new ThreadStart (BallFunc)); t.Start ();

public void Flnlsh ( ) {

>

life= false; t.3oln ();

void BallFunc ( ) { while (life) { x += dx; у += dy; lf(x>w || x<0) { dx= -dx; // Генерировать событие спец.объекту If (evSpecial != null) {

>

>

evSpecial ( );

if(y>h || y<0) { dy= -dy; // Генерировать событие спец.объекту if (evSpecial != null) <

>

>

evSpecial ( );

// Генерировать событие перерисовки окна if (ev 1= null) { ev();


>

>

>

Thread.Sleep (100);

// Класс специального объекта class SpecialBall

{

int x, у; E~l J _ III L U—

|| Координаты шара

//' и шИрИНа Псра // И Ссылка на поток II Признак выполнения

* . X,

Thread t; bool run;

потока

public int X {get{return х ; » public intY {get{return у ; » public int D <get{return d;>> // Конструктор public SpecialBall (lnt X, intY) {

>

run= false; x= X; y= Y;

// Обработать событие public void HandlerEvSpecial ( ) {

>

Start ( );

// Выполнить поток void ThrFunc ( ) {

>

d= 4; // Нарисовать шар толстым пером Thread.Sleep (200); d= 1; // Нарисовать шар тонким пером run= false;

// Создать и стартовать поток для утолщения пера рисования шара public void Start ( ) { if(!run) { run= true; t= newThread (newThreadStart(ThrFunc)); t.Start ( );


// Класс прикладного окна class Wlnd : Form

{

public SpecialBall speclalBall;// Ссылка на спец.объект Button butAdd; // Ссылка на кнопку ArrayList aL; // Ссылка на список Random r; // Ссылка на объект случайного числа public Wind ( )

{

// Конструктор

specialBall= new SpeclalBall (30, 30); // Создать спец.объект aL= new ArrayList ( ); // Создать список r= new Random ( ); // Создать объект случайного числа butAdd= new Button ( ); // Создать кнопку butAdd.Location = new Point (100, 24); // Разместить кнопку // Задать размер кнопки в области клиента дочерней формы butAdd.Size = new Size (32, 24); butAdd.Text = "Add"; // Поместить текст в кнопку // Подписать обработчик OnButAdd кнопки butAdd на событие Click butAdd.Click += new EventHandler (OnButAdd); Controls.Add (butAdd);

// Обработать событие кнопки vold OnButAdd (object obj, EventArgs arg) i Ball b= new Ball ( r.Next(this.Width),r.Next(this.Height), r.Next(5, 10),r.Next(5, 15), this.ClientSize.Width, this.aientSize.Height); b.ev += n e w delEv (HandlerEv ); b.evSpecial += n e w delEv (specialBall.HandlerEvSpedal); aL.Add (b);

>

// Перерисовать прикладное окно protected override vold OnPalnt (PaintEventArgs arg) { for (int i= 0; i < aL.Count; i++) { Ball b= (Ball) aL[l]; arg .Graphics.DrawEllipse (new Pen (Color.Blue), b.X, b.Y, 20, 20);

>

arg .Graphics.DrawEllipse (new Pen (Color.Yellow, speclalBall.D), specialBall.X, spec!alBall.Y, 20, 20);

// Обработать событие перерисовки из шара


void HandlerEv ( ) <

}

Invalidate ( );

// Обработать событие закрытия окна protected override void OnClosed (EventArgs e) { base.OnClosed (e); for (int i= 0; i < aL.Count; i++) {

}

>

Ball b= (Ball) aL[i]; b.Finish ( );

// Выполнить основной поток static void Main ( ) {

>

>

Wind w= new Wind ( ); Application.Run (w);

12.4. Задание 4 Программа содержит прикладное окно с кнопкой и особой областью, представленной прямоугольником. При нажатиях на кнопку в окне появляются перемещающиеся разноцветные шары, отталкиваясь от границ окна. При вхождении шара в особую область от него отделяются три шара меньшего размера и того же цвета, которые исчезают через 2 сек. В особой области может перемещаться только один шар (со своими небольшими шарами). Мелкие шары продолжают перемещаться в течении 2 сек в том же направлении, что и создавший их шар, с разными приращениями по вертикали. Пытающиеся войти в особую область шары выстраиваются в очередь у границы области. Примечание. Объекты больших шаров создаются из класса, содержащего поток, перемещающий эти шары в окие. Также этот поток генерирует собьггие (уведомление), обработчик которого осуществляют перерисовку окна. При входе в особую область поток создает три объекта небольших шаров с запуском их потоков. Небольшие шары помещаются в соответствующий список, который применяется при рисовании шаров в окне. Объекты небольших шаров создаются из своего класса, содержащего поток, перемещающий небольшой шар в течение 2 сек. При своем завершении поток небольшого шара генерирует собьггие (уведомление), обработчик которого осуществляют удаление соответствующего объекта шара из списка небольших шаров. Удобно хранить объекты шаров в списке, что облегчает их совместную обработку.

/////////////// // C# using using using

Большие шары создают небольшие шары в особой области System; System.Drawlng; System.Windows.Forms;


using System.Threading; using System.Collections; // Класс особой области class Region { public Rectangle r= new Rectangle (50, 50, 200, 100); public void Waiting ( ) {

>

4 I

Thread.Sleep(1000);

delegate void delEv ( ); // Делегат события перерисовки окна delegate v o i d delEvS (SmallBall sm); // Делегат удаления небольшого шара // Класс небольших шаров class SmallBall { private int x, у; // Координаты шара private int w, h; // Ширина и высота области клиента private int dx, dy; // Приращения координат шара private Thread t; // Ссылка на поток private bool life; // Признак существования потока private Color с; // Ссылка на объект цвета private int count; // Число циклов жизни небольшого шара public event delEvS evS; // Ссылка на событие удаления небольшого // шара // Свойство X public int X {get{return х ; » // Свойство Y public int Y {get{return у ; » // Свойство Color public Color Color {get{return с ; » // Конструктор public SmallBall (int X, int Y, int Dx, int Dy, int W, int H, Color С, Wind wind) { w= W; h= H; x= X; y= Y; dx= Dx; dy= Dy; c= С; this.evS+= n e w delEvS (wind.HandierEvS); // Создать и стартовать поток llfe= true; t= new Thread (new ThreadStart (BallFunc)); t.Start ( ) ;

// Завершить поток


public vold Finish ( ) {

>

life= false; t>Joln ( );

// Выполнить поток небольшого шара protected vold BallFunc ( ) { while (llfe) r

V

x += dx; у += dy; If (x>w || x<0) dx= -dx; lf(y>h || y<0) dy= -dy; Thread.Sleep (100); count+= 1; lock(this) { // Завершить поток через 20 * 100 секунд if(count==20) { life= false; // Сгенерировать событие удаления шара if (evS != null) {

>

>

>

>

>

>

evS (this);

// Класс объектов больших шаров class BigBall { public int x, у; // Координаты шара public int w, h; // Ширина и высота области клиента public int dx, dy; // Приращения координат шара public event delEv ev; // Объявление события перерисовки private Region reg; // Ссылка на особую область private bool inRect; // Признак нахождения шара в особой области private ArrayList aL; // Ссылка на список небольших шаров public Thread t; // Ссылка на поток public bool life; // Признак существования потока public Color с; // Ссылка на цвет шара Wind wind; // Ссылка на прикладное окно // Свойство X pubiic int X {get{return х ; » // Свойство Y public int Y


{get{return у ; » // Свойство Color public Color Color {get{return c ; » || Конструктор public BigBall (int X, lnt Y, int Dx, int Dy, lnt W, int H, Region R, Color C, ref ArrayList aL, Wind wind)

{

VY—

41. VV,

tl —

II,

А—

V ' Л ,

••— y—

*,. 1,

J . . _ U A -

r>... UAf

^ , . _ U y —

rs... L>y,

reg= R; this.aL= aL; reg= new Region (); inRect= false; c= C; this.wlnd= wind;

>

// Создать и стартовать поток life= true; t= newThread (newThreadStart (BallFunc)); t.Start ( );

// Выполнить поток большого шара public void BallFunc ( )

{

while (llfe) { x += dx; у += dy; if(x>w||x<0) dx=-dx; if(y>h || y<0) dy= -dy; Point p= new Point(x, у); // Синхронизовать параллельное выполнение потоков lock(this) { if(reg.r.Contains(p) && !inRect)

{

// Шар внутри особой области reg.Waiting ( ); inRect= true; for(int i=0; i<3; i++) {

>

>

SmallBall b= n e w SmallBall(x+i, y+i, dx, dy + 1*3, w , h, с, wind); aL.Add(b);

if(!reg.r,Contains(p) && lnRect) {

>

>

|| Шар вышел из особой области InRect= false;

// Сгенерировать событие перерисовки if (ev 1= null)


}

>

>

ev ( );

Thread.Sleep (100);

// Завершить поток большого шара public vold Finish ( ) { life- fdlbe; t.Join ( );

>

}

// Класс прикладного окна class Wlnd : Form { public Region reg; Button butAdd; ArrayList aL_blg; ArrayList aL_small; Random r;

// Ссылка на спец.область // Ссылка на кнопку // Ссылка на список больших шаров // Ссылка на список небольших шаров // Ссылка на объект случайного числа

// Конструктор public Wind ( ) { // Создать объект особой зоны reg= new Region (); // Создать объект списка больших шаров aL_big= new ArrayList (); aL_blg.Clear (); // Создать объектсписка небольших шаров aL_small= new ArrayList (); // Создать список aL_small.Clear (); // Создать объект случайного числа r= new Random ( ); // Создать кнопку butAdd= new Button(); butAdd.Location = new Point (100, 24); butAdd.Size = new Size (32, 24); butAdd.Text = "Add"; // Подписать обработчик OnButAdd кнопки butAdd на событие Click butAdd.Click += new EventHandler (OnButAdd); Controls.Add (butAdd);

> // Обработать событие кнопки void OnButAdd (object obj, EventArgs arg) { Colorc= (Color.FromArgb (r,Next(255), r.Next(255), r.Next(255))); BigBall b= new BigBall (r.Next(this.Width),r.Next(this.Height),


r.Next(5, 10),r.Next(5, 15), this.ClientSize.Width, this.ClientSize.Height, reg, c, ref al small, this);

>

// Подписать обработчик на событие перерисовки b.ev += n e w delEv (HandlerEv ); // Включить объект большого шара в список больших шаров aL_big.Add (b);

// Перерисовать окно protected override voia OnPaint (PaintEventArgs arg) { base.OnPaint (arg); // Просмотреть список больших шаров и нарисовать их IEnumerator inumBig= aL_big.GetEnumerator (); while (inumBig.MoveNext()) {

>

BigBall big= (BigBall) inumBig.Current; arg.Graphlcs.DrawEllipse (new Pen (big.c), big.X, big.Y, 20, 20);

// Просмотреть список небольших шаров и нарисовать их for (lnt i= 0; i < aL_small.Count; i++) { SmallBall b= (SmallBall) aL_small[i]; arg.Graphics.DrawElllpse (new Pen (b.c), b.X, b.Y, 10, 10);

>

// Нарисовать особую область arg .Graphics,DrawRectangle (new Pen (Color.Yellow), reg.r);

> // Обработать событие перерисовки от шара void HandlerEv ( ) { Invalidate ();

} // Обработать событие перерисовки от шара public void HandlerEvS (SmallBall sm) { lock(this)

{ }

aL_small.Remove ((SmailBall)sm);

}

// Обработать событие закрытия окна protected override void OnClosed (EventArgs e) { base.OnClosed(e); // Завершить потоки больших шаров for (int i= 0; i < aL_blg.Count; i++)


>

BigBall b= (BigBall) aL„big[i]; b.Finish ( );

// Завершить потоки небольших шаров for (int i= 0; i < aL_small.Count; i++) {

>

>

SmallBall b= (SmallBall) aL_small[i]; b.Finlsh ( );

// Выполнить основной поток static void Main()

{

>

>

Wind w= new Wind( ); Application.Run(w);

/////////////// // Java Importjava.awt.*; importjava.awt.event.*; Importjava.utll.*; class Region

{

public Rectangle r= new Rectangle (50, 50, 200, 100); public void Waiting ( )

{

try {

>

>

>

Thread.sleep (1000);

catch (InterruptedException e) { >

class Data { private boolean del; private Object obj; public boolean getDel ( ) {return del;> public Object getObject ( ) {return obj;}

>

public Data (boolean Del, Object Obj) {del= Del; obj= Obj;}

class SmallBall extends Observable implements Runnable

{

private intx,y;

//Координатышара


private private private private private private private

lnt w, h; int dx, dy; Thread t; boolean life; Color c; int count; Wind wlnd;

// Свойство X publicintgetX()

// Ширина и высота области клиента // Приращения координат шара // Ссылка на поток // Признак существования потока // Ссылка объект цвета // Число циклов жизни небольшого шара // Ссылка на прикладное окно

{returnx;}

// Свойство Y public int getY ( ) {return y;} // Свойство getColor public Color getColor ( ) {return c;} // Свойство getCount public lnt getCount ( ) {return count;> // Конструктор public SmallBall (int X, int Y, int Dx, int Dy, int W, int H, Color C, Wind wind)

{

w= W; h= H; x= X; y= Y; dx= Dx; dy= Dy; c= C; this.addObserver (wind); // Создать и стартовать поток life= true; t= new Thread (this); t.start ( );

} // Завершить поток public void Finish ( ) { !ife= false; try

{

>

>

t.join ( );

catch(InterruptedException e) { >

// Выполнить поток небольшого шара public void run ( ) < while (life) { x += dx; у += dy; if (x>w 11 x<0) dx= -dx; lf(y>h||y<0) dy=-dy; try


>

Thread.sleep (100);

catch(InterruptedException e) { > count+= 1; synchronized(this) { // Завершить поток через 20 * 100 секунд if(count==20) < life= false; // Сгенерировать событие удаления // небольшого шара setChanged ( ); notifyObservers (new Data (true, this));

>

>

>

>

>

//Класс объектов больших шаров class BigBall extends Observable implements Runnable

{

public int x, у; // Координаты шара public lnt w, h; // Ширина и высота области клиента public int dx, dy; // Приращения координат шара private Region reg; // Ссылка на особую область private boolean inRect; // Признак нахождения шара в особой области private ArrayList aL; // Ссылка на список небольших шаров public Thread t; // Ссылка на поток public boolean life; // Признак существования потока public Color с; // Ссылка на цвет шара Wind wind; // Ссылка на прикладное окно // Свойство X public lnt getX ( )

{return x;}

// Свойство Y ( ) {return у;> public lnt getY // Свойство getColor public Color getColor ( ) {return с;> // Конструктор public BigBall (int X, int Y, int Dx, int Dy, int W, int H, Region R, Color С, ArrayList aL, Wind wind)

{ w= W; h= H; x= X; y= Y; dx= Dx; dy= Dy; reg= R; this.aL= aL; reg= new Region ( ); inRect= false; c= С; this.wind= wind; // Создать и стартовать поток life= true;


t= new Thread (this); t.start ( ); // Выполнить поток большого шара pubiic vold run ( ) { while (life) { x += dx; у += dy; if(x>w|ix<0) dx=-dx; lf(y>h i| y<O) dy= -dy; Point p= new Point (x, у); // Синхронизовать параллельное выполнение потоков synchronized (this) { if(reg.r.contains(p) && !lnRect) { // Шар внутри особой области reg.Waiting ( ); inRect= true; for(lnt i=0; l<3; I++) {

>

>

SmallBall b= n e w SmallBall (x+i, y+i, dx, dy + i*3, w , h, с, this.wind); aL.add (b);

if(!reg.r.contains(p) && inRect) {

>

>

// Шар вышел из особой области inRect= false;

// Сгенерировать событие перерисовки setChanged ( ); notifyObservers ( n e w Data (faise, nuil)); try {

}

>

>

Thread.sleep (100);

catch(InterruptedException e) { >

// Завершить поток большого шара public void Finish ( ) { life= false; try

{

t.join ();


catch(InterruptedExceptlon e) { >

> public class Wlnd extends Frame implements Observer

{

public Region reg; Button butAdd; ArrayList aL_big; ArrayList aL_small; Random г; Object obj;

// Ссылка на спец.область // Ссылка на кнопку // Ссылка на список больших шаров // Ссылка на список небольших шаров // Ссылкз нз объект сяучзиного числз // Ссылка на корневой класс

// Конструктор public Wind ( ) { setSize (400, 300); setVisible (true); setLayout (null); // Создать объект особой зоны reg= new Region (); // Создать объект списка больших шаров aL_big= new ArrayList (); aL_big.clear (); // Создать объект списка небольших шаров aL_small= new ArrayList ( ); // Создать список aL_small.clear (); // Создать объект случайного числа r= new Random (); // Создать кнопку butAdd= new Button ("Add"); butAdd.setLocation (100, 24); butAdd.setSize (32, 24); add (butAdd); obj= this; // Подписать обработчик actionPerformed на событие кнопки butAdd.addActionListener (new ActionListener( )

{

public vold actlonPerformed (ActionEvent aE) { Color c= new Color (r.nextInt(255), r.nextInt(255), r.nextInt(255)); lnt x= r,nextInt(50)+10; int y= r.nextInt(50)+10; lnt dx= r.nextInt(10)+5; lnt dy= r.nextInt(10)+5; BigBall b= new BigBall (x, у, dx, dy, 300, 200, reg, с, aL_small, (Wind) obj); // Подписать обработчик на событие перерисовки b.addObserver ((Wind) obj); aL_big.add (b);


// Подписать обработчик windowClosing на событие закрытия this.addWindowListener (new WindowAdapter ( ) { public void windowClosing (WlndowEvent wE) { // Завершить потоки больших шаров for(inti= 0; i< aL_big.size(); i++) { BigBall b= (BigBall) aL_big.get(i); b.Finish ( );

}

// Завершить потоки небольших шаров for(int 1= 0; i< aL_small.size(); i++) {

> >

SmallBall b= (SmallBall) aLsmall.get(i); b.Finish ( );

System.exit (0);

»; // Перерисовать окно public vold paint (Graphics g) { // Просмотреть список больших шаров и нарисовать их for (int i= 0; i < aL_blg.size(); i++) { BigBall b= (BigBall) aL_big.get(i); g.setColor (b.getColor()); g.drawOval (b.getX(), b.getY(), 20, 20);

>

// Просмотреть список небольших шаров и нарисовать их for (int 1= 0; I < aL_small.size(); I++) {

>

SmallBall b= (SmallBall) aL_small.get(i); g.setColor (b.getColor()); g.drawOval (b.getX(), b.getY(), 10, 10);

g.setColor (Color.yellow); g.drawRect (reg.r.x, reg.r.y, reg.r.width, reg.r.height);

public void update (Observable obs, Object obj) { synchronized (this) { Data d= (Data) obj; if(d.getDel()) { System.out.println ("d.getDel()= true"); System.out.println ("del" + d.getDel());


aL_small.remove ((SmallBall)d.getObject());

>

}

>

else repaint ( ) ;

public static vold maln ( ) { Wind w= new Wlnd ( );


13. Ещё о важном в объектно-ориентированном программировании на С++, C# и J# (Java) Освоение ранее изложенных разделов об объектно-ориентированном программировании с многочисленными примерами программ позволит разрабатывать разнообразные много объектные и многопоточные консольные и Windows программы с использованием разделяемых ресурсов и интерфейсных управляющих элементов, несмотря на то, что ряд вопросов были опущены или затронуты поверхностно. Ведь книга затрагивает только основы объектно-ориентированного программирования и особенности его применительно к языкам С++, C++/CLI, C# и Java. Надеюсь, что полученные знания стимулируют читателя к самостоятельному изучению иных особенностей этих языков и более глубокому освоению объектно-ориентированного программирования. В завершающем разделе книги рассмотрим такие важнейшие понятия объектно-ориентированного программирования как виртуальные функции, абстрактные функции и абстрактные классы, знание которых позволит разрабатывать программы на более высоком абстрактном уровне.

13.1. Виртуальные функции Виртуальные функции (virtual functions) уже применялись в примерах приведённых программ, в которых наследовались базовые классы Form, Frame. В соответствии с правилами виртуальных функций требовалось применять ключевые слова virtual, override. Что и делалось, иначе компилятор укажет ошибку. C#. Например, удалите в порождённом классе слово override в заголовке виртуальной функции OnPaint(): protected override vold OnPaint (PaintEventArgs arg)

и компилятор сделает предупреждение, что в этом случае данная функция OnPaint() не переопределяет наследуемую виртуальную функцию OnPaint(), которая является обработчиком собьггия Paint. Компилятор сообщает, что теперь эта функция должна считаться иной функцией и должна помечаться уже ключевым словом new. Запустив программу без требуемого слова override, можно убедиться, что наша функция OnPaint() будет выполняться иначе, не отображая в окне описанное в данной функции.


Механизм виртуальных функций позволяет в иерархию наследования классов ввести особые функции - виртуальные функции, которые, будучи определёнными в базовом классе, могут применяться в объектах наследуемьа классах, а при необходимости и переопределяться, а в объектах дальнейших наследуемых классов использоваться уже переопределёнными. Функция OnPaint() была определена как виртуальная в наследуемом нами классе Form и, как обработчик события Paint, она была подписана на это событие. Эту функцию обычно переопределяют в порождённом классе, поскольку по умолчанию она имеет пустое тело, как и большинство виртуальных функций. Свойство виртуальности подчеркивает то, что эта функция особая и является, в данном случае, подписанным обработчиком. Ключевое слово override сохраняет это свойство функции, подчеркивая, что переопределённая функция является тем же самым обработчиком, но содержащим теперь иное тело. Она теперь перерисовывает окно в соответствии с нашими пожеланиями.

13.1.1. Преобразование типов При выполнении программы данные одного типа могут присваиваться данным другого типа - для этого необходимо выполнить преобразование типов. Язык С++ не только предоставляет программисту средства преобразования типов, но и разумно осуществляет преобразование сам, когда это допустимо. Проблемы возникают с преобразованиями классов: выполняемые в С++ преобразования, особенно связанные с так называемыми виртуальными функциями (virtual Junctions), иногда озадачивают неопытных программистов. Виртуальные функции появились для упрощения использования объектов, созданных из классов, порождённых из одного базового класса. Механизм виртуальных функций оказался уместным для СОМ-объектов, широко применяемых до появления .NET технологии, и составляет основу их функционирования. С появлением .NET технологии СОМ-объекты ушли на второй план. На виртуальных функциях основывается полиморфизм - один из принципов объектно-ориентированного программирования. Попытаемся понять необходимость виртуальных функций, связанную с преобразованием типов классов. Пусть из базового класса CA порождается класс CB, а из него класс CC. Каждый класс включает одноимённую функцию одинакового формата, но с разным телом. В примере 13.1.1.1 из классов создаются соответствующие объекты и вызываются их одноимённые функции F() по указателю базового класса, по указателям каждого объекта и по базовому указателю, приведённому к типу объектов. Пример 13.1.1.1. Приведение типов классов. //////////////////// //c++


#include <iostream.h> class CA { public: void F ( ) {cout «

>;

"CA:: F" «

endl;}

class CB : public CA { public: void F ( ) {cout << "CB:: F" << enai;}

>;

class CC : public CB { public: vold F ( ) {cout << "CC:: F" << endl;}

>;

void main ( ) { CA *pCA= new CA; CB *pCB= new CB; CC *pCC= new CC; PCA -> F ( ); PCB -> F ( ); pCC -> F (); pCA= pCB; pCA -> F ( ); pCA= pCC; pCA -> F ( ); ((CB*) pCA) -> F ( ); ((CC*) pCA) -> F ( ); > /* Result: / / — Ссылка по указателю каждого объекта CA::F CB::F CC::F / / — Ссылка по указателю базового класса CA::F CA::F / / — Ссылка по базовому классу, приведённому к типу объекта CB::F CC::F */

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


ты. Следующие две строки озадачивают: ведь мы же присваиваем указателю на базовый класс значения указателей на объекты? Но так и должно случиться: компилятор преобразовывает типы порождённых классов к типу базового класса, указывается базовый класс и, разумеется, поэтому вызывается функция F() базового класса CA. А будь функции виртуальными функциями, результат был бы иной. Две последние строки используют указатели на базовый класс, к которым применяется преобразование типа, и здесь мы имеем желаемый результат. Попробуем программно (без механизма виртуальных функций) реализовать иерархию наших классов таким образом, чтобы и при ссылке по базовому классу вызываемая функция F() определялась типом объекта, то есть, чтобы и для второй пары строк результата вместо CA::F CA::F получилось бы CB::F CC::F. Как мы увидим позднее, это позволяют сделать механизм виртуальных функций. Не используя же виртуальные функции, это можно реализовать, например, так, как показано в примере 13.1.1.2. Пример 13.1.1.2. Вызов функций по типу объекта. //////////////////// // С+ + #include <iostream.h>

class CA { protected:

char w h a t ;

public: CA ( ) {what= 'A';> void F ( ) { s w i t c h (what) { case 'A': // Тело функции F наследуемого класса CA cout << "CA:: F" << endl; break; case 'B': // Тело функции F наследуемого класса CB cout << "CB:: F" << endl; break; case 'C': // Тело функции F наследуемого класса CC cout<<"CC:: F"<<endl; break;

>;

>

>

class CB : public CA { public: CB ( ) {what= 'B';>


class CC : public CB { public: CC ( ) {what= 'C';>

>;

void main ( ) {

wi • рч*А— ncVv wA^ CB *pCB= new CB; CC *pCC= new CC; //--- Ссылка no указателю каждого объекта pCA -> F ( ); pCB -> F ( ); pCC -> F ( ); / / — Ссылка по указателю базового класса и типу объекта pCA= pCB; pCA -> F ( ); pCA= pCC; pCA -> F ( ); > /* Result: //--- Ссылка по указателю каждого объекта CA::F CB::F CC::F / / — Ссылка по указателю базового класса и типу объекта CB::F CC::F */

В базовый класс СЛ включена специальная переменная what, например, символьного типа, принимающая значение имени наследуемого класса, объект которого создан. Для этого конструктор каждого наследуемого класса должен присвоить этой переменной имя его класса. Разумеется, сама переменная what должна быть доступна в каждом наследуемом классе и не доступна вне этих классов. Поэтому она объявлена с доступом protected. Одноимённые функции F() перетаскиваем из наследуемых классов и включаем их тела в тело функции F() базового класса, которая будет выполнять их в зависимости от типа объекта, имя которого было присвоено переменной what конструктором при создании этого объекта. В примере 13.1.1.3 приводится иное обращение к объектам - через параметр типа базового класса CA функции Do(). Пример 13.1.1.3. Обращение к объектам через параметр базового класса.

//////////////////// //C++

#include <iostream.h>


class CA { protected: char what; public: CA ( ) {what= 'A';> vold F ( ) < switch (what) { case 'A': cout << "CA:: F" << endl; break; case 'B': cout << "CB:: F" << endl; break; case 'C': cout << "CC:: F" << endl; break;

>;

>

>

class CB : public CA { public: CB ( ) {what= 'B';>

>;

class CC : public CB { public: CC ( ) {what= 'C';>

>;

vold Do (CA* pCA) {

>

pCA -> F ( );

void main ( ) { CA *pCA= new CA; CB *pCB= new CB; CC *pCC= new CC;

//

Do (pCA); Do (pCB); Do (pCC);

> /* Result: CA::F CB::F


CC;:F */

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

13.1.2, Виртуальные функции Виртуальные функции решают проблему полиморфизма, позволяя объектам, созданным из иерархически взаимосвязанных классов, просто использовать одноименные функции с одинаковыми параметрами и типом возвращаемого значения. При этом обращаться к этим функциям необходимо по объекту через указатель базового класса для языка С++ и через ссылочную переменную базового класса для языка C# и Java. При объявлении виртуальной функции в языках С++ и C# перед её возвращаемым значением указывается ключевое слово virtual (виртуальный, действительный, фактический). В языке C# при переопределении виртуальной функции в порождённом классе необходимо использовать ключевое слово override (переопределить). Взгляните на пример 13.1.2.1 и обратите внимание на результат - сработал механизм виртуальных функций, и через указатель и ссылку на базовый класс вызваны функции требуемых объектов. Пример 13.1.2.1. Виртуальные функции. //////////////////// //C++ #include <stdafx.h> using namespace System; ciass А {

public: // Объявление виртуальной функции F() virtual void F ( ) {Console::WriteLine ("A:: F");}

>;

class В : public А { public: // Наследуемая виртуальная функция F() переопределяется void F ( ) {Console::WriteLine ("B:: F");}

>;

class С : public В {


public: // Наследуемая виртуальная функция F() переопределяется void F ( ) {Console::WrlteLlne ("C:: F");}

>;

class D : public С

f

public: // Наследуемая виртуальная функция F() не переопределяется // vold F ( ) {Console::WrlteLine ("D:: F");}

>;

vcidrnain < ) { А *pA= new А; В *pB= new В; С *pC= new С; pA -> F (); pB->F(); pC -> F ( ); pA= pB; pA -> F ( ); pA= pC; pA -> F ( );

>

D *pD= new D; pA= pD; pA -> F ( );

/*Result: A:: F B:: F C:: F B:: F C:: F C:: F */

/////////////// // C# using System; class А// Объявление виртуальной функции F() { virtual public void F ( ) {Consoie.WrlteLine("A.F");>

>

class В: А { // Наследуемая виртуальная функция F() переопределяется public override vold F ( ) {Console.WrlteLlne("B.F");} class С: А { // Наследуемая виртуальная функция F() переопределяется public override void F ( ) {Console.WriteLine("C.F");}


class D: С {

>

// Наследуемая виртуальная функция F() не переопределяется public vold F ( ) {Console.WriteLine("D.F");}

class Test { static void Maln ( ) {

А a= new А ( ) ; В b= new В ( ); С c= new С ( ), a.F(); b.F(); c.F(); a= b; a.F(); a= с; a.F();

>

>

D d= new D ( ); a= d; a.F();

/*Result: A.F B.F C.F B.F C.F C.F */ /////////////// // Java class А < // Объявление функции F() public void F ( ) {System.out.println("A.F");}

>

class В extends А { // Наследуемая функция F() переопределяется public void F ( ) {System.out.println("B.F");>

>

class С extends А { // Наследуемая функция F() переопределяется public void F ( ) {System.out.println("C.F");>

>

class D extends С

{

>

//public void F ( ) {System.out.printtn("D.F");}

class Test


{

public static vold main ( )

{

A a= new A ( ); B b= new B ( ); C c= new C ( ); A r= a; r.F(); i —

и ,

r.F(); r= c; r.F();

D d= new D ( ); r=d;

}

>

r.F();

/*Result: A.F B.F C.F C.F */

В примере 13.1.2.2 ссылка на базовый класс использована в качестве параметра функции Do(). Аргументы этой функции - ссылки на объекты порождённых классов. Пример 13.1.2.2. Объявление виртуальной функции в базовом классе.

//////////////////// //c++

#include <stdafx.h> using namespace System;

class А { public: virtual void F ( ) {Console::WriteLine("A:: F");>

>;

class В : public А { public: vold F ( ) {Console::WrlteLlne("B:: F");}

>;

class С : public В {


public: void F ( ) {Console::WrlteLine("C:: F");>

>;

vold Do (A* pA) {

}

pA -> F ( );

void main ( ) { A *pA= new A; B *pB= new B; C *pC= new C; Do (pA); Do (pB); Do (pC); > /* Result: A::F B::F C::F */ //////////////////// //C# using System; class A {

>

.

public virtual void F ( ) {Console.WriteLineCA.F");>

class B: A

<

>

public override void F ( ) {Console.WriteLine("B.F");}

dass 小: B {

>

public override void F ( ) {Console.WriteLine("C.F");}

class Test { static void Do (A a) {

>

a.F ( );

static void Main ( )


{ A a= new А ( ); В b= new В ( ); С c= new С ( );

> > /*Dociilfr>

Do (а); Do (b); Do (с);

/ •»«•»«•»• A.F B.F C.F */

//////////////////// / / Java class А

{

>;

public void F ( ) {System.out.println ("A.F");>

class В extends А { public void F ( ) {System.out.println ("B.F");} >;

class С extends В

{

public void F ( ) {System.out.println ("C.F");}

}; class Test { static void Do (А а) {

>

a.F ( );

public static void main ( ) { А a= new А ( ) ; В b= new В ( ); С c= new С ( ); Do (а); Do (b); Do (с); > /*


Result: A::F B::F C::F */

Примеры 13.1.2.1 и 13.1.2.2 подтверждают, что, объявив функцию виртуальной, мы можем динамически, при необходимости, во время выполнения программы, присвоить ссылке на базовый класс ссылку на требуемый объекта порождённого класса, и наша программа измснш функционирование, выполняя виртуальную функцию этого объекта. Виртуальные функции реализуют так называемое позднее связывание вызова функции с её телом, осуществляемое во время выполнения программы. Вызов функции при этом осуществляется косвенно и зависит от типа объекта. Так чем же виртуальная функция отличается от обычной функции? - вызов обычной функции связывается с её телом на этапе компиляции (в отличие от виртуальной функции здесь имеет место не косвенное, а прямое обращение к функции), то есть происходит так называемое раннее связывание-, - при объявлении виртуальной функции перед возвращаемым значением ставится ключевое слово virtual\ - в языке С++, если некоторая функция объявлена в базовом классе как виртуальная, то функция с тем же именем, с тем же возвращаемым значением и с тем же списком параметров, переопределённая в порождённом классе, автоматически становится виртуальной и ключевое слово virtual можно опустить при её описании; - в языке C#, если некоторая функция объявлена в базовом классе как виртуальная, то функция с тем же именем, с тем же возвращаемым значением и с тем же списком параметров, переопределённая в порождённом классе с ключевым словом override, автоматически становится виртуальной; - если виртуальная функция не была переопределена в порождённом классе, то при её вызове будет происходить обращение к ближайшей соответствующей виртуальной функции по иерархии базовых классов; - виртуальная функция вызывается только через указатель или ссылку на базовый класс. Экземпляр вызываемой виртуальной функции определяется классом объекта, адресуемого указателем или ссылкой, во время выполнения. Для реализации механизма виртуальных функций компилятор генерирует так называемую таблицу виртуальных функций. Таблица виртуальных функций класса содержит адреса виртуальных функций, объявленных в классе. В сам класс помещается скрытый указатель на эту таблицу pvtbl pointer to the virtual table). Если мы имеем несколько объектов одного класса с виртуальными функциями, то каждый объект будет содержать поля с его данными (атрибутами) и указатель на общую для этих объектов таблицу виртуальных функций. При множественном наследовании


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

13.1.3. Виртуальные функции, используемые в книге В различных примерах книги при использовании классов библиотеки .NET Framework и пакетов языка Java потребовались виртуальные функции OnPaint(), paini(), OnC!osed(), OnMouseDo\vn(), mouseDown(), mousePressed(), windowCIosing() и др. Следует обратить внимание, что все виртуальные функции применяются только в порождённых классах и не подлежат использованию вне классов. Поэтому все они имеют, как правило, доступ protected. Насколько широко применяются виртуальные функции говорит, например, то, что класс Form включает более 120 виртуальных функций. Аналогично много виртуальных функций содержит и класс Frame. При разработке программ необходимо выявить, является ли функция наследуемого класса виртуальная, и затем употребить её в соответствии с правилами языка.

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


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

13.2.1. Абстрактные функции Абстрактная функция объявляется без тела с обязательным ключевым словом abstract:

C#, Java a b s t r u c t тип-возвр-значения имя-функц (список-парам);

С++, C++/CLI. v i r t u a l тип-возвр-значения имя-функц (список-парам}=0; где abstract - ключевое слово, указывающее компилятору об объявлении абстрактной функции, virtual - ключевое слово, указывающее с " = 0 " компилятору об объявлении чистой виртуальной функции, имя-функц - это имя абстрактной функции, тип- возвр-значения - тип возвращаемого значения абстрактной функции, список-парам - список типов параметров абстрактной функции.

При объявлении абстрактной функции в классе необходимо указать требуемый доступ - protected или public. Поскольку абстрактная функция должна переопределяться в порождённом классе, она не может иметь доступ private. Абстрактными функциями представляют одноимённые функции порождённых классов, тела которых могут несколько отличаться для объектов различных порождаемых классов. Как видим, абстрактная функция схожа с виртуальной функцией, поскольку требует переопределения, но в отличие от виртуальной функции она не содержит тело. Обратим ещё раз внимание, что в языках С++ и C++/CLI абстрактная виртуальная объявляется как чистая виртуальная функция, не содержащая тело и определяемая как


virtual тип-возвр-значения имя-функц (список-парам)=0;

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

13.2.2. Абстрактные классы Класс, содержащий хотя бы одну абстрактную функцию, называется абстрактным классо.м и содержат перед словом c!ass ключевое слово abstract. Поскольку абстрактный класс должен наследоваться, то он, разумеется, не должен иметь модификаторы sealed и final. Содержа не доопределённые абстрактные функции, абстрактный класс может применяться только для порождения из него иных классов, среди которых опять могут возникнуть абстрактные классы, если в них не переопределяются наследуемые абстрактные функции. Из абстрактных классов, в виду их полной неопределённости, нельзя создавать объекты. Объекты создаются из порождённых классов. Обязательно включая хотя бы одну абстрактную функцию, абстрактный класс, как и обычный класс, может содержать множество обычных функций с телами, события и другие данные, которые удовлетворяют функциональности всех объектов порождённых классов, Абстрактные же функции должны обязательно переопределяться в порождённых классах, отображая специфику их объектов. Пример 13.2.2.1 иллюстрирует абстрактные классы и абстрактные функции. Пример 13.2.2.1. Абстрактные классы и функции //////////////////// //C++ #include <stdafx.h> using namespace System;

// Абстрактный класс class А { protected: // Объявление чистой виртуальной функции F() virtual void F ( }= О;

>;

class В : public А { public: // Наследуемая чистая виртуальная функция F() переопределяется void F ( ) {Console::WriteLine ("B:: F");}

>;

class С : public В { public: // Наследуемая чистая виртуальная функция F() не переопределяется //void F ( ) {Console::WriteLine ("C:: F");}


>; class D : public А { public: // Наследуемая чистая виртуальная функция F() переопределяется vold F ( ) {Console::WriteLine ("D:: F");}

>;

vold main ( ) {

а" *nDмши а> r ^ «<w»» u^, С *pC= new С; pB -> F (); pC -> F (); pB= pC; pB -> F ();

>

D *pD= new D; pD - > F ( ) ;

/*Result: B:: F B:: F B:: F D: F */

/////////////// //C# using System; // Абстрактный класс abstract class А { // Объявление абстрактной функции F() abstract publfc void F ( );

>

class В: А { // Наследуемая абстрактная функция F() переопределяется public override void F ( ) {Console.WrlteLine ("B.F");>

>

class С: В { // Наследуемая абстрактная функция F() не переопределяется //public override void F ( ) {Console.WriteLine ("C.F");>

>

class D: А { // Наследуемая абстрактная функция F() переопределяется public override void F ( ) {Console.WriteLine ("D.F");}

>

class Test { static void Main ( ) { B b= new В ( );


C c= new С (); b.F(); c.F ( ); b= с; b.F();

>

>

D d= new D (); d.F();

/*Result: B.F B.F B.F D.F */ /////////////// // Java // Абстрактный класс abstract class А { // Объявление абстрактную функции F() abstract public void F ( );

>

class В extends А { // Наследуемая абстрактная функция F() переопределяется public void F ( ) {System.out.println ("B.F");>

>

class С extends В { // Наследуемая абстрактная функция F() не переопределяется //public void F ( ) {System.out.println ("C.F");}

}

class D extends А { // Наследуемая абстрактная функция F() переопределяется public void F ( ) {System.out.println ("D.F");}

}

class Test { public static vold maln ( ) { В b= new В (); С c= new С ( ) ; b.F(); c.F(); b= с; b.F();

D d= new D (); d.F();

}


>

/*Result: B.F B.F B.F D.F */

То, что абстрактный класс наряду с абстрактными функциями может включать обычные данные и, в частности, обычные функции, свойства и события, а также наследовать другие классы и интерфейсы, иллюстрирует пример 13.2.2.2. Пример 13.2.2.1. Абстрактный класс с событием, свойством, обычной функцией и интерфейсом /////////////// // C# using System; interface I {void F l ();> delegate vold Del (int m); // Абстрактный abstract class А: класс I

{

public event Del ev; int x= 5; public int X {get{if(ev !=null )ev(x);return х ; » // Объявление абстрактной функции F() abstract public string F ( ); public void F l ( ) {

>

>

Console.WriteLine ("A.F1");

class В: А {

>

// Наследуемая абстрактная функция F() переопределяется public override string F ( ) {return "B.F";>

class Test { static vold Main ( ) { Test t= new Test ( ); B b= new B ( ); b.ev+= new De l(Test.Handler); Console.WriteLine ("Main: iks= " + b.X + " b.F()= " + b.F());


> >

b.Fl ( );

static public void Handler (int m) {Console.WriteLine ("Handler: x= " + m);}

/*Result: Handler: x= 5; lks= 5 b.F() = B.F A,F1 */ /

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

13.2. Нововведения в языке C# Рассмотрим некоторые нововведения в языке C#. В версии C# 2.0 введены общие типы (generics), упрощена работа с делегатами и событиями, введены статические классы, дозволяется описание класса частями и другое. Всего сделано четырнадцать усовершенствований языка. Мы уделим внимание делегатам, событиям и статическим классам. 13.2.1.Делегаты и события В версии C# 2.0 упрощена инициализация делегатов. Как мы знаем, в первой версии C# 1.0 явно создавался объект делегата с инкапсулированной функцией. При этом, как принято при создании любого объекта, применялся оператор new. C# 1.0. ссылка-на-делегат = new имя-делегата (имя-класса.имя-стат-функции); ссылка-на-делегат = new имя-делегата (ссылка-на-объект.имя-об-функции);

В новой же версии C# 2.0 объекту делегата упрощенно присваивается непосредственно имя статической функции или ссылка на объект с именем обычной функции. C# 2.0. Упрощенная инициализация делегата

ссылка-на-делегат = имя-стат-функции; ссылка-на-делегат = ссылка-на-объект.имя-об-функции;

Упростилось и добавление в объект делегата и удаление из объекта делегата указателей функций


C# 2.0. ссылка-на-делегат += {ссылка-на-объект.имя-об-функции| имя-стат-функции>; ссылка-на-делегат -= {ссылка-на-объект.имя-об-функции| и мя -стат-фун кци и >;

Также в версии C# 2.0 появилась возможность добавления в объект делегата анонимных функций (анонимных методов). Вспомним удобное применение анонимных классов в языке Java . Java или J#. // Подписать обработчик на событие кнопки butRun butRun.addActlonLlstener (new ActionListener ( ) { public void actionPerformed (ActionEvent aE) { for (int i= 0; i<aL.size(); i++) {

»;

>

>

Lorry lorry= (Lorry) aL.get(i); iorry.Run ( );

Как видим, объект анонимного класса создаётся непосредственно как аргумент функции addActionListener(), осуществляющей подписку этого объекта на событие нажатия кнопки "Run". Создатели новой версии C# 2.0 предложили при передаче имени функции делегату передать ему всё тело анонимной функции - функции, которая не имеет заголовка. Здесь обязательно применяется ключевое слово delegate. C# 2.0. ссылка-на-делегат += delegate{Teflo-aHOHHMHoft^yHKunn}; ссылка-на-делегат -= delegate{Teflo-aHOHHMHoft-cj)yHKUMH>;

При этом авторы версии C# 2.0 позаботились и о возможности передачи параметров этой анонимной функции, поместив список параметров в скобках после ключевого слова delegate. C# 2.0. ссылка-на-делегат фуНкции}; ссылка-на-делегат функции};

+= -=

delegate delegate

(список-параметров)

{тело-анонимной-

(список-параметров)

{тело-анонимной-


Поскольку события являются частным случаем делегата, то для них в C# 2.0 имеют место те же упрощения, что и для делегатов. Напомним, что при объявлении событий необходимо применить ключевое слово event, и что в отличие от делегата, который может выполняться там, где надо выполнить инкапсулированные в нём функции, событие может быть сгенерировано (выполнено) только в источнике - объекте, в классе которого объявлено событие с ключевым словом event. Теперь вместо C # 1.0. // Подписать обработчик butAdd,Click += new EventHandler (But_Add); // Обработать нажатие кнопки пуска private void But_Run (object о, EventArgs e) { IEnumerator inum= aL.GetEnumerator (); while (lnum.MoveNext ()) {

>

>

Lorry lorry= (Lorry) inum.Current; lorry.Run ( );

можно упрощённо написать C # 2.0. butAdd.Click += delegate (object о, EventArgs e)

{

IEnumerator inum= aL.GetEnumerator (); while (lnum.MoveNext ())

{

>;

>

Lorry lorry= (Lorry) inum.Current; lorry.Run ();

13.3.2. Статические классы Появление статических классов в версии C# 2.0 - это стремление фирмы Microsoft увеличить надёжность разрабатываемых программ. Уже говорилось, что применение статических функций требует при ссылке к ним использовать имя класса, в котором они описаны. При этом нет необходимости создавать объект класса, если класс не содержит обычные функции, при применении которых понадобятся ссылки к этому объекту класса. Использование статических функций просто. Напомним статические функции Console.WriteLine(), Thread.Sleep(), Monitor.Enter(). Для вызова стати-


ческой функции достаточно написать имя класса и через точку имя самой функции. Так может быть определить класс так, чтобы в нём дозволялись только статические функции и данные? Во второй версии C# 2.0 так и поступили. Теперь в языке C# можно описать такой статический класс. C# 2.0. static class имя-класса

< >

юлько статические функции и данные

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


Заключение Прочитав книгу, Вы познакомились не только с основами объектно-ориентированного программирования, но и с принципиальными особенностями языков C++/CLI, Java, J# и C#, что облегчит дальнейшее освоение этих языков и написание объектно-ориентированных программ. Потоки, события, источники собьггий и уведомляющие или наблюдаемые объекты, приёмники событий или обозреватели, виртуальные функции и абстрактные классы - как интересно, схоже и по-разному они реализованы в этих языках! Очаровывают анонимные классы и делегаты. Насколько делегаты упростили и сделали понятным описание потоков и событий на языке C#! А сколько ещё интересного есть в этих языках! Своеобразно и поразному создаются и применяются компоненты - особые объекты, из которых строятся сложные надёжные и безопасные программы. Компонентноориентированное программирование, основываясь на одних и тех же принципах, тем не менее, существенно различается на .NET платформе языков C# и C++/CLI от платформы языка Java. Полученные знания в этой книге и желание познать и одолеть новое, надеюсь, помогут сломать барьер, и более раскрыть интереснейший мир объектно- и компонентно-ориентированного программирования на выбранном языке программирования.


Список литературы 1. Аравинд Kopepa, Стивен Фрейзер, Сэм Джентайл, Нираджан Кумар, Скотт Маклин, Саймон Робинсон, П.Г. Саранг. Visual C++.NET: Пособие для разработчиков С++. - M.: Издательство "ЛОРИ", 2003. - 398 c.: ил. 2. Байдачный C.C. .NET Framework. Секреты создания Windows-приложений. - M.: СОЛОН-Пресс, 2004. - 496 c.: ил. 3. Иванова Г. С. и др. Объектно-ориентированное программирование: Учеб. для вузов. - M.: Изд-во МВТУ им. Н.Э.Баумана, 2001.- 320 c.: ил. 4. Либерти Д. Программирование на C#: 2-е издание. - СПб.: Символ-Плюс, 2003.-688 c.: ил. 5. Нейгел К. и др. C# 2005 для профессион