Uploaded by rel.for

C#2010-net-4-EndrewTroelson

advertisement
ЯЗЫК ПРОГРАММИРОВАНИЯ
C# 2010
И ПЛАТФОРМА .NET 4
5-е издание
PRO C# 2010
AND THE
.NET 4 PLATFORM
Fifth Edition
Andrew Troelsen
Apress
ЯЗЫК ПРОГРАММИРОВАНИЯ
C# 2010
И ПЛАТФОРМА .NET 4
5-е издание
Эндрю Троелсен
W
Москва • Санкт-Петербург • Киев
2011
ББК 32.973.26-018.2.75
Т70
УДК 681.3.07
И здательский дом “В ильям с”
Зав. редакцией С.Н. Тригуб
Перевод с английского Я.П. Волковой, А Л . М оргунова, Н.А. Мухина
Под редакцией Ю.Н. Арт ем енко
По общ им вопросам обращ айтесь в Издательский дом “В и льям с” по адресу:
info@ williainspublishing.com , http://www.williamspublishing.com
Троелсен, Эндрю.
Т70
Язык программирования C# 2010 и платформа .NET 4.0, 5-е изд. : Пер. с англ. —
М. : ООО “И.Д. Вильямс”, 2011. — 1392 с. : ил. — Парал. тит. англ.
ISBN 978-5-8459-1682-2 (рус.)
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками
соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой
бы то ни было форме и какими бы то ни было средствами, будь то электронные или механиче­
ские, включая фотокопирование и запись на магнитный носитель, если на это нет письменного
разрешения издательства APress, Berkeley, СА.
Authorized translation from the English language edition published by APress, Inc., Copyright © 2010.
All rights reserved. No part of this work may be reproduced or transmitted in any form or by any
means, electronic or mechanical, including photocopying, recording, or by any information storage or
retrieval system, without the prior written permission of the copyright owner and the publisher.
Trademarked names may appear in this book. Rather than use a trademark symbol with every
occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit
of the trademark owner, with no intention of infringement of the trademark.
Russian language edition is published by Williams Publishing House according to the Agreement
with R&I Enterprises International, Copyright ©2011.
Научно-популярное издание
Эндрю Троелсен
Язык программирования C# 2010
и платформа .NET 4.0
5-е издание
Верстка
Художественный редактор
Т.Н. Артеменко
С.А. Чернокозинский
Подписано в печать 28.10.2010. Формат 70x100/16.
Гарнитура Times. Печать офсетная.
Уел. печ. л. 112,23. Уч.-изд. л. 87,1.
Тираж 2000 экз. Заказ № 24430.
Отпечатано по технологии CtP
в ОАО “Печатный двор” им. А. М. ГЪрького
197110, Санкт-Петербург, Чкаловский пр., 15.
ООО “И. Д. Вильямс”, 127055, г. Москва, ул. Лесная, д. 43, стр. 1
ISBN 978-5-8459-1682-2 (рус.)
ISBN 978-1-43-022549-2 (англ.)
© Издательский дом “Вильямс”, 2011
© by Andrew Ttoelsen, 2010
Оглавление
Часть I. Общие сведения о языке C# и платформе .NET
43
DiaBa 1. Философия .NET
DiaBa 2. Создание приложений на языке C#
44
80
Часть II. Главные конструкции программирования на C#
105
DiaBa 3. Пгавные конструкции программирования на С#: часть I
DiaBa 4. DiaBHbie конструкции программирования на С#: часть II
DiaBa 5. Определение инкапсулированных типов классов
106
150
185
231
265
292
DiaBa 6. Понятия наследования и полиморфизма
DiaBa 7. Структурированная обработка исключений
DiaBa 8. Время жизни объектов
Часть III. Дополнительные конструкции программирования на C#
DiaBa 9. Работа с интерфейсами
DiaBa 10. Обобщения
DiaBa 11. Делегаты, события и лямбда-выражения
DiaBa 12. Расширенные средства языка С #
DiaBa 13. LINQ to Objects
Часть IV. Программирование с использованием сборок .NET
DiaBa 14. Конфигурирование сборок .NET
319
320
356
386
421
463
493
494
DiaBa 15. Рефлексия типов, позднее связывание и программирование
с использованием атрибутов
DiaBa 16. Процессы, домены приложений и контексты объектов
DiaBa 17. Язык CIL и роль динамических сборок
DiaBa 18. Динамические типы и исполняющая среда динамического языка
Часть V. Введение в библиотеки базовых классов .NET
DiaBa 19. Многопоточность и параллельное программирование
DiaBa 20. Файловый ввод-вывод и сериализация объектов
DiaBa 21. ADO.NET, часть I: подключенный уровень
DiaBa 22. ADO.NET, часть II: автономный уровень
DiaBa 23. ADO.NET, часть III: Entity Framework
DiaBa 24. Введение в LINQ to XML
DiaBa 25. Введение в Windows Communication Foundation
DiaBa 26. Введение в Windows Workflow Foundation 4.0
Часть VI. Построение настольных пользовательских
приложений с помощью WPF
DiaBa 27. Введение в Windows Presentation Fbundation и XAML
DiaBa 28. Программирование с использованием элементов управления WPF
DiaBa 29. Службы визуализации графики WPF
DiaBa 30. Ресурсы, анимация и стили WPF
542
582
607
648
669
670
711
754
804
857
891
906
961
995
996
1048
1103
1137
DiaBa 31. Шаблоны элементов управления WPF и пользовательские
элементы управления
Часть VII. Построение веб-приложений с использованием ASP.NET
DiaBa 32. Построение веб-страниц ASP.NET
DiaBa 33. Веб-элементы управления, мастер-страницы и темы ASP.NET
DiaBa 34. Управление состоянием в ASP.NET
1170
1213
1214
1257
1294
Часть VIII. Приложения
1327
Приложение А. Программирование с помощью Windows Forms
Приложение Б. Независимая от платформы разработка .NET-приложений
с помощью Mono
1328
Предметный указатель
1369
1386
Содержание
Об авторе
О техническом редакторе
Благодарности
31
31
31
Введение
Автор и читатели — одна команда
Краткий обзор содержания
Исходный код примеров
От издательства
32
33
33
42
42
Часть I. Общие сведения о языке C# и платформе .NET
43
Глава 1. Философия .NET
44
44
45
45
45
46
46
47
48
49
50
50
52
54
54
56
56
58
58
59
60
60
61
61
62
62
63
63
63
64
66
66
67
71
71
73
74
74
74
74
75
Предыдущее состояние дел
Подход с применением языка С и API-интерфейса Windows
Подход с применением языка C++ и платформы MFC
Подход с применением Visual Basic 6.0
Подход с применением Java
Подход с применением СОМ
Сложность представления типов данных СОМ
Решение .NET
Главные компоненты платформы .NET (CLR, CTS и CLS)
Роль библиотек базовых классов
Что привносит язык C#
Другие языки программирования с поддержкой .NET
Ж изнь в многоязычном окружении
Что собой представляют сборки в .NET
Однофайловые и многофайловые сборки
Роль CIL
Преимущества CIL
Компиляция CIL-кода в инструкции, ориентированные на конкретную платформу
Роль метаданных типов в .NET
Роль манифеста сборки
Что собой представляет общая система типов (CTS)
Типы классов
Типы интерфейсов
Типы структур
Типы перечислений
Типы делегатов
Члены типов
Встроенные типы данных
Что собой представляет общеязыковая спецификация (CLS)
Забота о соответствии правилам CLS
Что собой представляет общеязыковая исполняющая среда (CLR)
Различия между сборками, пространствами имен и типами
Роль корневого пространства Microsoft
Получение доступа к пространствам имен программным образом
Добавление ссылок на внешние сборки
Изучение сборки с помощью утилиты ildasm.ехе
Просмотр CIL-кода
Просмотр метаданных типов
Просмотр метаданных сборки (манифеста)
Изучение сборки с помощью утилиты Reflector
Содержание
7
Развертывание исполняющей среды .NET
Клиентский профиль исполняющей среды .NET
Не зависящая от платформы природа .NET
Резюме
76
77
77
79
Глава 2. Создание приложений на языке C#
80
80
81
Роль комплекта .NET Framework 4 .0 SDK
Окно командной строки в Visual Studio 2010
Создание приложений на C# с использованием esc. ехе
Указание целевых входных и выходных параметров
Добавление ссылок на внешние сборки
Добавление ссылок на несколько внешних сборок
Компиляция нескольких файлов исходного кода
Работа с ответными файлами в C#
Создание приложений .NET с использованием Notepad++
Создание приложений.NET с помощью SharpDevelop
Создание простого тестового проекта
Создание приложений .NET с использованием Visual С# 2010 Express
Некоторые уникальные функциональные возможности Visual C# 2010 Express
Создание приложений .NET с использованием Visual Studio 2010
Некоторые уникальные функциональные возможности Visual Studio 2010
Ориентирование на .NET Framework в диалоговом окне New Project
Использование утилиты Solution Explorer
Утилита Class View
Утилита Obj ect Browser
Встроенная поддержка рефакторинга программного кода
Возможности для расширения и окружения кода
Утилита Class Designer
Интегрируемая система документации .NET Framework 4.0
Резюме
89
90
91
92
92
93
93
95
95
96
98
100
102
104
Часть II. Главные конструкции программирования на C#
105
Глава 3. Главные конструкции программирования на С#: часть I
106
106
108
109
110
111
Разбор простой программы на C#
Варианты метода Ма iп ()
Спецификация кода ошибки в приложении
Обработка аргументов командной строки
Указание аргументов командной строки в Visual Studio 2010
Интересное отклонение от темы: некоторые дополнительные члены
класса System.Environment
Класс System.Console
Базовый ввод-вывод с помощью класса Console
Форматирование вывода, отображаемого в окне консоли
Форматирование числовых данных
г
Форматирование числовых данных в приложениях, отличных от консольных
Системные типы данных и их сокращенное обозначение в C#
Объявление и инициализация переменных
Внутренние типы данных и операция new
Иерархия классов типов данных
Члены числовых типов данных
Члены System.Boolean
Члены System.Char
Синтаксический разбор значений из строковых данных
81
82
84
84
85
85
87
88
112
113
114
115
116
117
117
119
120
121
123
123
124
125
8
Содержание
Типы System.DateTime и System.TimeSpan
Пространство имен System.Numerics в .NET4.0
Работа со строковыми данными
Базовые операции манипулирования строками
Конкатенация строк
Управляющие последовательности символов
Определение дословных строк
Строки и равенство
Неизменная природа строк
Тип System.Text.StringBuilder
Сужающие и расширяющие преобразования типов данных
Перехват сужающих преобразований данных
Настройка проверки на предмет возникновения условий
переполнения в масштабах проекта
Ключевое слово unchecked
Роль класса System.Convert
Неявно типизированные локальные переменные
Ограничения, связанные с неявно типизированными переменными
Неявно типизированные данные являются строго типизированными
Польза от неявно типизированных локальных переменных
Итерационные конструкции в С #
Цикл for
Цикл foreach
Использование var в конструкциях foreach
Конструкции wh i 1е и do /while
Конструкции принятия решений и операции сравнения
Оператор if/else
Оператор switch
Резюме
125
126
127
128
129
130
131
131
132
133
135
137
Глава 4. Главные конструкции программирования на С#: часть II
150
150
151
152
153
154
156
157
158
160
161
162
163
163
165
165
167
Методы и модификаторы параметров
Стандартное поведение при передаче параметров
Модификатор out
Модификатор ref
Модификатор par ams
Определение необязательных параметров
Вызов методов с использованием именованных параметров
Перегрузка методов
Массивы в С #
Синтаксис инициализации массивов в С #
Неявно типизированные локальные массивы
Определение массива объектов
Работа с многомерными массивами
Использование массивов в качестве аргументов и возвращаемых значений
Базовый класс System.Array
Тип enum
Управление базовым типом, используемым для хранения
значений перечисления
Объявление переменных типа перечислений
Тип System.Enum
Динамическое обнаружение пар “имя/значение” перечисления
Типы структур
Создание переменных типа структур
139
139
140
140
142
143
144
144
145
145
146
146
147
147
148
149
168
168
169
170
172
173
Содержание
Типы значения и ссылочные типы
Типы значения, ссылочные типы и операция присваивания
Типы значения, содержащие ссылочные типы
Передача ссылочных типов по значению
Передача ссылочных типов по ссылке
Заключительные детали относительно типов значения и ссылочных типов
Нулевые типы в C#
Работа с нулевыми типами
Операция??
Резюме
Глава 5. Определение инкапсулированных типов классов
Знакомство с типом класса C#
Размещение объектов с помощью ключевого слова new
Понятие конструктора
Роль конструктора по умолчанию
Определение специальных конструкторов
Еще раз о конструкторе по умолчанию
Роль ключевого слова t h is
Построение цепочки вызовов конструкторов с использованием t h i s
Обзор потока конструктора
Еще раз об необязательных аргументах
Понятие ключевого слова s t a t i c
Определение статических методов
Определение статических полей данных
Определение статических конструкторов
Определение статических классов
Основы объектно-ориентированного программирования
Роль инкапсуляции
Роль наследования
Роль полиморфизма
Модификаторы доступа С#
Модификаторы доступа по умолчанию
Модификаторы доступа и вложенные типы
Первый принцип: службы инкапсуляции C#
Инкапсуляция с использованием традиционных методов доступа и изменения
Инкапсуляция с использованием свойств .NET
Использование свойств внутри определения класса
Внутреннее представление свойств
Управление уровнями видимости операторов get/set свойств
Свойства, доступные только для чтения и только для записи
Статические свойства
Понятие автоматических свойств
Взаимодействие с автоматическими свойствами
Замечания относительно автоматических свойств и значений по умолчанию
Понятие синтаксиса инициализации объектов
Вызов специальных конструкторов с помощью синтаксиса инициализации
Инициализация вложенных типов
Работа с данными константных полей
Понятие полей только для чтения
Статические поля только для чтения
Понятие частичных типов
Резюме
9
174
175
177
178
179
180
181
183
184
184
185
185
187
188
188
189
190
191
193
195
196
197
198
198
201
202
203
204
204
206
207
208
208
209
210
212
214
215
217
218
218
219
221
221
223
224
225
226
228
228
229
230
10
Содержание
Глава 6. Понятия наследования и полиморфизма
231
Базовый механизм наследования
Указание родительского класса для существующего класса
О множественном наследовании
Ключевое слово s e a le d
Изменение диаграмм классов Visual Studio
Второй принцип ООП: подробности о наследовании
Управление созданием базового класса с помощью ключевого слова base
Хранение фамильных тайн: ключевое слово protected
Добавление запечатанного класса
Реализация модели включения/делегации
Определения вложенных типов
Третий принцип ООП: поддержка полиморфизма в C#
Ключевые слова virtual и override
Переопределение виртуальных членов в Visual Studio 2010
Запечатывание виртуальных членов
Абстрактные классы
Полиморфный интерфейс
Сокрытие членов
Правила приведения к базовому и производному классу
Ключевое слово as
Ключевое слово is
Родительский главный класс System.Object
Переопределение System.Object.ToStringO
Переопределение System.Object .Equals ()
Переопределение System.Object.GetHashCode()
Тестирование модифицированного класса Person
Статические члены System.Object
Резюме
231
232
234
234
235
236
238
240
241
242
243
245
245
247
248
249
250
253
255
257
257
258
261
261
262
263
264
264
Глава 7. Структурированная обработка исключений
265
265
266
267
268
269
271
272
273
273
274
275
275
277
278
278
280
281
282
284
285
286
287
287
Ода ошибкам и исключениям
Роль обработки исключений в .NET
Составляющие процесса обработки исключений в .NET
Базовый класс System. Exception
Простейший пример
Генерация общего исключения
Перехват исключений
Конфигурирование состояния исключения
Свойство TargetSite
Свойство StackTrace
Свойство HelpLink
Свойство Data
Исключения уровня системы (System. SystemExcept ion)
Исключения уровня приложения (System. ApplicationException)
Создание специальных исключений, способ первый
Создание специальных исключений, способ второй
Создание специальных исключений, способ третий
Обработка многочисленных исключений
Общие операторы catch
Передача исключений
Внутренние исключения
Блок finally
Какие исключения могут выдавать методы
Содержание
11
К чему приводят необрабатываемые исключения
Отладка необработанных исключений с помощью Visual Studio
Несколько слов об исключениях, связанных с поврежденным состоянием
Резюме
288
289
290
291
Глава 8. Время жизни объектов
Классы, объекты и ссылки
Базовые сведения о времени жизни объектов
CIL-код, генерируемый для ключевого слова new
Установка объектных ссылок в nu 11
Роль корневых элементов приложения
Поколения объектов
Параллельная сборка мусора в версиях .NET 1.0 — .NET 3.5
Фоновая сборка мусора в версии .NET 4.0
Тип System. GC
Принудительная активизация сборки мусора
Создание финализируемых объектов
Переопределение System. Object.Finalize ()
Описание процесса финализации
Создание высвобождаемых объектов
Повторное использование ключевого слова using в C#
Создание финализируемых и высвобождаемых типов
Формализованный шаблон очистки
Отложенная инициализация объектов
Настройка процесса создания данных Lazy о
Резюме
292
292
293
294
296
297
298
299
300
300
302
304
305
307
307
310
311
312
314
317
318
Часть III. Дополнительные конструкции программирования на C#
зш
Глава 9. Работа с интерфейсами
320
Что собой представляют типы интерфейсов
320
Сравнение интерфейсов и абстрактных базовых классов
321
Определение специальных интерфейсов
324
Реализация интерфейса
326
Вызов членов интерфейса на уровне объектов
328
Получение ссылок на интерфейсы с помощью ключевого слова as
329
Получение ссылок на интерфейсы с помощью ключевого слова is
329
Использование интерфейсов в качестве параметров
330
Использование интерфейсов в качестве возвращаемых значений
332
Массивы типов интерфейсов
332
Реализация интерфейсов с помощью Visual Studio 2010
333
Устранение конфликтов на уровне имен за счет реализации интерфейсов явным образом 334
Проектирование иерархий интерфейсов
337
Множественное наследование в случае типов интерфейсов
338
Создание перечислимых типов (IEnumerable и IE numerator)
340
Создание методов итератора с помощью ключевого слова yield
343
Создание именованного итератора
344
Внутреннее представление метода итератора
345
Создание клонируемых объектов (ICloneable)
346
Более сложный пример клонирования
348
Создание сравнимых объектов (IComparable)
350.
Указание множества критериев для сортировки (IComparer)
353
Использование специальных свойств и специальных типов для сортировки
354
Резюме
355
12
Содержание
Глава 10. Обобщения
Проблемы, связанные с необобщенными коллекциями
Проблема производительности
Проблемы с безопасностью типов
Роль параметров обобщенных типов
Указание параметров типа для обобщенных классов и структур
Указание параметров типа для обобщенных членов
Указание параметров типов для обобщенных интерфейсов
Пространство имен System.Collections .Generic
Синтаксис инициализации коллекций
Работа с классом List<T>
Работа с классом Stack<T>
Работа с классом Queue<Т>
Работа с классом SortedSet<T>
Создание специальных обобщенных методов
Выведение параметра типа
Создание специальных обобщенных структур и классов
Ключевое слово de fau It в обобщенном коде
Обобщенные базовые классы
Ограничение параметров типа
Примеры использования ключевого слова where
Недостаток ограничений операций
Резюме
Глава 11. Делегаты, события и лямбда-выражения
Понятие типа делегата .NET
Определение типа делегата в C#
Базовые классы System.MulticastDelegate и System.Delegate
Простейший пример делегата
Исследование объекта делегата
Отправка уведомлений о состоянии объекта с использованием делегатов
Включение группового вызова
Удаление целей из списка вызовов делегата
Синтаксис групповых преобразований методов
Понятие ковариантности делегатов
Понятие обобщенных делегатов
Эмуляция обобщенных делегатов без обобщений
Понятие событий C#
Ключевое слово event
“За кулисами” событий
Прослушивание входящих событий
Упрощенная регистрация событий с использованием Visual Studio 2010
Создание специальных аргументов событий
Обобщенный делегат EventHandler<T>
Понятие анонимных методов C#
Доступ к локальным переменным
Понятие лямбда-выражений
Анализ лямбда-выражения
Обработка аргументов внутри множества операторов
Лямбда-выражения с несколькими параметрами и без параметров
Усовершенствование примера PrimAndProperCarEvents за счет
использования лямбда-выражений
Резюме
356
356
358
362
365
366
367
367
369
370
371
373
374
375
376
378
379
380
381
382
383
384
385
386
386
387
389
391
392
393
396
397
398
400
402
403
404
405
406
407
408
409
410
411
413
413
416
417
418
419
419
Содержание
13
Глава 12. Расширенные средства языка C#
421
Понятие методов-индексаторов
Индексация данных с использованием строковых значений
Перегрузка методов-индексаторов
Многомерные индексаторы
Определения индексаторов в интерфейсных типах
Понятие перегрузки операций
Перегрузка бинарных операций
А как насчет операций += и -=?
Перегрузка унарных операций
Перегрузка операций эквивалентности
Перегрузка операций сравнения
.внутреннее представление перегруженных операций
Финальные соображения относительно перегрузки операций
Понятие преобразований пользовательских типов
Числовые преобразования
Преобразования между связанными типами классов
Создание специальных процедур преобразования
Дополнительные явные преобразования типа Square
Определение процедур неявного преобразования
Внутреннее представление процедур пользовательских преобразований
Понятие расширяющих методов
Понятие частичных методов
Понятие анонимных типов
Анонимные типы, содержащие другие анонимные типы
Работа с типами указателей
Ключевое слово u nsafe
Работа с операциями * и &
Небезопасная и безопасная функция обмена значений
Доступ к полям через указатели (операция ->)
Ключевое слово s t a c k a llo c
Закрепление типа ключевым словом f i x e d
Ключевое слово s i z e o f
Резюме
421
423
424
425
425
426
427
429
429
430
431
432
433
434
434
434
435
437
438
439
440
448
450
454
455
456
458
459
459
460
460
461
462
Глава 13. LINQ to Objects
463
463
464
464
465
466
466
467
468
468
469
470
471
472
473
474
475
475
476
Программные конструкции, специфичные для LINQ
Неявная типизация локальных переменных
Синтаксис инициализации объектов и коллекций
Лямбда-выражения
Расширяющие методы
Анонимные типы
Роль LINQ
Выражения LINQ строго типизированы
Основные сборки LINQ
Применение запросов LINQ к элементарным массивам
Решение без использования LINQ
Рефлексия результирующего набора LINQ
LINQ и неявно типизированные локальные переменные
LINQ и расширяющие методы
Роль отложенного выполнения
Роль немедленного выполнения
Возврат результата запроса LINQ
Возврат результатов LINQ через немедленное выполнение
14
Содержание
Применение запросов LINQ к объектам коллекций
Доступ к содержащимся в контейнере подобъектам
Применение запросов LINQ к необобщенным коллекциям
Фильтрация данных с использованием OfType<T>()
Исследование операций запросов LINQ
Базовый синтаксис выборки
Получение подмножества данных
Проекция новых типов данных
Получение счетчиков посредством Enumerable
Обращение результирующих наборов
Выражения сортировки
LINQ как лучшее средство построения диаграмм
Исключение дубликатов
Агрегатные операции LINQ
Внутреннее представление операторов запросов LINQ
Построение выражений запросов с использованием операций запросов
Построение выражений запросов с использованием типа Enumerable
и лямбда-выражений
Построение выражений запросов с использованием типа Enumerable
и анонимных методов
Построение выражений запросов с использованием типа Enumerable
и низкоуровневых делегатов
Резюме
•
477
478
478
479
480
481
482
482
484
484
484
485
486
486
487
488
488
490
490
491
Часть IV. Программирование с использованием сборок .NET
493
Глава 14. Конфигурирование сборок .NET
494
494
Определение специальных пространств имен
Устранение конфликтов на уровне имен за счет использования полностью
уточненных имен
Устранение конфликтов на уровне имен за счет использования псевдонимов
Создание вложенных пространств имен
Пространство имен, используемое по умолчанию в Visual Studio 2010
Роль сборок .NET
Сборки повышают возможность повторного использования кода
Сборки определяют границы типов
Сборки являются единицами, поддерживающими версии
Сборки являются самоописываемыми
Сборки поддаются конфигурированию
Формат сборки .NET
Заголовок файла Windows
Заголовок файла CLR
CIL-код, метаданные типов и манифест сборки
Необязательные ресурсы сборки
Однофайловые и многофайловые сборки
Создание и использование однофайловой сборки
Исследование манифеста
Исследование CIL-кода
Исследование метаданных типов
Создание клиентского приложения на С #
Создание клиентского приложения на Visual Basic
Межъязыковое наследование в действии
Создание и использование многофайловой сборки
Исследование файла uf о .netmodule
496
497
498
499
500
500
501
501
501
501
502
502
504
505
505
505
506
510
512
512
513
514
515
516
517
Содержание
Исследование файла airvehicles .dll
Использование многофайловой сборки
Приватные сборки
Идентификационные данные приватной сборки
Процесс зондирования
Конфигурирование приватных сборок
Конфигурационные файлы и Visual Studio 2010
Разделяемые сборки
Строгие имена
Генерирование строгих имен в командной строке
Генерирование строгих имен с помощью Visual Studio 2010
Установка сборок со строгими именами в GAC
Просмотр содержимого GAC с помощью проводника Windows
Использование разделяемой сборки
Исследование манифеста SharedCarLibClient
Конфигурирование разделяемых сборок
Фиксация текущей версии разделяемой сборки
Создание разделяемой сборки версии 2.0.0.0
Динамическое перенаправление на конкретную версию разделяемой сборки
Сборки политик издателя
Отключение политик издателя
Элемент <codeBase>
Пространство имен System. Configuration
Резюме
Глава 15. Рефлексия типов, позднее связывание и программирование
с использованием атрибутов
Необходимость в метаданных типов
Просмотр (части) метаданных перечисления EngineState
Просмотр (части) метаданных типа Саг
Изучение блока ТурeRef
Просмотр метаданных самой сборки
Просмотр метаданных внешних сборок, на которые имеются ссылки в текущей сборке
Просмотр метаданных строковых литералов
Рефлексия
Класс System.Туре
Получение информации о типе с помощью System.Object.GetType ()
Получение информации о типе с помощью typeof ()
Получение информации о типе с помощью System.Туре .GetType ()
Создание специальной программы для просмотра метаданных
Рефлексия методов
Рефлексия полей и свойств
Рефлексия реализуемых интерфейсов
Отображение различных дополнительных деталей
Реализация метода Main ()
Рефлексия обобщенных типов
Рефлексия параметров и возвращаемых значений методов
Динамически загружаемые сборки
Рефлексия разделяемых сборок
Позднее связывание
Класс System.Activator
Вызов методов без параметров
Вызов методов с параметрами
Роль атрибутов .NET
15
518
518
519
520
520
521
522
524
524
526
528
529
530
531
532
533
533
533
536
537
538
538
540
541
542
542
543
544
546
546
546
547
547
548
549
549
549
550
550
551
552
552
552
554
554
556
558
560
560
562
563
564
16
Содержание
Потребители атрибутов
Применение предопределенных атрибутов в C#
Сокращенное обозначение атрибутов в C#
Указание параметров конструктора для атрибутов
Атрибут [Obsolete] в действии
Создание специальных атрибутов
Применение специальных атрибутов
Синтаксис именованных свойств
Ограничение использования атрибутов
Атрибуты уровня сборки и модуля
Файл Assembly Inf о .cs, генерируемый Visual Studio 2010
Рефлексия атрибутов с использованием раннего связывания
Рефлексия атрибутов с использованием позднего связывания
Возможное применение на практике рефлексии, позднего связывания
и специальных атрибутов
Создание расширяемого приложения
Создание сборки CommonSnappableTypes .dll
Создание оснастки на C#
Создание оснастки на Visual Basic
Создание расширяемого приложения Windows Forms
Резюме
565
565
567
567
567
568
569
569
570
571
572
572
573
Глава 16. Процессы, домены приложений и контексты объектов
582
582
583
585
587
588
588
590
592
592
594
594
596
597
598
599
600
601
602
603
604
604
606
606
Роль процесса Windows
Роль потоков
Взаимодействие с процессами в рамках платформы .NET
Перечисление выполняющихся процессов
Изучение конкретного процесса
Изучение набора потоков процесса
Изучение набора модулей процесса
Запуск и останов процессов программным образом
Управление запуском процесса с использованием класса ProcessStartlnfo
Домены приложений .NET
Класс System.AppDomain
Взаимодействие с используемым по умолчанию доменом приложения
Перечисление загружаемых сборок
Получение уведомлений о загрузке сборок
Создание новых доменов приложений
Загрузка сборок в специальные домены приложений
Выгрузка доменов приложений программным образом
Границы контекстов объектов
Контекстно-свободные и контекстно-зависимые типы
Определение контекстно-зависимого объекта
Инспектирование контекста объекта
Итоговые сведения о процессах, доменах приложений и контекстах
Резюме
Глава 17. Язык CIL и роль динамических сборок
Причины для изучения грамматики языка CIL
Директивы, атрибуты и коды операций в CIL
Роль директив CIL
Роль атрибутов CIL
1
Роль кодов операций CIL
Разница между кодами операций и их мнемоническими эквивалентами в CIL
575
576
576
577
577
578
581
607
607
608
609
609
609
609
Содержание
Помещение и извлечение данных из стека в CIL
Двунаправленное проектирование
Роль меток в коде CIL
Взаимодействие с CIL: модификация файла * . i l
Компиляция CIL-кода с помощью i 1a sm. ехе
Создание CIL-кода с помощью SharpDevelop
P o л ь p e v e r if у . ехе
Использование директив и атрибутов в CIL
Добавление ссылок на внешние сборки в CIL
Определение текущей сборки в CIL
Определение пространств имен в CIL
Определение типов классов в CIL
Определение и реализация интерфейсов в CIL
Определение структур в CIL
Определение перечислений в CIL
Определение обобщений в CIL
Компиляция файла СILTypes . i l
Соответствия между типами данных в библиотеке базовых классов .NET, C# и CIL
Определение членов типов в CIL
Определение полей данных в CIL
Определение конструкторов для типов в CIL
Определение свойств в CIL
Определение параметров членов
Изучение кодов операций в CIL
Директива .m axstack
Объявление локальных переменных в CIL
Отображение параметров на локальные переменные в CIL
Скрытая ссылка t h is
Представление итерационных конструкций в CIL
Создание сборки .NET на CIL
Создание СIL C a r s . d l l
Создание C lL C a r C lie n t . ехе
Динамические сборки
Пространство имен System. Re f l e e t io n .Em it
Роль типа S y s t e m .R e fle c t io n .E m it. IL G e n e ra to r
Создание динамической сборки
Генерация сборки и набора модулей
Роль типа M od u leB u ild er
Генерация типа H e llo C la s s n принадлежащей ему строковой переменной
Генерация конструкторов
Генерация метода S a y H ello ()
Использование динамически сгенерированной сборки
Резюме
Глава 18. Динамические типы и исполняющая среда динамического языка
Роль ключевого слова C# dynamic
Вызов членов на динамически объявленных данных
Роль сборки Microsoft.CSharp.dll
Область применения ключевого слова dynamic
Ограничения ключевого слова dynamic
Практическое применение ключевого слова dynamic
Роль исполняющей среды динамического языка (DLR)
Роль деревьев выражений
Роль пространства имен System. Dynamic
17
610
612
615
616
617
618
619
619
619
620
620
621
622
623
623
623
624
625
626
626
627
627
628
628
631
631
632
633
633
634
634
637
638
639
640
641
642
643
644
645
646
646
647
648
648
650
651
652
653
653
654
654
655
18
Содержание
Динамический поиск в деревьях выражений во время выполнения
Упрощение вызовов позднего связывания с использованием динамических типов
Использование ключевого слова dynamic для передачи аргументов
Упрощение взаимодействия с СОМ посредством динамических данных
Роль первичных сборок взаимодействия
Встраивание метаданных взаимодействия
Общие сложности взаимодействия с СОМ
Взаимодействие с СОМ с использованием средств языка C# 4.0
Взаимодействие с СОМ без использования средств языка C# 4.0
Резюме
655
656
657
659
660
661
661
662
666
667
Часть V. Введение в библиотеки базовых классов .NET
669
Глава 19. Многопоточность и параллельное программирование
670
670
671
672
672
674
675
675
676
676
678
679
680
681
682
683
684
684
685
685
687
688
689
690
692
694
695
696
697
698
700
700
701
702
703
704
705
708
709
709
710
Отношения между процессом, доменом приложения, контекстом и потоком
Проблема параллелизма
Роль синхронизации потоков
Краткий обзор делегатов .NET
Асинхронная природа делегатов
М етоды Beginlnvoke() HEndlnvokeO
Интерфейс System.IAsyncResult
Асинхронный вызов метода
Синхронизация вызывающего потока
Роль делегата AsyncCallback
Роль класса AsyncResult
Передача и прием специальных данных состояния
Пространство имен System.Threading
Класс System.Threading.Thread
Получение статистики о текущем потоке
Свойство Name
Свойство Priority
Программное создание вторичных потоков
Работа с делегатом ThreadStart
Работа с делегатом ParametrizedThreadStart
Класс AutoResetEvent
Потоки переднего плана и фоновые потоки
Пример проблемы, связанной с параллелизмом
Синхронизация с использованием ключевого слова C# lock
Синхронизация с использованием типа System.Threading.Monitor
Синхронизация с использованием типа System.Threading. Interlocked
Синхронизация с использованием атрибута [Synchronization]
Программирование с использованием обратных вызовов Tim er
Пул потоков CLR
Параллельное программирование на платформе .NET
Интерфейс Thsk Parallel Library API
Роль класса Parallel
Понятие параллелизма данных
Класс Task
Обработка запроса на отмену
Понятие параллелизма задач
Запросы параллельного LINQ (PLINQ)
Выполнение запроса PLINQ
Отмена запроса PLINQ
Резюме
л
Содержание
Глава 20. Файловый ввод-вывод и сериализация объектов
Исследование пространства имен System.10
Классы Directory (Directorylnfo) и File (Filelnfo)
Абстрактный базовый класс FileSystemlnfo
Работа с типом Directorylnfo
Перечисление файлов с помощью типа Directorylnfo
Создание подкаталогов с помощью типа Directorylnfo
Работа с типом Directory
Работа с типом Drive Info «
Работа с классом Filelnfo
Метод Filelnfo.Create ()
Метод Filelnfo.Open()
Методы FileOpen.OpenRead() и Filelnfo.OpenWrite ()
Метод Filelnfo.OpenText ()
Методы Filelnfo.CreateText () и Filelnfo.AppendText ()
Работа с типом File
Дополнительные члены File
Абстрактный класс Stream
Работа с классом FileStream
Работа с классами StreamWriter и StreamReader
Запись в текстовый файл
Чтение из текстового файла
Прямое создание экземпляров классов StreamWriter/StreamReader
Работа с классами StringWriter и StringReader
Работа с классами BinaryWriter и BinaryReader
Программное отслеживание файлов
Понятие сериализации объектов
Роль графов объектов
Конфигурирование объектов для сериализации
Определение сериализуемых типов
Общедоступные поля, приватные поля и общедоступные свойства
Выбор форматера сериализации
Интерфейсы IFormatter и IRemotingFormatter
Точность типов среди форматеров
Сериализация объектов с использованием BinaryFormatter
Десериализация объектов с использованием BinaryFormatter
Сериализация объектов с использованием SoapFormatter
Сериализация объектов с использованием XmlSerializer
Управление генерацией данных XML
Сериализация коллекций объектов
Настройка процессов сериализации SOAP и двоичной сериализации
Углубленный взгляд на сериализацию объектов
Настройка сериализации с использованием интерфейса ISerializable
Настройка сериализации с использованием атрибутов
Резюме
Глава 21. AD0.NET, часть I: подключенный уровень
Высокоуровневое определение ADO.NET
Три стороны ADO.NET
Поставщики данных ADO.NET
Поставщики данных ADO.NET от Microsoft
О сборке System.Data.OracleClient.dll
Получение сторонних поставщиков данных ADO.NET
Дополнительные пространства имен ADO.NET
19
711
711
712
713
714
715
716
717
717
719
719
720
721
722
722
722
723
724
725
726
727
728
729
730
731
732
734
736
737
737
738
738
739
740
741
742
743
743
744
746
747
748
749
751
752
754
754
755
756
757
759
759
759
20
Содержание
Типы из пространства имен System.Data
Роль интерфейса IDbConneсt ion
Роль интерфейса IDbTra ns act ion
Роль интерфейса IDbCommand
Роль интерфейсов IDbDataParameter и IDataParameter
Роль интерфейсов IDbDataAdapter и IDataAdapter
Роль интерфейсов IDataReader и IDataRecord
Абстрагирование поставщиков данных с помощью интерфейсов
Повышение гибкости с помощью конфигурационных файлов приложения
Создание базы данных AutoLot
Создание таблицы Inventory
Создание хранимой процедуры GetPetName ()
Создание таблиц Customers и Orders
Визуальное создание отношений между таблицами
Модель генератора поставщиков данных ADO.NET
Полный пример генератора поставщиков данных
Возможные трудности с моделью генератора поставщиков
Э лем ент <connectionStrings>
Подключенный уровень ADO.NET
Работа с объектами подключения
Работа с объектами ConnectionStringBuilder
Работа с объектами команд
Работа с объектами чтения данных
Получение множественных результатов с помощью объекта чтения данных
Создание повторно используемой библиотеки доступа к данным
Добавление логики подключения
Добавление логики вставки
Добавление логики удаления
Добавление логики изменения
Добавление логики выборки
Работа с параметризованными объектами команд
Выполнение хранимой процедуры
Создание консольного пользовательского интерфейса
Реализация метода Main ()
Реализация метода Showlnstructions ()
Реализация метода ListInventory ()
Реализация метода DeleteCar ()
Реализация метода InsertNewCar ()
Реализация метода UpdateCarPetName ()
Реализация метода LookUpPetName ()
Транзакции баз данных
Основные члены объекта транзакции ADO.NET
Добавление таблицы CreditRisks в базу данных AutoLot
Добавление метода транзакции в InventoryDAL
Тестирование транзакции в нашей базе данных
Резюме
Глава 22. AD0.NET, часть II: автономный уровень
Знакомство с автономным уровнем ADO.NET
Роль объектов D ataSet
Основные свойства класса DataSet
Основные методы класса DataSet
Создание DataSet
Работа с объектами DataColumn
760
761
762
762
762
763
763
764
766
767
767
769
770
772
772
774
776
777
778
779
781
782
783
784
785
786
787
788
788
789
790
792
793
794
795
795
796
797
797
798
799
800
800
801
802
803
804
804
805
806
807
807
808
Содержание
Создание объекта DataColumn
Включение автоинкрементных полей
Добавление объектов DataColumn в DataTable
Работа с объектами DataRow
Свойство RowState
Свойство DataRowVersion
Работа с объектами DataTable
Вставка объектов DataTable в DataSet
Получение данных из объекта DataSet
Обработка данных из DataTable с помощью объектов DataTableReader
Сериализация объектов DataTable и DataSet в формате XML
Сериализация объектов DataTable и DataSet в двоичном формате
Привязка объектов DataTable к графическим интерфейсам Windows Forms
Заполнение DataTable из обобщенного List<T>
Удаление строк из DataTable
Выборка строк с помощью фильтра
Изменение строк в DataTable
Работа с типом DataView
Работа с адаптерами данных
Простой пример адаптера данных
Замена имен из базы данных более понятными названиями
Добавление в AutoLotDAL.dll возможности отключения
Определение начального класса
Настройка адаптера данных с помощью SqlCommandBuilder
Реализация метода Get All Invent or у ()
Реализация метода Updatelnventory ()
Установка номера версии
Тестирование автономной функциональности
Объекты DataSet для нескольких таблиц и взаимосвязь данных
Подготовка адаптеров данных
Создание отношений между таблицами
Изменение таблиц базы данных
Переходы между взаимосвязанными таблицами
Средства конструктора баз данных в Windows Forms
Визуальное проектирование элементов DataGridView
Сгенерированный файл арр. conf ig
Анализ строго типизированного DataSet
Анализ строго типизированного DataTable
Анализ строго типизированного DataRow
Анализ строго типизированного адаптера данных
Завершение приложения Windows Forms
Выделение строго типизированного кода работы с базами данных
в библиотеку классов
Просмотр сгенерированного кода
Выборка данных с помощью сгенерированного кода
Вставка данных с помощью сгенерированного кода
Удаление данных с помощью сгенерированного кода
Вызов хранимой процедуры с помощью сгенерированного кода
Программирование с помощью LINQ to DataSet
Библиотека расширений DataSet
Получение DataTable, совместимого с LINQ
Метод расширения DataRowExtensions .Field<T> ()
Заполнение новых объектов DataTable с помощью LINQ-запросов
Резюме
21
809
810
810
810
812
813
814
815
815
816
817
818
819
820
822
823
826
826
828
829
829
830
831
831
833
833
833
833
834
835
836
837
837
839
840
843
843
845
845
845
846
847
848
849
850
850
851
851
853
853
855
855
856
22
Содержание
Глава 23. ADO.NET, часть III: Entity Framework
Роль Entity Framework
Роль сущностей
Строительные блоки Entity Framework
Роль служб объектов
Роль клиента сущности
Роль файла *.edmx
Роль классов ObjectContext и ObjectSet<T>
Собираем все вместе
Построение и анализ первой модели EDM
Генерация файла *. е dmх
Изменение формы сущностных данных
Просмотр отображений
Просмотр данных сгенерированного файла *. еdmx
Просмотр сгенерированного исходного кода
Улучшение сгенерированного исходного кода
Программирование с использованием концептуальной модели
Удаление записи
Обновление записи
Запросы с помощью LINQ to Entities
Запросы с помощью Entity SQL
Работа с объектом EntityDataReader
Проект AutoLotDAL версии 4.0, теперь с сущностями
Отображение хранимой процедуры
Роль навигационных свойств
Использование навигационных свойств внутри запросов LINQ to Entity
Вызов хранимой процедуры
Привязка данных сущностей к графическим пользовательским
интерфейсам Windows Forms
Добавление кода привязки данных
Резюме
Глава 24. Введение в LINQ to XML
История о двух API-интерфейсах XML
Интерфейс LINQ to XML как лучшая модель DOM
Синтаксис литералов Visual Basic как наилучший интерфейс LINQ to XML
Члены пространства имен System.Xml.Linq
Осевые методы LINQ to XML
Избыточность XName (и XNamespace)
Работа c XElement и XDocument
Генерация документов из массивов и контейнеров
Загрузка и разбор XML-содержимого
Манипулирование XML-документом в памяти
Построение пользовательского интерфейса приложения LINQ to XML
Импорт файла Inventory, xml
Определение вспомогательного класса LINQ to XML
Оснащение пользовательского интерфейса вспомогательными методами
Резюме
Глава 25. Введение в Windows Communication Foundation
API-интерфейсы распределенных вычислений
Роль DCOM
Роль служб СОМ+/Enterprise Services
Роль MSMQ
857
857
859
860
861
861
863
863
865
866
866
869
871
871
873
875
875
876
877
878
879
880
881
881
882
884
885
886
888
890
891
891
893
893
895
895
897
898
899
901
901
901
902
902
904
905
906
906
907
908
909
Содержание
Роль .NET Remo ting
Роль веб-служб XML
Именованные каналы, сокеты и P2P
Роль WCF
Обзор средств WCF
Обзор архитектуры, ориентированной на службы
WCF: итоги
Исследование основных сборок WCF
Шаблоны проектов WCF в Visual Studio
Шаблон проекта WCF Service
Базовая композиция приложения WCF
Понятие АВС в WCF
Понятие контрактов WCF
Понятие привязок WCF
Понятие адресов WCF
Построение службы WCF
Атрибут [ServiceContract]
Атрибут [OperationContract]
Служебные типы как контракты операций
Хостинг службы WCF
Установка АВС внутри файла Арр. сon fig
Кодирование с использованием типа ServiceHost
Указание базового адреса
Подробный анализ типа ServiceHost
Подробный анализ элемента <system. serviceModel>
Включение обмена метаданными
Построение клиентского приложения WCF
Генерация кода прокси с использованием svcutil.exe
Генерация кода прокси с использованием Visual Studio 2010
Конфигурирование привязки на основе TCP
Упрощение конфигурационных настроек в WCF 4.0
Конечные точки по умолчанию в WCF 4.0
Предоставление одной службы WCF с использованием множества привязок
Изменение установок для привязки WCF
Конфигурация поведения МЕХ по умолчанию в WCF 4.0
Обновление клиентского прокси и выбор привязки
Использование шаблона проекта WCF Service Library
Построение простой математической службы
Тестирование службы WCF с помощью WcfTestClient.exe
Изменение конфигурационных файлов с помощью SvcConfigEditor.exe
Хостинг службы WCF в виде службы Windows
Спецификация АВС в коде
Включение МЕХ
Создание программы установки для службы Windows
Установка службы Windows
Асинхронный вызов службы на стороне клиента
Проектирование контрактов данных WCF
Использование веб-ориентированного шаблона проекта WCF Service
Реализация контракта службы
Роль файла *.svc
Содержимое файла Web. сon fig
Тестирование службы
Резюме
23
909
910
913
913
914
914
915
916
917
918
919
920
920
921
924
925
926
927
928
928
929
930
930
932
933
934
936
937
938
939
940
941
942
943
944
945
946
947
947
948
949
950
951
952
953
954
955
956
958
959
959
960
960
24
Содержание
Глава 26. Введение в Windows Workflow Foundation 4.0
961
Определение бизнес-процесса
Роль WF 4.0
Построение простого рабочего потока
Просмотр полученного кода XAML
Исполняющая среда WF 4.0
Хостинг рабочего потока с использованием класса Workf lowlnvoker
Хостинг рабочего потока с использованием класса Workf lowApplication
Переделка первого рабочего потока
Знакомство с действиями Windows Workflow 4.0
Действия потока управления
Действия блок-схемы
Действия обмена сообщениями
Действия исполняющей среды и действия-примитивы
Действия транзакций
Действия над коллекциями и действия обработки ошибок
Построение рабочего потока в виде блок-схемы
Подключение действий к блок-схеме
Работа с действием InvokeMethod
Определение переменных уровня рабочего потока
Работа с действием FlowDeсision
Работа с действием TerminateWorkf low
Построение условия “true”
Работа с действием ForEach<T>
Завершение приложения
Промежуточные итоги
Изоляция рабочих потоков в выделенных библиотеках
Определение начального проекта
Импорт сборок и пространств имен
Определение аргументов рабочего потока
Определение переменных рабочего потока
Работа с действием Assign
Работа с действиями If и Switch
Построение специального действия кода
Использование библиотеки рабочего потока
Получение выходного аргумента рабочего потока
Резюме
962
962
963
965
967
967
970
971
971
971
972
973
973
974
974
975
975
976
977
978
978
979
979
981
982
984
984
985
986
986
987
987
988
991
992
993
Часть VI. Построение настольных пользовательских
П р и лож ен и й С ПОМОЩЬЮ WPF
995
Глава 27. Введение в Windows Presentation Foundation и XAML
Мотивация, лежащая в основе WPF
Унификация различных API-интерфейсов
Обеспечение разделения ответственности через XAML
Обеспечение оптимизированной модели визуализации
Упрощение программирования сложных пользовательских интерфейсов
Различные варианты приложений WPF
Традиционные настольные приложения
WPF-приложения на основе навигации
Приложения ХВАР
Отношения между WPF и Silverlight
Исследование сборок WPF
996
997
997
998
998
999
1000
1000
1001
1002
1003
1004
Содержание
25
Роль класса Application
Роль класса Window
Роль класса System.Windows.Controls.ContentControl
Роль класса System.Windows .Controls .Control
Роль класса System.Windows.FrameworkElement
Роль класса System.Windows .UIElement
Роль класса System.Windows.Media.Visual
Роль класса System.Windows.DependencyObject
Роль класса System.Windows.Threading.DispatcherObject
Построение приложения WPF без XAML
Создание строго типизированного окна
Создание простого пользовательского интерфейса
Взаимодействие с данными уровня приложения
Обработка закрытия объекта Window
Перехват событий мыши
Перехват клавиатурных событий
Построение приложения WPF с использованием только XAML
Определение Ма inWindow в XAML
Определение объекта Application в XAML
Обработка файлов XAML с помощью msbuild.exe
Трансформация разметки в сборку .NET
Отображение XAML-данных окна на код C#
Роль BAML
Отображение XAML-данных приложения на код C#
Итоговые замечания о процессе трансформирования XAML в сборку
Синтаксис XAML для WPF
Введение в Kaxaml
Пространства имен XAML XML и “ключевые слова” XAML
Управление объявлениями классов и переменных-членов
Элементы XAML, атрибуты XAML и преобразователи типов
Понятие синтаксиса XAML “свойство-элемент”
Понятие присоединяемых свойств XAML
Понятие расширений разметки XAML
Построение приложений WPF с использованием файлов отделенного кода
Добавление файла кода для класса MainWindow
Добавление файла кода для класса МуАрр
Обработка файлов кода с помощью msbuild.exe
Построение приложений WPF с использованием Visual Studio 2010
Шаблоны проектов WPF
Знакомство с инструментами визуального конструктора WPF
Проектирование графического интерфейса окна
Реализация события Loaded
Реализация события Click объекта Button
Реализация события C lo sed
Тестирование приложения
Резюме
1005
1007
1007
1008
1009
1010
1010
1010
1011
1011
1013
1013
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1027
1027
1029
1031
1032
1033
1034
1034
1036
1036
1037
1038
1038
1039
1039
1042
1044
1045
1046
1046
1047
Глава 28. Программирование с использованием элементов управления WPF
1048
1048
1049
1051
1051
1052
1052
Обзор библиотеки элементов управления WPF
Работа с элементами управления WPF в Visual Studio 2010
Элементы управления Ink API
Элементы управления документами WPF
Общие диалоговые окна WPF
Подробные сведения находятся в документации
26
Содержание
Управление компоновкой содержимого с использованием панелей
Позиционирование содержимого внутри панелей Canvas
Позиционирование содержимого внутри панелей WrapPanel
Позиционирование содержимого внутри панелей StackPanel
Позиционирование содержимого внутри панелей Grid
Позиционирование содержимого внутри панелей DockPanel
Включение прокрутки в типах панелей
Построение главного окна с использованием вложенных панелей
Построение системы меню
Построение панели инструментов
Построение строки состояния
Завершение дизайна пользовательского интерфейса
Реализация обработчиков событий MouseEnter/MouseLeave
Реализация логики проверки правописания
Понятие управляющих команд WPF
Внутренние объекты управляющих команд
Подключение команд к свойству Command
Подключение команд к произвольным действиям
Работа с командами Open и Save
Построение пользовательского интерфейса WPF с помощью Expression Blend
Ключевые аспекты IDE-среды Expression Blend
Использование элемента TabControl
Построение вкладки Ink API
Проектирование элемента Tool Bar
Элемент управления RadioButton
Элемент управления InkCanvas
Элемент управления ComboBox
Сохранение, загрузка и очистка данных InkCanvas
Введение в интерфейс Documents API
Блочные элементы и встроенные элементы
Диспетчеры компоновки документа
Построение вкладки Documents
Наполнение FlowDocument с использованием Blend
Наполнение FlowDocument с помощью кода
Включение аннотаций и “клейких” заметок
Сохранение и загрузка потокового документа
Введение в модель привязки данных WPF
Построение вкладки Data Binding
Установка привязки данных с использованием Blend
Свойство DataContext
Преобразование данных с использованием IValueConverter
Установка привязок данных в коде
Построение вкладки DataGrid
Резюме
1062
1063
1064
1065
1065
1066
1066
1067
1067
1068
1069
1071
1072
1073
1077
1079
1080
1082
1084
1086
1087
1088
1088
1089
1089
1091
1091
1093
1094
1095
1096
1096
1097
1099
1099
1100
1102
Глава 29. Службы визуализации графики WPF
1103
Службы графической визуализации WPF
Опции графической визуализации WPF
Визуализация графических данных с использованием фигур
Добавление прямоугольников, эллипсов и линий на поверхность Canvas
Удаление прямоугольников, эллипсов и линий с поверхности Canvas
Работа с элементами Polyline и Polygon
Работа с элементом Path
Кисти и перья WPF
1103
1104
1105
1107
1110
1110
1111
1115
1053
1054
1056
1058
1059
1060
1061
Содержание
Конфигурирование кистей с использованием Visual Studio 2010
Конфигурирование кистей в коде
Конфигурирование перьев
Применение графических трансформаций
Первый взгляд на трансформации
Трансформация данных С a nva s
Работа с фигурами в Expression Blend
Выбор фигуры для визуализации из палитры инструментов
Преобразование фигур в пути
Комбинирование фигур
Редакторы кистей и трансформаций
Визуализация графических данных с использованием рисунков и геометрий
Построение кисти DrawingBrush с использованием объектов Geometry
Рисование с помощью DrawingBrush
Включение типов Drawing в Drawinglmage
Генерация сложной векторной графики с использованием Expression Design
Экспорт документа Expression Design в XAML
Визуализация графических данных с использованием визуального уровня
Базовый класс Visual и производные дочерние классы
Первый взгляд на класс DrawingVisual
Визуализация графических данных в специальном диспетчере компоновки
Реагирование на операции проверки попадания
Резюме
1115
1117
1118
1118
1119
1120
1122
1122
1123
1123
1123
1125
1126
1127
1128
1128
1129
1130
1130
1131
1133
1134
1136
Глава 30. Ресурсы, анимация и стили WPF
Система ресурсов WPF
Работа с двоичными ресурсами
Программная загрузка изображения
Работа с объектными (логическими) ресурсами
Роль свойства Resources
Определение ресурсов уровня окна
Расширение разметки {StaticResource}
Изменение ресурса после извлечения
Расширение разметки {DynamicResource}
Ресурсы уровня приложения
Определение объединенных словарей ресурсов
Определение сборки из одних ресурсов
Извлечение ресурсов в Expression Blend
Службы анимации WPF
Роль классов анимации
Свойства То, From и By
Роль базового класса Timeline
Написание анимации в коде C#
Управление темпом анимации
Запуск в обратном порядке и циклическое выполнение анимации
Описание анимации в XAML
Роль раскадровки
Роль триггеров событий
Анимация с использованием дискретных ключевых кадров
Роль стилей WPF
Определение и применение стиля
Переопределение настроек стиля
Автоматическое применение стиля с помощью TargetType
Создание подклассов существующих стилей
27
4
1137
1137
1138
1139
1142
1143
1143
1145
1145
1146
1146
1147
1149
1150
1152
1152
1153
1154
1154
1155
1156
1157
1158
1158
1159
1160
1160
1161
1161
1162
28
Содержание
Роль безымянных стилей
Определение стилей с триггерами
Определение стилей с множеством триггеров
Анимированные стили
Программное применение стилей
Генерация стилей с помощью Expression Blend
Работа с визуальными стилями по умолчанию
Резюме
Глава 31. Шаблоны элементов управления WPF и пользовательские
элементы управления
1163
1164
1164
1165
1165
1166
1167
1169
Роль свойств зависимости
Проверка существующего свойства зависимости
Важные замечания относительно оболочек свойств CLR
Построение специального свойства зависимости
Добавление процедуры проверки достоверности данных
Реакция на изменение свойства
Маршрутизируемые события
Роль маршрутизируемых пузырьковых событий
Продолжение или прекращение пузырькового распространения
Роль маршрутизируемых туннелируемых событий
Логические деревья, визуальные деревья и шаблоны по умолчанию
Программный просмотр логического дерева
Программный просмотр визуального дерева
Программный просмотр шаблона по умолчанию для элемента управления
Построение специального шаблона элемента управления в Visual Studio 2010
Шаблоны как ресурсы
Включение визуальных подсказок с использованием триггеров
Роль расширения разметки {TemplateBinding}
Роль класса ContentPresenter
Включение шаблонов в стили
Построение специальных элементов UserControl с помощью Expression Blend
Создание проекта библиотеки UserControl
Создание WPF-приложения JackpotDeluxe
Извлечение UserControl из геометрических объектов
Роль визуальных состояний .NET 4.0
Завершение приложения JackpotDeluxe
Резюме
1170
1170
1172
1175
1176
1179
1179
1181
1182
1182
1183
1185
1185
1187
1188
1191
1192
1193
1194
1196
1196
1197
1198
1204
1204
1205
1209
1212
Часть VII. Построение веб-приложений с использованием ASP.NET
1213
Глава 32. Построение веб-страниц ASP.NET
1214
1214
1215
Роль протокола HTTP
Цикл запрос/ответ HTTP
HTTP — протокол без поддержки состояния
Веб-приложения и веб-серверы
Роль виртуальных каталогов IIS
Веб-сервер разработки ASP.NET
Роль языка HTML
Структура HTML-документа
Роль формы HTML
Инструменты визуального конструктора HTML в Visual Studio 2010
Построение формы HTML
Роль сценариев клиентской стороны
Пример сценария клиентской стороны
1215
1216
1216
1217
1217
1218
1219
1219
1220
1221
1223
Содержание
29
Обратная отправка веб-серверу
Обратные отправки в ASP.NET
Набор средств API-интерфейса ASP.NET
Основные средства ASP.NET 1.0-1.1
Основные средства ASP.NET 2.0
Основные средства ASP.NET 3.5 (и .NET 3.5 SP1)
Основные средства ASP.NET 4.0
Построение однофайловой веб-страницы ASP.NET
Ссылка на сборку AutoLotDAL.dll
Проектирование пользовательского интерфейса
Добавление логики доступа к данным
Роль директив ASP.NET
Анализ блока script
Анализ объявлений элементов управления ASP.NET
Цикл компиляции для однофайловых страниц
Построение веб-страницы ASP.NET с использованием файлов кода
^Ссылка на сборку AutoLotDAL.dll
Обновление файла кода
Цикл компиляции многофайловых страниц
Отладка и трассировка страниц ASP. NET
Веб-сайты и веб-приложения ASP.NET
Структура каталогов веб-сайта ASP.NET
Ссылаемые сборки
Роль папки App_Code
Цепочка наследования типа Раде
Взаимодействие с входящим запросом HTTP
Получение статистики браузера
Доступ к входным данным формы
Свойство IsPostBack
Взаимодействие с исходящим ответом HTTP
Выдача HTML-содержимого
Перенаправление пользователей
Жизненный цикл веб-страницы ASP.NET
Роль атрибута AutoEventWiгеир
Событие Error
Роль файла Web. сon fig
Утилита администрирования веб-сайтов ASP.NET
Резюме
1224
1225
1225
1225
1227
1228
1228
1229
1229
1230
1231
1233
1235
1235
1236
1237
1239
1240
1240
1241
1242
1243
1244
1244
1245
1246
1247
1248
1248
1249
1250
1250
1251
1252
1253
1254
1255
1256
Глава 33. Веб-элементы управления, мастер-страницы и темы ASP.NET
1257
1257
1258
1259
1260
1260
1262
1263
1264
1265
1266
1267
1268
1268
1274
Природа веб-элементов управления
Обработка событий серверной стороны
Свойство AutoPostBack
Базовые классы Control и WebControl
Перечисление содержащихся элементов управления
Динамическое добавление и удаление элементов управления
Взаимодействие с динамически созданными элементами управления
Функциональность базового класса WebControl
Основные категории веб-элементов управления ASP.NET
Краткая информация о System.Web.UI.HtmlControls
Документация по веб-элементам управления
Построение веб-сайта ASP.NET Cars
e
Работа с мастер-страницами
Определение страницы содержимого Default.aspx
30
Содержание
Проектирование страницы содержимого Inventory.aspx
Проектирование страницы содержимого BuildCar.aspx
Роль элементов управления проверкой достоверности
Класс RequiredFieldValidator
Класс RegularExpressionValidator
Класс RangeValidator
Класс CompareValidator
Создание итоговой панели проверки достоверности
Определение групп проверки достоверности
Работа с темами
Ф а й л ы * . skin
Применение тем ко всему сайту
Применение тем на уровне страницы
Свойство SkinID
Программное назначение тем
Резюме
1276
1279
1282
1283
1284
1284
1284
1285
1286
1288
1289
Глава 34. Управление состоянием в ASP.NET
1294
1294
1296
1296
1297
1299
1299
1301
1302
1303
1303
1305
1306
1307
1307
1309
1311
1314
1315
1315
1316
1317
1317
1318
1319
1319
1320
1322
1323
1325
Проблема поддержки состояния
Приемы управления состоянием ASP. NET
Роль состояния представления ASP NET
Демонстрация работы с состоянием представления
Добавление специальных данных в состояние представления
Роль файла Global.asax
Глобальный обработчик исключений “последнего шанса”
Базовый класс HttpApplication
Различие между свойствами Application и Session
Поддержка данных состояния уровня приложения
Модификация данных приложения
Обработка останова веб-приложения
Работа с кэшем приложения
Работа с кэшированием данных
Модификация файла *. aspx
Поддержка данных сеанса
Дополнительные члены HttpSessionState
Cookie-наборы
Создание cookie-наборов
Чтение входящих cookie-данных
Роль элем ента <sessionState>
Хранение данных сеанса на сервере состояния сеансов ASP.NET
Хранение информации о сеансах в выделенной базе данных
Интерфейс ASP. NET Profile API
База данных ASPNETDB.mdf
Определение пользовательского профиля в Web. con fig
Программный доступ к данным профиля
Группирование данных профиля и сохранение специальных объектов
Резюме
1291
1291
1291
1292
1293
Часть VIII. Приложения
1327
Приложение А. Программирование с помощью Windows Forms
1328
Приложение Б. Независимая от платформы разработка .NET-приложений
с помощью Mono
1369
Предметный указатель
1386
Об авторе
Эндрю Троелсен (Andrew Ttolesen) с любовью вспоминает свой самый первый ком­
пьютер Atari 400, оснащенный кассетным устройством хранения и черно-белым телеви­
зионным монитором “(который его родители разрешили ему поставить у себя в спальне,
за что им большое спасибо). Еще он благодарен ушедшему в небытие журналу Compute!,
степени бакалавра в области математической лингвистики и трем годам формально­
го изучения санскрита. Все это оказало значительное влияние на его сегодняшнюю
карьеру.
В настоящее время Эндрю работает в центре Intertech, занимающемся консультиро­
ванием и обучением работе с .NET и Java (www.intertech.com).
На его счету уже несколько написанных книг, в числе которых Developer s Workshop to
COM and ATL 3.0 (Wordware Publishing*, 2000 n), COM and .NETInteroperability (Apress, 2002 r.)
и Visual Basic 2008 and the .NET 3.5 Platform: An Advanced Guide (Apress, 2008 r.).
0 техническом редакторе
Энди Олсен (Andy Olsen) — независимый консультант и инструктор, проживающий
в Великобритании. Энди работает с .NET, начиная с самой первой бета-версии этого
продукта, и занимается активным исследованием новых функциональных возможно­
стей, которые появились в .NET 4.0. Он живет у моря в городе Суонси вместе со своей
женой Джейн и детьми Эмили и Томом. Любит делать пробежки вдоль побережья (peiyлярно останавливаясь на чашечку кофе по пути), кататься на лыжах и следить за лебе­
дями и орликами. Связаться с ним можно по адресу andyo@olsensof t .com.
Благодарности
По какой-то непонятной причине (а может и целому ряду причин) настоящее изда­
ние книги оказалось гораздо более трудным в написании, чем ожидалось. Если бы не
помощь и поддержка многих хороших людей, скорее всего, оно не вышло бы в свет так
скоро.
Прежде всего, огромное спасибо техническому редактору Энди Олсену. Помимо ука­
зания на пропущенные точки с запятой он привнес массу замечательных предложе­
ний, позволивших сделать первоначальные примеры кода более понятными и точными.
Спасибо тебе, Энди!
Далее хочу выразить благодарность всей команде литературных редакторов, а имен­
но — Мэри Бер (Магу Behr), Патрику Мидеру (Patrick Meader), Кэти Стэне (Katie Stence)
и Шэрон Тердеман (Sharon Tferdeman), которые выявили и устранили массу граммати­
ческих ошибок.
Особая благодарность Дебре Кэлли (Debra Kelly) из Apress. Это наш первый совмест­
ный проект и, несмотря на многочисленные опоздания с предоставлением глав, путани­
цу с электронными письмами и постоянное добавление обновлений, я все равно очень
надеюсь, что она согласится работать со мной снова. Спасибо тебе, Дебра!
И, наконец, напоследок спасибо моей жене Мэнди. Как всегда, ты поддерживаешь
меня в здравом уме во время написания всех моих проектов.
Введение
ервое издание настоящей книги появилось вместе с выпуском Microsoft второй
бета-версии .NET 1.0 (летом 2001 г.) и с тех пор постоянно переиздавалось в том
или ином виде. С того самого времени автор с чрезвычайным удовольствием и благо
дарностью наблюдал за тем, как данная работа продолжала пользоваться популярно­
стью в прессе и, самое главное, среди читателей. Через некоторое время, в 2002 г., она
даже была номинирована на премию Jolt Award (которую, увы, получить так и не уда­
лось, но книга попала в число финалистов), а в 2003 г. — еще и на премию Referenceware
Excellence Award, где стала лучшей книгой года по программированию.
Даже еще более важно то, что автору стали приходить электронные письма от чи­
тателей со всего мира. Общаться с множеством людей и узнавать, что данная книга
как-то помогла им в карьере, просто замечательно. В связи с этим, хотелось бы отме­
тить, что настоящая книга с каждым разом становится все лучше именно благодаря
читателям, которые присылают различные предложения по улучшению, указывают на
допущенные в тексте опечатки и обращают внимание на прочие промахи.
Автор был просто ошеломлен, узнав, что эта книга использовалась и продолжает ис­
пользоваться на занятиях в колледжах и университетах и является обязательной для
прочтения на многих предвыпускных и выпускных курсах в области вычислительной
техники.
Автор благодарит прессу, читателей, преподавателей и всех остальных и желает им
успешного программирования!
С самого первого выпуска настоящей книги автор прилагал все усилия и обновлял
книгу так, чтобы она отражала текущие возможности каждой выходившей версии плат­
формы .NET. В настоящем издании материал был полностью пересмотрен и расширен с
целью охвата новых средств языка C# 2010 и платформы .NET 4.0. Вы найдете инфор­
мацию по таким новым компонентам, как Dynamic Language Runtime (DLR), Tksk Parallel
Library (TPL), Parallel LINQ (PLINQ) и ADO.NET Entity Framework (EF). Кроме того, в книге
описан ряд менее значительных (но очень полезных) обновлений, наподобие именован­
ных и необязательных аргументов в C# 2010, типа класса Lazy<T> и т.д.
Помимо описания новых компонентов и возможностей, в книге по-прежнему пре­
доставляется весь необходимый базовый материал по языку C# в целом, основам объ­
ектно-ориентированного программирования (ООП), конфигурированию сборок, получе­
нию доступа к базам данных (через ADO.NET), а также процессу построения настольных
приложений с графическим пользовательским интерфейсом, веб-приложений и распре­
деленных систем (и многим другим темам).
Как и в предыдущих изданиях, в этом издании весь материал по языку программи­
рования C# и библиотекам базовых классов .NET подается в дружественной и понятной
читателю манере. Автор никогда не понимал, зачем другие технические авторы стара­
ются писать свои книги так, чтобы те больше напоминали сложный научный труд, а
не легкое для восприятия пособие. В новом издании основное внимание уделяется пре­
доставлению информации, которой необходимо владеть для того, чтобы разрабатывать
программные решения прямо сегодня, а не глубокому изучению малоинтересных эзо­
терических деталей.
П
Введение
зз
Автор и читатели - одна команда
Авторам книг по технологиям приходится писать для очень требовательной группы
людей. Всем известно, что детали разработки программных решений с помощью любой
платформы (.NET, Java и СОМ) очень сложны и сильно зависят от отдела, компании,
клиентской базы и поставленной задачи. Кто-то работает в сфере электронных публи­
каций, кто-то занимается разработкой систем для правительства и региональных орга­
нов власти, а кто-то сотрудничает с НАСА или военными департаментами. Что касается
автора настоящей книги, то сам он занимается разработкой детского образовательного
ПО (возможно, вам приходилось слышать об Oregon Trail или Amazon Trail), а также раз­
личных многоуровневых систем и проектов в медицинской и финансовой сфере. А это
значит, что код, который придется писать читателю, скорее всего, будет иметь мало
чего общего с тем, с которым приходится иметь дело автору.
Поэтому в настоящей книге автор специально старался избегать приведения приме­
ров, свойственных только какой-то конкретной области производства или программи­
рования. Из-за этого все концепции, связанные с С#, ООП, CLR и библиотеками базовых
классов .NET, объясняются на общих примерах. В частности, здесь везде применяется
одна и та же тема, которая так или иначе близка каждому — автомобили. Остальное
остается за читателем.
Задача автора состоит в том, чтобы максимально доступно объяснить читателю
язык программирования C# и основные концепции платформы .NET настолько хорошо,
а также описать все возможные инструменты и стратегии, которые могут потребовать­
ся для продолжения обучения по прочтении данной книги.
Задача читателя состоит в том, чтобы усвоить всю эту информацию и научить­
ся применять ее на практике при разработке своих программных решений. Конечно,
скорее всего, проекты, которые понадобится выполнять в будущем, не будут связаны
с автомобилями и их дружественными именами, но именно в этом и состоит вся суть
применения получаемых знаний на практике! После изучения представленных в этой
книге концепций можно будет спокойно создавать решения .NET, удовлетворяющие тре­
бованиям любой среды программирования.
Краткий обзор содержания
Эта книга логически разделена на восемь частей, в каждой из которых содержит­
ся ряд взаимосвязанных между собой глав. Те, кто читал предыдущие издания данной
книги, сразу же отметят ряд отличий. Например, новые средства языка C# больше не
описываются в отдельной главе. Вместо этого они рассматриваются в тех главах, в ко­
торых их появление является вполне естественным. Кроме того, по просьбе читателей
был значительно расширен материал по технологии Windows Presentation Foundation
(WPF). Ниже приведено краткое описание содержимого каждой из частей и глав настоя­
щей книги.
Часть I. Общие сведения о языке C# и платформе .NET
Назначением первой части этой книги является общее ознакомление читателя с
природой платформы .NET и различными средствами разработки, которые могут при­
меняться при построении приложений .NET (многие из которых распространяются с
открытым исходным кодом), а также некоторыми основными концепциями языка про­
граммирования C# и системы типов .NET.
34
Введение
Глава 1. Ф илософ ия . N ET
Первая глава выступает в роли своего рода основы для изучения всего остального
излагаемого в данной книге материала. В начале в ней рассказывается о традиционной
разработке приложений Windows и недостатках, которые существовали в этой сфере ра­
нее. Главной целью данной главы является ознакомление читателя с набором ключевых
составляющих .NET: общеязыковой исполняющей средой (Common Language Runtime —
CLR), общей системой типов (Common Type System — CTS), общеязыковой специфика­
цией (Common Language Specification — CLRS) и библиотеками базовых классов. Здесь
читатель сможет получить первоначальное впечатление о том, что собой представляет
язык программирования С#, и том, как выглядит формат сборок .NET, а также узнать о
независимой от платформы природе .NET (о которой более детально рассказывается в
приложении Б).
Глава 2. Создание приложений на языке С #
Целью этой главы является ознакомление читателя с процессом компиляции фай­
лов исходного кода на C# с применением различных средств и методик. Будет показа­
но, как использовать компилятор командной строки C# (c s c .e x e ) и файлы ответов, а
также различные редакторы кода и интегрированные среды разработки, в том числе
Notepad++, SharpDevelop, Visual C# 2010 Express и Visual Studio 2010. Кроме того, вы
узнаете о том, как устанавливать на машину разработки локальную копию документа­
ции .NET Framework 4.0 SDK.
Часть II. Главные конструкции программирования на C#
Темы, представленные в этой части книги, довольно важны, поскольку подходят для
разработки приложений .NET любого типа (т.е. веб-приложений, настольных приложе­
ний с графическим пользовательским интерфейсом, библиотек кода и служб Windows).
Здесь читатель ознакомится с основными конструкциями языка C# и некоторыми де­
талями объектно-ориентированного программирования (ООП), обработкой исключений
на этапе выполнения, а также автоматическим процессом сборки мусора в .NET.
Глава 3. Главные конструкции программирования на С#; часть I
В этой главе начинается формальное изучение языка программирования С#. Здесь
читатель узнает о роли метода Main () и многочисленных деталях работы с внутрен­
ними типами данных в .NET, в том числе — о манипуляциях текстовыми данными с
помощью System. String и System. Text.StringBuilder. Кроме того, будут описаны
итерационные конструкции и конструкции принятия решений, операции сужения и
расширения, а также ключевое слово unchecked.
Глава 4. Главные конструкции программирования на С#; часть II
В этой главе завершается рассмотрение ключевых аспектов С#. Будет показано, как
создавать перегруженные методы в типах и определять параметры с использованием
ключевых слов out, ref и par am s. Также рассматриваются появившиеся в C# 2010
концепции именованных аргументов и необязательных параметры. Кроме того, будут
описаны создание и манипулирование массивами данных, определение нулевых типов
(с помощью операций ? и ??) и отличия типов значения (включающих перечисления и
специальные структуры) от ссылочных типов.
Глава 5. Определение инкапсулированных типов классов
В этой главе начинается рассмотрение концепций объектно-ориентированного про­
граммирования (ООП) в языке С#. Вначале объясняются базовые понятия ООП (та­
Введение
35
кие как инкапсуляция, наследование и полиморфизм). Затем показано, как создавать
надежные типы классов с применением конструкторов, свойств, статических членов,
констант и доступных только для чтения полей. Наконец, рассматриваются частичные
определения типов, синтаксис инициализации объектов и автоматические свойства.
Глава 6. Понятия наследования и полиморфизма
Здесь читатель сможет ознакомиться с двумя такими основополагающими концеп­
циями ООП, как наследование и полиморфизм, которые позволяют создавать семейст­
ва взаимосвязанных типов классов. Будет описана роль виртуальных и абстрактных
методов (а также абстрактных базовых классов) и полиморфных интерфейсов. И, на­
конец, в главе рассматривается роль одного из главных базовых классов в .NET —
System.Object.
Глава 7. Структурированная обработка исклю чений
В этой главе рассматривается решение проблемы аномалий, возникающих в коде во
время выполнения, за счет применения методики структурированной обработки исклю­
чений. Здесь описаны ключевые слова, предусмотренные для этого в C# (try, catch,
throw и finally), а также отличия между исключениями уровня приложения и уровня
системы. Кроме того, рассматриваются различные инструменты, предлагаемые в Visual
Studio 2010, которые предназначены для проведения отладки исключений.
Глава 8. Врем я жизни объектов
В этой главе рассказывается об управлении памятью CLR-средой с использованием
сборщика мусора .NET. Будет описана роль корневых элементов приложений, поколе­
ний объектов и типа System.GC. Будет показано, как создавать самоочшцаемые объ­
екты (с применением интерфейса IDisposable) и обеспечивать процесс финализации
(с помощью метода System. Object.Finalize ()). Вы узнаете о появившемся в .NET 4.0
классе Lazy<T>, который позволяет определять данные так, чтобы они не размещались
в памяти до тех пор, пока вызывающая сторона не запросит их. Этот класс позволяет
не загромождать память такими объектами, которые пока не требуются.
Часть III. Дополнительные конструкции программирования на C#
В этой части читателю предоставляется возможность углубить знания языка C# за
счет изучения других более сложных (но очень важных) концепций. Здесь завершается
ознакомление с системой типов .NET описанием типов делегатов и интерфейсов. Кроме
того, описана роль обобщений и дано краткое введение в язык LINQ (Language Integrated
Query). Также рассматриваются некоторые более сложные средства C# (такие как мето­
ды расширения, частичные методы и приемы манипулирования указателями).
Глава 9. Работа с интерфейсами
Материал этой главы предполагает наличие понимания концепций объектно-ори­
ентированной разработки и посвящен программированию с использованием интер­
фейсов. Здесь будет показано, как определять классы и структуры, поддерживающие
множество поведений, как обнаруживать эти поведения во время выполнения и как
выборочно скрывать какие-то из них за счет явной реализации интерфейсов. Помимо
создания специальных интерфейсов, рассматриваются вопросы реализации стандарт­
ных интерфейсов из состава .NET и их применения для построения объектов, которые
могут сортироваться, копироваться, перечисляться и сравниваться.
36
Введение
Глава 10. Обобщения
Эта глава посвящена обобщениям. Программирование с использованием обобщений
позволяет создавать типы и члены типов, содержащие заполнители, которые заполняют­
ся вызывающим кодом. В целом, обобщения позволяют значительно улучшить произво­
дительность приложений и безопасность в отношении типов. В главе рассматриваются
типы обобщений из пространства имен System.Collections .Generic, а также показа­
но, как создавать собственные обобщенные методы и типы (с ограничениями и без).
Глава 11. Делегаты, события и лямбда-выражения
Благодаря этой главе, станет понятно, что собой представляет тип делегата. Любой
делегат в .NET представляет собой объект, который указывает на другие методы в при­
ложении. С помощью делегатов можно создавать системы, позволяющие многочислен­
ным объектам взаимодействовать между собой в обоих направлениях. После изучения
способов применения делегатов в .NET, будет показано, как применять в C# ключевое
слово event, которое упрощает манипулирование делегатами. Кроме того, рассматри­
вается роль лямбда-операции (=>) и связь между делегатами, анонимными методами и
лямбда-выражениями.
Глава 12. Расш иренные средства языка C#
В этой главе описаны расширенные средства языка С#, в том числе перегрузка опе­
раций, создание специальных процедур преобразования (явного и неявного) для типов,
построение и взаимодействие с индексаторами типов, работа с расширяющими мето­
дами, анонимными типами, частичными методами, а также указателями C# с исполь­
зованием в коде контекста unsafe.
Глава 13. LINO to O bject
В этой главе начинается рассмотрение LINQ (Language Integrated Query — язык ин­
тегрированных запросов). Эта технология позволяет создавать строго типизированные
выражения запросов, применять их к ряду различных целевых объектов LINQ и тем са­
мым манипулировать данными в самом широком смысле этого слова. Глава посвящена
API-интерфейсу LINQ to Objects, который позволяет применять LINQ-выражения к кон­
тейнерам данных (т.е. массивам, коллекциям и специальным типам). Эта информация
будет полезна позже при рассмотрении других дополнительных API-интерфейсов, таких
как LINQ to XML, LINQ to DataSet, PLINQ и LINQ to Entities.
Часть IV. Программирование с использованием сборок .NET
Эта часть книги посвящена деталям формата сборок .NET. Здесь вы узнаете не только
о способах развертывания и конфигурирования библиотек кода .NET, но также о внут­
реннем устройстве двоичного образа .NET. Будет описана роль атрибутов .NET и опре­
деления информации о типе во время выполнения. Кроме того, рассматривается роль
среды DLR (Dynamic Language Runtime — исполняющая среда динамического языка) в
.NET 4.0 и ключевого слова dynamic в C# 2010. Наконец, объясняется, что собой пред­
ставляют контексты объектов, как устроен CIL-код и как создавать сборки в памяти.
Глава 14. Конфигурирование сборок .N E T
На самом высоком уровне термин сборка применяется для описания любого двоич­
ного файла * . d l l или * . ехе, который создается с помощью компилятора .NET. В дейст­
вительности возможности сборок намного шире. В этой главе будет показано, чем отли­
чаются однофайловые и многофайловые сборки, как создавать и развертывать сборки
обеих разновидностей, как делать сборки приватными и разделяемыми с использовали-
Введение
37
ем XML-файлов * . conf ig и специальных сборок политик издателя. Кроме того, в главе
описан глобальный кэш сборок (Global Assembly Cache — CAG), а также изменения CAG
в версии .NET 4.0.
Глава 15. Реф лексия типов, позднее связывание
и программирование с использованием атрибутов
В главе 15 продолжается изучение сборок .NET. В ней показано, как обнаруживать
типы во время выполнения с использованием пространства имен System. Ref lection.
Типы из этого пространства имен позволяют создавать приложения, способные считы­
вать метаданные сборки на лету. Кроме того, в главе рассматривается динамическая за­
грузка и создание типов во время выполнения с помощью позднего связывания, а также
роль атрибутов .NET (стандартных и специальных). Для закрепления материала в конце
главы приводится пример построения расширяемого приложения Windows Forms.
Глава 16. Процессы , домены приложений и контексты объектов
В этой главе рассказывается о создании загруженных исполняемых файлов .NET.
Целью главы является иллюстрация отношений между процессами, доменами прило­
жений и контекстными границами. Все эти темы подготавливают базу для изучения
процесса создания многопоточных приложений в главе 19.
Глава 17. Язык C IL и роль динам ических сборок
В этой главе подробно рассматривается синтаксис и семантика языка CIL, а также
роль пространства имен System. Re flection. Em it, с помощью типов из которого мож­
но создавать программное обеспечение, способное генерировать сборки .NET в памяти
во время выполнения. Формально сборки, которые определяются и выполняются в па­
мяти, называются динамическими сборками. Их не следует путать с динамическими
типами, которые являются темой главы 18.
Глава 18. Динам ические типы и исполняющая ср ед а динам ического языка
В .NET 4.0 появился новый компонент исполняющей среды .NET, который называ­
ется исполняющей средой динамического языка (Dynamic Language Runtime — DLR).
С помощью DLR в .NET и ключевого слова dynamic в C# 2010 можно определять дан­
ные, которые в действительности разрешаются во время выполнения. Использование
этих средств значительно упрощает решение ряда очень сложных задач по програм­
мированию приложений .NET. В главе рассматриваются некоторые практические спо­
собы применения динамических данных, включая более гладкое использование A PIинтерфейсы рефлексии .NET, а также упрощенное взаимодействие с унаследованными
библиотеками СОМ.
Часть V. Введение в библиотеки базовых классов .NET
В этой части рассматривается ряд наиболее часто применяемых служб, постав­
ляемых в составе библиотек базовых классов .NET, включая создание многопоточных
приложений, файловый ввод-вывод и доступ к базам данных с помощью ADO.NET.
Здесь также показано, как создавать распределенные приложения с помощью Windows
Communication Foundation (WCF) и приложения с рабочими потоками, которые исполь­
зуют API-интерфейсы Windows Workflow Foundation (WF) и LINQ to XML.
Глава 19. Многопоточность и параллельное программирование
Эта глава посвящена созданию многопоточных приложений. В ней демонстрируется
ряд приемов, которые можно применять для написания кода, безопасного в отноше­
38
Введение
нии потоков. В начале главы кратко напоминается о том, что собой представляет тип
делегата в .NET для упрощения понимания предусмотренной в нем внутренней под­
держки для асинхронного вызова методов. Затем рассматриваются типы пространства
имен System.Threading и новый API-интерфейс TPL flhsk Parallel Library — библиотека
параллельных задач), появившийся в .NET 4.0. С применением этого API-интерфейса
можно создавать .NET-приложения, распределяющие рабочую нагрузку среди доступ­
ных ЦП в исключительно простой манере. В главе также описан API-интерфейс PINQ
(Parallel LINQ), который позволяет создавать масштабируемые LINQ-запросы.
Глава 20. Файловый ввод-вы вод и сериализация объектов
Пространство имен System. 10 позволяет взаимодействовать существующей струк­
турой файлов и каталогов. В главе будет показано, как программно создавать (и удалять)
систему каталогов и перемещать данные в различные потоки (файловые, строковые и
находящиеся в памяти). Кроме того, рассматриваются службы .NET, предназначенные
для сериализации объектов. Сериализация представляет собой процесс, который позво­
ляет сохранять данные о состоянии объекта (или набора взаимосвязанных объектов) в
потоке для использования в более позднее время, а десериализация— процесс извлече­
ния данных о состоянии объекта из потока в память для последующего использования
в приложении. В главе описана настройка процесса сериализации с применением ин­
терфейса ISerializable и набора атрибутов .NET.
Глава 21. A D 0.N ET, часть I: подключенный уровень
В этой первой из трех посвященных базам данных главам дано введение в APIинтерфейс ADO.NET. Рассматривается роль поставщиков данных .NET и взаимодейст­
вие с реляционной базой данных с применением так называемого подключенного уровня
ADO.NET, который представлен объектами подключения, объектами команд, объектами
транзакций и объектами чтения данных. В этой главе также приведен пример создания
специальной базы данных и первой версии специальной библиотеки доступа к данным
(AutoLotDAL.dll), неоднократно применяемой в остальных примерах книги.
Глава 23. ADO.NET, часть II: автономный уровень
В этой главе продолжается описание способов работы с базами данных и рассказы­
вается об автономном уровне ADO.NET. Рассматривается роль типа DataSet и объектов
адаптеров данных, а также многочисленных средств Visual Studio 2010, которые спо­
собны упростить процесс создания приложений, управляемых данными. Будет показа­
но, как связывать объекты DataTable с элементами пользовательского интерфейса, а
также как применять запросы LINQ к находящимся в памяти объектам DataSet с ис­
пользованием API-интерфейса LINQ to DataSet.
Глава 23. ADO.NET, часть III: Entity Framework
В этой главе завершается изучение ADO.NET и рассматривается роль технологии
Entity Framework (EF), которая позволяет создавать код доступа к данным с исполь­
зованием строго типизированных классов, напрямую отображающихся на бизнес-мо­
дель. Здесь будут описаны роли таких входящих в состав EF компонентов, как службы
объектов EF, клиент сущностей и контекст объектов. Будет показано устройство файла
* . edmx, а также взаимодействие с реляционными базами данных с применением APIинтерфейса LINQ to Entities. Кроме того, в главе создается последняя версия специаль­
ной библиотеки доступа к данным (AutoLotDAL.dll), которая будет использоваться в
нескольких последних главах книги.
Введение
39
Глава 24. Введение в LINQ to XML
В главе 14 были даны общие сведения о модели программирования LINQ и об
API-интерфейсе LINQ to Objects. В этой главе читателю предлагается углубить свои
знания о технологии LINQ и научиться применять запросы LINQ к XML-документам.
Сначала будут рассмотрены сложности, которые существовали в .NET первоначально
в области манипулирования XML-данными, на примере применения типов из сборки
System.Xml .dll. Затем будет показано, как создавать XML-документы в памяти, обес­
печивать их сохранение на жестком диске и перемещаться по их содержимому с ис­
пользованием модели программирования LINQ (LINQ to XML).
Глава 25. Введение в Windows Comm unication Foundation
В этой главе читатель узнает об API-интерфейсе Windows Communication Foundation
(WCF), который позволяет создавать распределенные приложения симметричным обра­
зом, какими бы не были лежащие в их основе низкоуровневые детали. Будет показано,
как создавать службы, хосты и клиентские приложения WCF. Службы WCF являются
чрезвычайно гибкими, поскольку позволяют использовать для клиентов и хостов кон­
фигурационные файлы на основе XML, в которых декларативно задаются необходимые
адреса, привязки и контракты. Кроме того, рассматриваются полезные сокращения,
которые появились в .NET 4.0.
Глава 26. Введение в Windows Workflow Foundation 4 .0
API-интерфейс Windows Workflow Foundation (WF) вызывает больше всего путаницы
у разработчиков-новичков. В версии .NET 4.0 первоначальный вариант API-интерфейса
WF (появившийся в .NET 3.0) полностью переделан. В этой главе описана роль прило­
жений, поддерживающих рабочие потоки, и способы моделирования бизнес-процессов
с применением API-интерфейса WF 4.0. Рассматривается библиотека действий, постав­
ляемая в составе WF 4.0, а также показано, как создавать специальные действия.
Часть VI. Построение настольных пользовательских
приложений с помощью WPF
В .NET 3.0 был предложен замечательный API-интерфейс под названием Windows
Presentation Foundation (WPF). Он быстро стал заменой модели программирования на­
стольных приложений Windows Forms. WPF позволяет создавать настольные приложе­
ния с векторной графикой, интерактивной анимацией и операциями привязки данных
с использованием декларативной грамматики разметки XAML. Более того, архитектура
элементов управления WPF позволяет легко изменять внешний вид и поведение лю бо­
го элемента управления с помощью правильно оформленного XAML-кода. В настоящем
издании модели программирования WPF посвящено целых пять глав.
Глава 27. Введение в Windows Presentation Foundation и XAM L
Технология WPF позволяет создавать чрезвычайно интерактивные и многофункцио­
нальные интерфейсы для настольных приложений (и косвенно для веб-приложений).
В отличие от Windows Forms, в WPF множество ключевых служб (наподобие двухмерной
и трехмерной графики, анимации, форматированных документов и т.п.) интегрируется
в одну универсальную объектную модель. В главе предлагается введение в WPF и язык
XAML (Extendable Application Markup Language — расширяемый язык разметки прило­
жений). Будет показано, как создавать WPF-приложения без использования только кода
без XAML, с использованием одного лишь XAML и с применением обоих подходов вме­
сте, а также приведен пример создания специального XAML-редактора, который приго­
диться при изучении остальных глав, посвященных WPF.
40
Введение
Глава 28. Программирование с использованием элементов управления WPF
В этой главе читатель научится работать с предлагаемыми в WPF элементами управ­
ления и диспетчерами компоновки. Будет показано, как создавать системы меню, раз­
делители окон, панели инструментов и строки состояния. Также в главе рассматрива­
ются API-интерфейсы (и связанные с ними элементы управления), входящие в состав
WPF — Documents API, Ink API и модель привязки данных. В главе приводится началь­
ное описание IDE-среды Expression Blend, которая значительно упрощает процесс соз­
дания многофункциональных пользовательских интерфейсов для приложений WPF.
Глава 29. Службы визуализации графики WPF
В API-интерфейсе WPF интенсивно используется графика, в связи с чем WPF предос­
тавляет три пути визуализации графических данных — фигуры, рисунки и визуальные
объекты. В главе подробно описаны все эти пути. Кроме того, рассматривается набор
важных графических примитивов (таких как кисти, перья и трансформации), а приме­
нение Expression Blend для создания графики. Также показано, как выполнять опера­
ции проверки попадания (hit-testing) в отношении графических данных.
Глава 30. Ресурсы , анимация и стили WPF
В этой главе освещены три важных (и связанных между собой) темы, которые позво­
лят углубить знания API-интерфейса Windows Presentation Foundation. В первую очередь
рассказывается о роли логических ресурсов. Система логических (также называемых
объектными) ресурсов позволяет назначать наиболее часто используемым в W PFприложении объектам имена и затем ссылаться на них. Кроме того, будет показано, как
определять, выполнять и управлять анимационной последовательностью. И, наконец,
в главе рассматривается роль стилей в WPF. Подобно тому, как для веб-страниц могут
применяться таблицы стилей CSS и механизм тем ASP.NET, в приложениях WPF для
набора элементов управления может быть определен общий вид и поведение.
Глава 31. Шаблоны элементов управления WPF
и пользовательские элементы управления
В этой главе завершается изучение модели программирования WPF и демонстри­
руется процесс создания специализированных элементов управления. Сначала рас­
сматриваются две важных темы, связанные с созданием любого специального элемен­
та — свойства зависимости и маршрутизируемые событиях. Затем описывается роль
шаблонов по умолчанию и способы их просмотра в коде во время выполнения. Наконец,
рассказывается о том, как создавать специальные классы UserControl с помощью
Visual Studio 2010 и Expression Blend, в том числе и с применением .NET 4.0 Visual
State Manager (VSM).
Часть VII. Построение веб-приложений с использованием ASP.NET
Эта часть посвящена деталям построения веб-приложений с применением APIинтерфейса ASP.NET. Данный интерфейс был разработан Microsoft специально для
предоставления возможности моделировать процесс создания настольных пользова­
тельских интерфейсов путем наложения стандартного объектно-ориентированной,
управляемой событиями платформы поверх стандартных запросов и ответов HTTP.
Глава 32. Построение веб-страниц A S P .N E T
В этой главе начинается изучение процесса разработки веб-приложений с помощью
ASP.NET. Как будет показано, вместо кода серверных сценариев теперь применяются
самые настоящие объектно-ориентированные языки (наподобие C# и VB.NET). В главе
Введение
41
рассматривается типовой процесс создания веб-страницы ASP.NET, лежащая в основе
модель программирования и другие важные аспекты ASP.NET, вроде того, как выбираеть веб-сервер и работать с файлами Web.conf ig.
Глава 33. Веб-элементы управления, мастер-страницы и темы A S P .N E T
В отличие от предыдущей главы, посвященной созданию объектов Раде из ASP.NET, в
данной главе рассказывается об элементах управления, которые заполняют внутреннее
дерево элементов ASP.NET. Здесь описаны основные веб-элементы управления ASP.NET,
включая элементы управления проверкой достоверности, элементы управления навига­
цией по сайту и различные операции привязки данных. Кроме того, рассматривается
роль мастер-страниц и механизма применения тем ASP.NET, который является сервер­
ным аналогом традиционных таблиц стилей.
Глава 34. Управление состоянием в A S P .N E T
В этой главе рассматриваются разнообразные способы управления состоянием в
.NET Как и в классическом ASP, в ASP.NET можно создавать cookie-наборы и перемен­
ные уровня приложения и уровня сеанса. Кроме того, в ASP.NET есть еще одно средство
для управления состоянием, которое называется кэшем приложения. Вдобавок в главе
рассказывается о роли базового класса HttpApplication и демонстрируется динами­
ческое переключение поведения веб-приложения с помощью файла Web.conf ig.
Часть VIII. Приложения
В этой заключительной части книги рассматриваются две темы, которые не совсем
вписывались в контекст основного материала. В частности, здесь кратко рассматрива­
ется более ранняя платформа Windows Forms для построения графических интерфейсов
настольных приложений, а также использование платформы Mono для создания прило­
жений .NET, функционирующих под управлением операционных систем, отличных от
Microsoft Windows.
Приложение А. Программирование с помощью Windows Form s
Исходный набор инструментов для построения настольных пользовательских интер­
фейсов, который поставляется в рамках платформы .NET с самого начала, называется
Windows Forms. В этом приложении описана роль этого каркаса и показано, как с его
помощью создавать главные окна, диалоговые окна и системы меню. Кроме того, здесь
рассматриваются вопросы наследования форм и визуализации двухмерной графики с
помощью пространства имен System. Drawing. В конце приложения приводится при­
мер создания программы для рисования (средней сложности), иллюстрирующий прак­
тическое применение всех описанных концепций.
Приложение Б. Независим ая от платформы разработка
.N E T -приложений с помощью Mono
Приложение Б посвящено использованию распространяемой с открытым исходным
кодом реализации платформы .NET под названием Mono. Она позволяет разрабатывать
многофункциональные приложения .NET, которые можно создавать, развертывать и
выполнять под управлением самых разных операционных систем, включая Mac OS X,
Solaris, AIX и многочисленные дистрибутивы Linux. Так как Mono в основном эмули­
рует платформу .NET от Microsoft, ее функциональные возможности вполне очевидны.
Поэтому в приложении основное внимание уделено не возможностям платформы Mono,
а процессу ее установки, предлагаемым в ее составе инструментам для разработки, а
также используемому механизму исполняющей среды.
42
Введение
Исходный код примеров
Исходный код всех рассматриваемых в настоящей книге примеров доступен для за­
грузки на сайте издательства по адресу:
http://www.williamspublishing.com
От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше
мнение и хотим знать, что было сделано нами правильно, что можно было сделать луч­
ше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые
другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумаж­
ное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои
замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нра­
вится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши
книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а
также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обяза­
тельно учтем его при отборе и подготовке к изданию последующих книг.
Наши координаты:
E-mail:
inf о@ williamspublishing. com
WWW:
http://www.williamspublishing.com
Информация для писем из:
России:
Украины:
127055, г. Москва, ул. Лесная, д. 43, стр. 1
03150, Киев, а/я 152
ЧАСТЬ I
Общие сведения
о языке C#
и платформе .NET
В этой ча сти ...
Глава 1. Философия .NET
Глава 2. Создание приложений на языке C#
ГЛАВА
1
Философия .NET
аждые несколько лет современному программисту требуется серьезно обновлять
свои знания, чтобы оставаться в курсе всех последних новейших технологий.
Языки (C++, Visual Basic 6.0, Java), библиотеки приложений (MFC, ATL, STL), архитекту
ры (COM, CORBA, EJB) и API-интерфейсы, которые провозглашались универсальными
средствами для разработки программного обеспечения, постепенно начинают затме­
вать более совершенные или, в самом крайнем случае, более новые технологии.
Какое бы чувство расстройства не возникало в связи с необходимостью обновления
своей внутренней базы знаний, избежать его, честно говоря, не возможно. Поэтому це­
лью данной книги является рассмотрение деталей тех решений, которые предлагаются
Microsoft в сфере разработки программного обеспечения сегодня, а именно — деталей
платформы .NET и языка программирования С#.
Задача настоящей главы заключается в изложении концептуальных базовых све­
дений для обеспечения возможности успешного освоения всего остального материала
книги. Здесь рассматриваются такие связанные с .NET концепции, как сборки, общий
промежуточный язык (Common Intermediate Language — CIL) и оперативная компиля­
ция (Just-In-Time Compilation — JIT). Помимо предварительного ознакомления с неко­
торыми ключевыми словами, также разъясняются взаимоотношения между различ­
ными компонентами платформы .NET, такими как общеязыковая исполняющая среда
(Common Language Runtime — CLR), общая система типов (Common Type System — CTS)
и общеязыковая спецификация (Common Language Specification — CLS).
Кроме того, в настоящей главе описан набор функциональных возможностей, по­
ставляемых в библиотеках базовых классов .NET 4.0, для обозначения которых иногда
используют аббревиатуру BCL (Base Class Libraries — библиотеки базовых классов) или
FCL (Framework Class Libraries — библиотеки классов платформы). И, наконец, напос­
ледок в главе кратко затрагивается тема независимой от языков и платформ сущности
.NET (да, это действительно так — .NET не ограничивается только операционной сис­
темой Windows). Как не трудно догадаться, многие из этих тем будут более подробно
освещены в остальной части книги.
К
Предыдущее состояние дел
Прежде чем переходить к изучению специфических деталей мира .NET, не поме­
шает узнать о некоторых вещах, которые послужили стимулом для создания Microsoft
текущей платформы. Чтобы настроиться надлежащим образом, давайте начнем эту
главу с краткого урока истории, чтобы вспомнить об истоках и ограничениях, которые
существовали в прошлом (ведь признание существования проблемы является первым
шагом на пути к ее решению). После завершения этого краткого экскурса в историю мы
обратим наше внимание на те многочисленные преимущества, которые предоставляет
язык C# и платформа .NET.
Глава 1. Философия .NET
45
Подход с применением языка С и API-интерфейса Windows
Традиционно разработка программного обеспечения для операционных систем се­
мейства Windows подразумевала использование языка программирования С в сочета­
нии с API-интерфейсом Windows (Application Programming Interface — интерфейс при­
кладного программирования). И хотя то, что за счет применения этого проверенного
временем подхода было успешно создано очень много приложений, мало кто станет
спорить по поводу того, что процесс создания приложений с помощью одного только
API-интерфейса является очень сложным занятием.
Первая очевидная проблема состоит в том, что С представляет собой очень лаконич­
ный язык. Разработчики программ на языке С вынуждены мириться с необходимостью
“вручную” управлять памятью, безобразной арифметикой указателей и ужасными син­
таксическими конструкциями. Более того, поскольку С является структурным языком
программирования, ему не хватает преимуществ, обеспечиваемых объектно-ориентиро­
ванным подходом (здесь можно вспомнить о спагетти-подобном коде). Из-за сочетания
тысяч глобальных функций и типов данных, определенных в API-интерфейса Windows,
с языком, который и без того выглядит устрашающе, совсем не удивительно, что сего­
дня в обиходе присутствует столь много дефектных приложений.
Подход с применением языка C++ и платформы MFC
Огромным шагом вперед по сравнению с подходом, предполагающим применение
языка С прямо с API-интерфейсом, стал переход на использование языка программиро­
вания C++. Язык C++ во многих отношениях может считаться объектно-ориентирован­
ной надстройкой поверх языка С. Из-за этого, хотя в случае его применения програм­
мисты уже могут начинать пользоваться преимуществами известных “главных столпов
ООП” (таких как инкапсуляция, наследование и полиморфизм), они все равно вынуж­
дены иметь дело с утомительными деталями языка С (вроде необходимости осуществ­
лять управление памятью “вручную”, безобразной арифметики указателей и ужасных
синтаксических конструкций).
Невзирая на сложность, сегодня существует множество платформ для программиро­
вания на C++. Например, MFC (Microsoft Foundation Classes — библиотека базовых клас­
сов Microsoft) предоставляет в распоряжение разработчику набор классов C++, которые
упрощают процесс создания приложений Windows. Основное предназначение MFC
заключается в представлении “разумного подмножества” исходного A PI-интерфейса
Windows в виде набора классов, “магических” макросов и многочисленных средств
для автоматической генерации программного кода (обычно называемых мастерами).
Несмотря на очевидную пользу данной платформы приложений (и многих других осно­
ванных на C++ наборов средств), процесс программирования на C++ остается трудным
и чреватым допущением ошибок занятием из-за его исторической связи с языком С.
Подход с применением Visual Basic 6.0
Благодаря искреннему желанию иметь возможность наслаждаться более простой
жизнью, многие программисты перешли из “мира платформ” на базе С (C++) в мир
менее сложных и более дружественных языков наподобие Visual Basic 6.0 (VB6). Язык
VB6 стал популярным благодаря предоставляемой им возможности создавать сложные
пользовательские интерфейсы, библиотеки программного кода (вроде COM-серверов) и
логику доступа к базам данных с приложением минимального количества усилий. Во
многом как и в MFC, в VB6 сложности API-интерфейса Windows скрываются из вида за
счет предоставления ряда интегрированных мастеров, внутренних типов данных, клас­
сов и специфических функций VB.
46
Часть I. Общие сведения о языке C# и платформе .NET
Главный недостаток языка VB6 (который с появлением платформы .NET был устра­
нен) состоит в том, что он является не полностью объектно-ориентированным, а ско­
рее — просто “объектным”. Например, VB6 не позволяет программисту устанавливать
между классами отношения “подчиненности” (т.е. прибегать к классическому наследо­
ванию) и не обладает никакой внутренней поддержкой для создания параметризован­
ных классов. Более того, VB6 не предоставляет возможности для построения многопо­
точных приложений, если только программист не готов опускаться до уровня вызовов
API-интерфейса Windows (что в лучшем случае является сложным, а в худшем — опас­
ным подходом).
На заметку! Язык Visual Basic, используемый внутри платформы .NET (и часто называемый языком
VB.NET), имеет мало чего общего с языком VB6. Например, в современном языке VB поддержи­
вается перегрузка операций, классическое наследование, конструкторы типов и обобщения.
Подход с применением Java
Теперь пришел черед языка Java. Язык Java представляет собой объектно-ориенти­
рованный язык программирования, который своими синтаксическими корнями уходит
в C++. Как многим известно, достоинства Java не ограничиваются одной лишь только
поддержкой независимости от платформ. Java как язык не имеет многих из тех непри­
ятных синтаксических аспектов, которые присутствуют в C++, а как платформа — пре­
доставляет в распоряжение программистам большее количество готовых пакетов с раз­
личными определениями типов внутри. За счет применения этих типов программисты
на Java могут создавать “на 100% чистые Java-приложения” с возможностью подклю­
чения к базе данных, поддержкой обмена сообщениями, веб-интерфейсами и богатым
настольными интерфейсами для пользователей (а также многими другими службами).
Хотя Java и представляет собой очень элегантный язык, одной из потенциальных
проблем является то, что применение Java обычно означает необходимость использо­
вания Java в цикле разработки и для взаимодействия клиента с сервером. Надежды на
появление возможности интегрировать Java с другими языками мало, поскольку это
противоречит главной цели Java — быть единственным языком программирования для
удовлетворения любой потребности. В действительности, однако, в мире существуют
миллионы строк программного кода, которым бы идеально подошло смешивание с бо­
лее новым программным кодом на Java. К сожалению, Java делает выполнение этой
задачи проблематичной. Пока в Java предлагаются лишь ограниченные возможности
для получения доступа к отличным от Java API-интерфейсам, поддержка для истинной
межплатформенной интеграции остается незначительной.
Подход с применением СОМ
Модель COM (Component Object Model — модель компонентных объектов) была пред­
шествующей платформой для разработки приложений, которая предлагалась Microsoft,
и впервые появилась в мире программирования приблизительно в 1991 г. (или в 1993 г.,
если считать моментом ее появления рождение версии OLE 1.0). Она представляет со­
бой архитектуру, которая, по сути, гласит следующей: в случае построения типов в со­
ответствии с правилами СОМ, будет получаться блок многократно используемого дво­
ичного кода. Такие двоичные блоки кода СОМ часто называют “серверами СОМ”.
Одним из главным преимуществ двоичного СОМ-сервера является то, что к нему
можно получать доступ независимым от языка образом. Это означает, что программи­
сты на C++ могут создавать COM-классы, пригодные для использования в VB6, про­
граммисты на Delphi — применять COM-классы, созданные с помощью С, и т.д. Однако,
Глава 1. Философия .NET
47
как не трудно догадаться, подобная независимость СОМ от языка является несколько
ограниченной. Например, никакого способа для порождения нового COM-класса с ис­
пользованием уже существующего не имеется (поскольку СОМ не обладает поддержкой
классического наследования). Вместо этого для использования типов COM-класса тре­
буется задавать несколько неуклюжее отношение принадлежности (has-a).
Еще одним преимуществом СОМ является прозрачность расположения. За счет при­
менения конструкций вроде системного реестра, идентификаторов приложений (АррШ),
заглушек, прокси-объектов и исполняющей среды СОМ программисты могут избегать
необходимости иметь дело с самими сокетам, RPC-вызовам и другими низкоуровневы­
ми деталями при создании распределенного приложения. Например, рассмотрим сле­
дующий программный код COM-клиента HaVB6:
1 Данный тип MyCOMClass мог бы быть написан на любом
1 поддерживающем СОМ языке и размещаться в любом месте
1 в сети (в том числе и на локальной машине) .
Dim obj as MyCOMClass
Set obj = New MyCOMClass 1 Местонахождение определяется с помощью AppID.
ob;j .DoSomeWork
Хотя COM и можно считать очень успешной объектной моделью, ее внутреннее уст­
ройство является чрезвычайно сложным (и требует затрачивания программистами мно­
гих месяцев на его изучение, особенно теми, которые программируют на C++). Для облег­
чения процесса разработки двоичных COM-объектов программисты могут использовать
многочисленные платформы, поддерживающие СОМ. Например, в ATL (Active Template
Library — библиотека активных шаблонов) для упрощения процесса создания СОМсерверов предоставляется набор специальных классов, шаблонов и макросов на C++.
Во многих других языках приличная часть инфраструктуры СОМ тоже скрывается
из вида. Поддержки одного только языка, однако, для сокрытия всей сложности СОМ не
хватает. Даже при выборе относительно простого поддерживающего СОМ языка вроде
VB6, все равно требуется бороться с “хрупкими” записями о регистрации и многочис­
ленными деталями развертывания (в совокупности несколько саркастично называемы­
ми адом DLL).
Сложность представления типов данных СОМ
Хотя СОМ, несомненно, упрощает процесс создания программных приложений с по­
мощью различных языков программирования, независимая от языка природа СОМ не
является настолько простой, насколько возможно хотелось бы.
Некоторая доля этой сложности является следствием того факта, что приложения,
которые сплетаются вместе с помощью разнообразных языков, получаются совершенно
не связанными с синтаксической точки зрения. Например, синтаксис JScript во мно­
гом похож на синтаксис С, а синтаксис VBScript представляет собой подмножество син­
таксиса VB6. COM-серверы, которые создаются для выполнения в исполняющей среде
СОМ+ (представляющей собой компонент операционной системы Windows, который
предлагает общие службы для библиотек специального кода, такие как транзакции,
жизненный цикл объектов, безопасность и т.д.), имеют совершенно не такой вид и по­
ведение, как ориентированные на использование в веб-сети ASP-страницы, в которых
они вызываются. В результате получается очень запутанная смесь технологий.
Более того, что, пожалуй, даже еще важнее, каждый язык и/или технология облада­
ет собственной системой типов (которая может быть совершенно не похожа на систему
типов другого языка или технологии). Помимо того факта, что каждый API-интерфейс
поставляется с собственной коллекцией готового кода, даже базовые типы данных мо­
гут не всегда интерпретироваться идентичным образом. Например, тип CComBSTR в ATL
48
Часть I. Общие сведения о языке C# и платформе .NET
представляет собой не совсем то же самое, что тип String в VB6, и оба они не имеют
совершенно ничего общего с типом char* в С.
Из-за того, что каждый язык обладает собственной уникальной системой типов,
СОМ-программистам обычно требуется соблюдать предельную осторожность при соз­
дании общедоступных методов в общедоступных классах СОМ. Например, при возник­
новении у разработчика на C++ необходимости в создании метода, способного возвра­
щать массив целых чисел в приложении VB6, ему пришлось бы полностью погружаться
в сложные вызовы API-интерфейс а СОМ для построения структуры SAFE ARRAY, которое
вполне могло бы потребовать написания десятков строк кода. В мире СОМ тип данных
SAFEARRAY является единственным способом для создания массива, который могли бы
распознавать все платформы СОМ. Если разработчик на C++ вернет просто собствен­
ный массив C++, у приложения VB6 не будет никакого представления о том, что с ним
делать.
Подобные сложности могут возникать и при построении методов, предусматриваю­
щих выполнение манипуляций над простыми строковыми данными, ссылками на дру­
гие объекты СОМ и даже обычными булевскими значениями. Мягко говоря, программи­
рование с использованием СОМ является очень несимметричной дисциплиной.
Решение .NET
Немало информации для короткого урока истории. Пгавное понять, что жизнь про­
граммиста Windows-приложений раньше была трудной. Платформа .NET Framework яв­
ляет собой достаточно радикальную “силовую” попытку сделать жизнь программистов
легче. Как можно будет увидеть в остальной части настоящей книги, .NET Framework
представляет собой программную платформу для создания приложений на базе семей­
ства операционных систем Windows, а также многочисленных операционных систем
производства не Microsoft, таких как Mac OS X и различные дистрибутивы Unix и Linux.
Для начала не помешает привести краткий перечень некоторых базовых функциональ­
ных возможностей, которыми обладает .NET.
• Возможность обеспечения взаимодействия с существующим программным
кодом. Эта возможность, несомненно, является очень хорошей вещью, поскольку
позволяет комбинировать существующие двоичные единицы СОМ (т.е. обеспечи­
вать их взаимодействие) с более новыми двоичными единицами .NET и наоборот.
С выходом версии .NET 4.0 эта возможность стала выглядеть даже еще проще,
благодаря добавлению ключевого слова dynamic (о котором будет более подробно
рассказываться в главе 18).
• Поддержка для многочисленных языков программирования. Приложения .NET
можно создавать с помощью любого множества языков программирования (С#,
Visual Basic, F#, S# и т.д.).
• Общий исполняющий механизм, используемый всеми поддерживающими .NEH
языками. Одним из аспектов этого механизма является наличие хорошо опре­
деленного набора типов, которые способен понимать каждый поддерживающий
.NET язык.
• Полная и тотальная интеграция языков. В .NET поддерживается межъязыковое на­
следование, межъязыковая обработка исключений и межъязыковая отладка кода.
• Обширная библиотека базовых классов. Эта библиотека позволяет избегать слож­
ностей, связанных с выполнением прямых вызовов к API-интерфейсу, и предлага­
ет согласованную объектную модель, которую могут использовать все поддержи­
вающие .NET языки.
Глава 1. Философия .NET
49
• Отсутствие необходимости в предоставлении низкоуровневых деталей СОМ.
В двоичной единице .NET нет места ни для интерфейсов IClassFactory, IUnknown
и IDispatch, ни для кода IDL, ни для вариантных типов данных (подобных BSTR,
SAFEARRAY
И Т.Д.).
• Упрощенная модель развертывания. В .NET нет никакой необходимости забо­
титься о регистрации двоичной единицы в системном реестре. Более того, в .NET
позволяется делать так, чтобы многочисленные версии одной и той же сборки
* .dll могли без проблем сосуществовать на одной и той же машине.
Как не трудно догадаться по приведенному выше перечню, платформа .NET не имеет
ничего общего с СОМ (за исключением разве что того факта, что обе этих платформы
являются детищем Microsoft). На самом деле единственным способом, которым может
обеспечиваться взаимодействие между типами .NET и СОМ, будет использование уров­
ня функциональной совместимости.
Главные компоненты платформы
.NET (CLR, CTS и CLS)
Теперь, когда о некоторых из предоставляемых .NET преимуществах уже известно,
давайте вкратце ознакомимся с тремя ключевыми (и связанными между собой) сущно­
стями, которые делают предоставление этих преимуществ возможным: CLR, CTS и CLS.
С точки зрения программиста .NET представляет собой исполняющую среду и обшир­
ную библиотеку базовых классов. Уровень исполняющей среды называется общеязы­
ковой исполняющей средой (Common Language Runtime) или, сокращенно, средой CLR.
Главной задачей CLR является автоматическое обнаружение, загрузка и управление ти­
пами .NET (вместо программиста). Кроме того, среда CLR заботится о ряде низкоуров­
невых деталей, таких как управление памятью, обслуживание приложения, обработка
потоков и выполнение различных проверок, связанных с безопасностью.
Другим составляющим компонентом платформы .NET является общая система
типов (Common Type System) или, сокращенно, система CTS. В спецификации CTS пред­
ставлено полное описание всех возможных типов данных и программных конструкций,
поддерживаемых исполняющей средой, того, как эти сущности могут взаимодействовать
друг с другом, и того, как они могут представляться в формате метаданных .NET (кото­
рые более подробно рассматриваются далее в этой главе и полностью — в главе 15).
Важно понимать, что любая из определенных в CTS функциональных возможно­
стей может не поддерживаться в отдельно взятом языке, совместимом с .NET. Поэтому
существует еще общеязыковая спецификация (Common Language Specification) или, со­
кращенно, спецификация CLS, в которой описано лишь то подмножество общих типов
и программных конструкций, каковое способны воспринимать абсолютно все поддер­
живающие .NET языки программирования. Следовательно, в случае построения типов
.NET только с функциональными возможностями, которые предусмотрены в CLS, мож­
но оставаться полностью уверенным в том, что все совместимые с .NET языки смогут
их использовать. И, наоборот, в случае применения такого типа данных или конст­
рукции программирования, которой нет в CLS, рассчитывать на то, что каждый язык
программирования .NET сможет взаимодействовать с подобной библиотекой кода .NET,
нельзя. К счастью, как будет показано позже в этой главе, существует очень простой
способ указывать компилятору С#, чтобы он проверял весь код на предмет совмести­
мости с CLS.
50
Часть I. Общие сведения о языке C# и платформе .NET
Роль библиотек базовых классов
Помимо среды CLR и спецификаций CTS и CLS, в составе платформы .NET постав­
ляется библиотека базовых классов, которая является доступной для всех языков про­
граммирования .NET. В этой библиотеке не только содержатся определения различных
примитивов, таких как потоки, файловый ввод-вывод, системы графической визуали­
зации и механизмы для взаимодействия с различными внешними устройствами, но
также предоставляется поддержка для целого ряда служб, требуемых в большинстве
реальных приложений.
Например, в библиотеке базовых классов содержатся определения типов, которые
способны упрощать процесс получения доступа к базам данных, манипулирования
XML-документами, обеспечения программной безопасности и создания веб-, а так­
же обычных настольных и консольных интерфейсов. На высоком уровне взаимосвязь
между CLR, CTS, CLS и библиотекой базовых классов выглядит так, как показано на
рис. 1.1.
Библиотека базовых классов
Доступ
к базе данных
Н астольны е
граф ические
AP I-ин тер ф е й сы
О р га ни зац и я
п ото ко в ой обра б о тки
Файловый
ввод-вывод
Безопасность
API-интерфейсы
для работы
с веб-содержимым
API-интерф ейсы
для удаленной работы
и другие
Общеязыковая исполняющая среда (CLR)
Общая система типов (CTS)
Общеязыковая спецификация (CLS)
Рис. 1.1. Отношения между CLR, CTS, CLS и библиотеками базовых классов
Что привносит язык C#
Из-за того, что платформа .NET столь радикально отличается от предыдущих тех­
нологий, в Microsoft разработали специально под нее новый язык программирования
С#. Синтаксис этого языка программирования очень похож на синтаксис языка Java.
Однако сказать, что C# просто переписан с Java, будет неточно. И язык С#, и язык Java
просто оба являются членами семейства языков программирования С (в которое также
входят языки С, Objective С, C++ и т.д.) и потому имеют схожий синтаксис.
Правда состоит в том, что многие синтаксические конструкции в C# моделируются
согласно различным особенностям Visual Basic 6.0 и C++. Например, как и в VB6, в C#
поддерживается понятие формальных свойств типов (в противоположность традицион­
ным методам get и set) и возможность объявлять методы, принимающие переменное
количество аргументов (через массивы параметров). Как и в C++, в C# допускается пе­
регружать операции, а также создавать структуры, перечисления и функции обратного
вызова (посредством делегатов).
Глава 1. Философия .NET
51
Более того, по мере изучения излагаемого в этой книге материала, можно будет бы­
стро заметить, что в C# поддерживается целый ряд функциональных возможностей,
которые традиционно встречаются в различных функциональных языках программи­
рования (например, LISP или Haskell) и к числу которых относятся лямбда-выражения
и анонимные типы. Кроме того, с появлением технологии LINQ в C# стали поддержи­
ваться еще и конструкции, которые делают его довольно уникальным в мире програм­
мирования. Несмотря на все это, наибольшее влияние на него все-таки оказали именно
языки на базе С.
Благодаря тому факту, что C# представляет собой собранный из нескольких языков
гибрид, он является таким же “чистым” с синтаксической точки зрения, как и язык
Java (а то и “чище” его), почти столь же простым, как язык VB6, и практически таким
же мощным и гибким как C++ (только без ассоциируемых с ним громоздких элементов).
Ниже приведен неполный список ключевых функциональных возможностей языка С#,
которые присутствуют во всех его версиях.
• Никаких указателей использовать не требуется! В программах на C# обычно не
возникает необходимости в манипулировании указателями напрямую (хотя опус­
титься к этому уровню все-таки можно, как будет показано в главе 12).
• Управление памятью осуществляется автоматически посредством сборки мусора.
По этой причине ключевое слово delete в C# не поддерживается.
• Предлагаются формальные синтаксические конструкции для классов, интерфей­
сов, структур, перечислений и делегатов.
• Предоставляется аналогичная C++ возможность перегружать операции для поль­
зовательских типов, но без лишних сложностей (например, заботиться о “возврате
*this для обеспечения связывания” не требуется).
• Предлагается поддержка для программирования с использованием атрибутов.
Такой подход в сфере разработки позволяет снабжать типы и их членов аннота­
циями и тем самым еще больше уточнять их поведение.
С выходом версии .NET 2.0 (примерно в 2005 г.), язык программирования C# был
обновлен и стал поддерживать многочисленные новые функциональные возможности,
наиболее заслуживающие внимания из которых перечислены ниже.
• Возможность создавать обобщенные типы и обобщенные элементы-члены. За
счет применения обобщений можно создавать очень эффективный и безопасный
для типов код с многочисленными метками-заполнителями, подстановка значе­
ний в которые будет происходить в момент непосредственного взаимодействия с
данным обобщенным элементом.
• Поддержка для анонимных методов, каковые позволяют предоставлять встраи­
ваемую функцию везде, где требуется использовать тип делегата.
• Многочисленные упрощения в модели “делегат-событие”, в том числе возмож­
ность применения ковариантности, контравариантности и преобразования групп
методов. (Если какие-то из этих терминов пока не знакомы, не стоит пугаться; все
они подробно объясняются далее в книге.)
• Возможность определять один тип в нескольких файлах кода (или, если необходи­
мо, в виде представления в памяти) с помощью ключевого слова partial.
В версии .NET 3.5 (которая вышла примерно в 2008 г.) в язык программирования
C# снова были добавлены новые функциональные возможности, наиболее важные из
которых описаны ниже.
• Поддержка для строго типизированных запросов (также называемых запросами
LINQ), которые применяются для взаимодействия с различными видами данных.
52
Часть I. Общие сведения о языке C# и платформе .NET
• Поддержка для анонимных типов, которые позволяют моделировать форму типа,
а не его поведение.
• Возможность расширять функциональные возможности существующего типа с
помощью методов расширения.
• Возможность использовать лямбда-операцию (=>), которая даже еще больше упро­
щает работу с типами делегатов в .NET.
• Новый синтаксис для инициализации объектов, который позволяет устанавли­
вать значения свойств во время создания объектов.
В текущем выпуске платформы .NET версии 4.0 язык C# был опять обновлен и до­
полнен рядом новых функциональных возможностей. Хотя приведенный ниже перечень
новых конструкций может показаться довольно ограниченным, по ходу прочтения на­
стоящей книги можно будет увидеть, насколько полезными они могут оказаться.
• Поддержка н еобязательн ы х параметров в методах, а также именованных
аргументов.
• Поддержка динамического поиска членов во время выполнения посредством клю­
чевого слова dynamic. Как будет показано в главе 18, эта поддержка предоставля­
ет в распоряжение универсальный подход для осуществления вызова членов “на
лету”, с помощью какой бы платформы они не были реализованы (COM, IronRuby,
IronPython, HTML DOM или службы рефлексии .NET).
,
• Вместе с предыдущей возможностью в .NET 4.0 значительно упрощается обеспе­
чение взаимодействия приложений на C# с унаследованными серверами СОМ,
благодаря устранению зависимости от сборок взаимодействия (interop assemblies)
и предоставлению поддержки необязательных аргументов ref.
• Работа с обобщенными типами стала гораздо понятнее, благодаря появлению
возможности легко отображать обобщенные данные на и из общих коллекций
System.Object с помощью ковариантности и контравариантности.
Возможно, наиболее важным моментом, о котором следует знать, программируя на
С#, является то, что с помощью этого языка можно создавать только такой код, который
будет выполняться в исполняющей среде .NET (использовать C# для построения “клас­
сического” COM-сервера или неуправляемого приложения с вызовами API-интерфейса и
кодом на С и C++ нельзя). Официально код, ориентируемый на выполнение в исполняю­
щей среде .NET, называется управляемым кодом (managed code), двоичная единица, в
которой содержится такой управляемый код — сборкой (assembly; о сборках будет более
подробно рассказываться позже в настоящей главе), а код, который не может обслужи­
ваться непосредственно в исполняющей среде .NET — неуправляемым кодом (unma­
naged code).
Другие языки программирования
с поддержкой .NET
Следует понимать, что C# является не единственным языком, который может приме­
няться для построения .NET-приложений. При установке доступного для бесплатной за­
грузки комплекта разработки программного обеспечения Microsoft .NET 4.0 Framework
Software Development Kit (SDK), равно как и при установке Visual Studio 2010, для выбо­
ра становятся доступными пять управляемых языков: С#, Visual Basic, C++/CLI, JScript
.NET и F#.
Глава 1. Философия .NET
53
На заметку! F# — это новый язык .NET, основанный на семействе функциональных языков ML и
главным образом — на OCaml. Хотя он может применяться в качестве чисто функционального
языка, в нем также предлагается поддержка для конструкций ООП и библиотек базовых клас­
сов .NET. Тем,.кому интересно узнать больше о нем, могут посетить его официальную веб-ст­
раницу по следующему адресу: http://msdn .microsoft.сот/fsharp.
Помимо управляемых языков, предлагаемых Microsoft, существуют .NET-компиляторы, которые предназначены для таких языков, как Smalltalk, COBOL и Pascal (и это
далеко не полный перечень). Хотя в настоящей книге все внимание практически полно­
стью уделяется лишь С#, следующий веб-сайт тоже вызвать интерес:
h ttp : //www. dotnetlanguages. net
Щелкнув на ссылке Resources (Ресурсы) в самом верху домашней страницы этого
сайта, можно получить доступ к списку всех языков программирования .NET и соот­
ветствующих ссылок, по которым для них можно загружать различные компиляторы
(рис. 1.2).
_ . Т -Т " «
i. N
E T LML (чaс> n
tilл г
D e d ic a t e d
g u a g es
INVESTIG
News |FAQI Resources |Contact |RSSFeed |Rec*»nt „omments F»wJ
R e so u rce s
Following is a listing of resources that you may find useful either to own or bookmark during
your navigation through the NET language space. If you feel that there s a resource that
other .NET developers should know about, please contact me.
.NET Language Sites
APL
ASP NET; ASM to IL
AsmL
ASP (Gotham)
Basic
O VB NET (Microsoft)
О VB .NET (Mono)
BETA
Boo
BlueDragon
C
О Ice
_____
Ф
Internet | Protected Mode- On
Рис. 1.2. Один из многочисленных сайтов с документацией
по известным языкам программирования .NET
Хотя настоящая книга ориентирована главным образом на тех, кого интересует раз­
работка программ .NET с использованием С#, все равно рекомендуется посетить ука­
занный сайт, поскольку там наверняка можно будет найти много языков для .NET, за­
служивающих отдельного изучения в свободное время (вроде LISP .NET).
54
Часть I. Общие сведения о языке C# и платформе .NET
Жизнь в многоязычном окружении
В начале процесса осмысления разработчиком нейтральной к языкам природы плат­
формы .NET, у него возникает множество вопросов и, прежде всего, следующий: если
все языки .NET при компиляции преобразуются в управляемый код, то почему сущест­
вует не один, а множество компиляторов? Ответить на этот вопрос можно по-разному.
Мы, программисты, бываем очень привередливы, когда дело касается выбора языка
программирования. Некоторые предпочитают языки с многочисленными точками с за­
пятой и фигурными скобками, но с минимальным набором ключевых слов. Другим нра­
вятся языки, предлагающие более “человеческие” синтаксические лексемы (вроде языка
Visual Basic). Кто-то не желает отказываться от своего опыта работы на мэйнфреймах и
предпочитает переносить его и на платформу .NET (использовать COBOL .NET).
А теперь ответьте честно: если бы в Microsoft предложили единственный “офици­
альный” язык .NET, например, на базе семейства BASIC, то все ли программисты были
бы рады такому выбору? Или если бы “официальный” язык .NET основывался на син­
таксисе Fortran, то сколько людей в мире просто бы проигнорировало платформу .NET?
Поскольку среда выполнения .NET демонстрирует меньшую зависимость от языка, ис­
пользуемого для построения управляемого программного кода, программисты .NET мо­
гут, не меняя своих синтаксических предпочтений, обмениваться скомпилированными
сборками со своими коллегами, другими отделами и внешними организациями (не об­
ращая внимания на то, какой язык .NET в них применяется).
Еще одно полезное преимущество интеграции различных языков .NET в одном уни­
фицированном программном решении вытекает из того простого факта, что каждый
язык программирования имеет свои сильные (а также слабые) стороны. Например,
некоторые языки программирования обладают превосходной встроенной поддержкой
сложных математических вычислений. В других лучше реализованы финансовые или
логические вычисления, взаимодействие с мэйнфреймами и т.п. А когда преимущества
конкретного языка программирования объединяются с преимуществами платформы
.NET, выигрывают все.
Конечно, в реальности велика вероятность того, что будет возможность тратить боль­
шую часть времени на построение программного обеспечения с помощью предпочитае­
мого языка .NET Однако, после освоения синтаксиса одного из языков .NET, изучение
синтаксиса какого-то другого языка существенно упрощается. Вдобавок это довольно
выгодно, особенно тем, кто занимается консультированием по разработке ПО. Например,
тому, у кого предпочитаемым языком является С#, в случае попадания в клиентскую
среду, где все построено на Visual Basic, это все равно позволит эксплуатировать функ­
циональные возможности .NET Framework и разбираться в общей структуре кодовой
базы с минимальным объемом усилий и беспокойства. Сказанного вполне достаточно.
Что собой представляют сборки в .NET
Какой бы язык .NET не выбирался для программирования, важно понимать, что хотя
двоичные .NET-единицы имеют такое же файловое расширение, как и двоичные едини­
цы COM-серверов и неуправляемых программ Win32 (* . dll или * . ехе), внутренне они
устроены абсолютно по-другому. Например, двоичные .NET-единицы * .dll не экспор­
тируют методы для упрощения взаимодействия с исполняющей средой СОМ (поскольку
.NET — это не СОМ). Более того, они не описываются с помощью библиотек СОМ-типов
и не регистрируются в системном реестре. Пожалуй, самым важным является то, что
они содержат не специфические, а наоборот, не зависящие от платформы инструкции
на промежуточном языке (Intermediate Language — IL), а также метаданные типов. На
рис. 1.3 показано, как все это выглядит схематически.
Глава 1. Философия .NET
55
Рис. 1.3. Все .NET-компиляторы генерируют IL-инструкции и метаданные
На заметку! Относительно сокращения “IL” уместно сказать несколько дополнительных слов. В ходе
разработки .NET официальным названием для IL было Microsoft Intermediate Language (MSIL).
Однако в вышедшей последней версии .NET это название было изменено на OIL (Common
Intermediate Language — общий промежуточный язык). Поэтому при прочтении литературы по
.NET следует помнить о том, что IL, MSIL и CIL обозначают одно и то же. Для отражения совре­
менной терминологии в настоящей книге будет применяться аббревиатура CIL.
При создании файла * .dll или * .ехе с помощью .NET-компилятора получаемый
большой двоичный объект называется сборкой (assembly). Все многочисленные детали
.NET-сборок будет подробно рассматриваться в главе 14. Для облегчения повествования
об исполняющей среде здесь, однако, все-таки необходимо рассказать хотя бы об основ­
ных свойствах этого нового формата файлов.
Как уже упоминалось, в сборке содержится CIL-код, который концептуально похож
на байт-код Java тем, что не компилируется в ориентированные на конкретную плат­
форму инструкции до тех пор, пока это не становится абсолютно необходимым. Обычно
этот момент “абсолютной необходимости” наступает тогда, когда к какому-то блоку CILинструкций (например, к реализации метода) выполняется обращение для его исполь­
зования в исполняющей среде .NET.
Помимо CIL-инструкций, в сборках также содержатся метаданные, которые де­
тально описывают особенности каждого имеющегося внутри данной двоичной .NETединицы “типа”. Например, при наличии класса по имени SportsCar они будут опи­
сывать детали наподобие того, как выглядит базовый класс этого класса SportsCar,
какие интерфейсы реализует SportsCar (если вообще реализует), а также, подробно,
какие члены он поддерживает. Метаданные .NET всегда предоставляются внутри сборки
и автоматически генерируются компилятором соответствующего распознающего .NET
языка.
И, наконец, помимо CIL и метаданных типов, сами сборки тоже описываются с помо­
щью метаданных, которые официально называются манифестом (manifest). В каждом
таком манифесте содержится информация о текущей версии сборки, сведения о культу­
ре (применяемые для локализации строковых и графических ресурсов) и перечень ссы­
лок на все внешние сборки, которые требуются для правильного функционирования.
Разнообразные инструменты, которые можно использовать для изучения типов, мета­
данных и манифестов сборок, рассматриваются в нескольких последующих главах.
56
Часть I. Общие сведения о языке C# и платформе .NET
Однофайловые и многофайловые сборки
В большом количестве случаев между сборками .NET и файлами двоичного кода
(* . d l l или * . ехе) соблюдается простое соответствие “один к одному”. Следовательно,
получается, что при построении * . d l l -библиотеки .NET, можно спокойно полагать, что
файл двоичного кода и сборка представляют собой одно и то же, и что, аналогичным
образом, при построении исполняемого приложения для настольной системы на файл
* . ехе можно ссылаться как на саму сборку. Однако, как будет показано в главе 14, это
не совсем так. С технической точки зрения, сборка, состоящая из одного единственного
модуля * . d l l или * . ехе, называется однофайловой сборкой. В однофайловых сборках
все необходимые CIL-инструкции, метаданные и манифесты содержатся в одном авто­
номном четко определенном пакете.
Многомофайловые сборки, в свою очередь, состоят из множества файлов двоичного
кода .NET, каждый из которых называется модулем (module). При построении много­
файловой сборки в одном из ее модулей (называемом первичным или главным (primary)
модулем) содержится манифест всей самой сборки (и, возможно, CIL-инструкции и ме­
таданные по различным типам), а во всех остальных — манифест, CIL-инструкции и
метаданные типов, охватывающие уровень только соответствующего^ модуля. Как не­
трудно догадаться, в главном модуле содержится описание набора требуемых дополни­
тельных модулей внутри манифеста сборки.
На заметку! В главе 14 будет более подробно разъясняться, в чем состоит различие между одно­
файловыми и многофайловыми сборками. Однако следует иметь в виду, что Visual Studio 2010
может применяться только для создания однофайловых сборок. В тех редких случаях возник­
новения необходимости в создании именно многофайловой сборки требуется использовать
соответствующие утилиты командной строки.
Роль CIL
Теперь давайте немного более подробно посмотрим, что же собой представляет CILкод, метаданные типов и манифест сборки. CIL является таким языком, который стоит
выше любого конкретного набора ориентированных на определенную платформу ин­
струкций. Например, ниже приведен пример кода на С#, в котором создается модель
самого обычного калькулятора. Углубляться в конкретные детали синтаксиса пока не
нужно, главное обратить внимание на формат такого метода в этом классе Calc, как
A d d () .
//Класс C alc.cs
using System;
namespace CalculatorExample
{
/ / В этом классе содержится точка для входа в приложение.
class Program
{
static void M ain()
{
Calc c = new Calc () ;
int ans = c.Add(10, 84);
Console .WnteLine (" 10 + 84 is {0}.", ans) ;
// Обеспечение ожидания нажатия пользователем
// клавиши <Enter> перед выходом.
Console.ReadLine();
// Калькулятор на С#.
class Calc
Глава 1. Философия .NET
57
{
public int Add(int x, int y)
{ return x + y; }
}
}
После выполнения компиляции файла с этим кодом с помощью компилятора C#
(csc.exe) получится однофайловая сборка * .ехе, в которой будет содержаться мани­
фест, CIL-инструкции и метаданные, описывающие каждый из аспектов класса Calc и
Program.
На заметку! О том, как выполнять компиляцию кода с помощью компилятора С#, а также исполь­
зовать графические IDE-среды, подобные Microsoft Visual Studio 2010, Microsoft Visual C# 2010
Express и SharpDevelop, будет более подробно рассказываться в главе 2.
Например, открыв данную сборку в утилите ild a s m .e x e (которая более подробно
рассматривается далее в настоящей главе), можно увидеть, что метод Add () был преоб­
разован в CIL так, как показано ниже:
.method public hidebysig instance int32 Add(int32 x, int32 y) cil managed
{
// Code size
9 (0x9)
// Размер кода 9 (0x9)
.maxstack 2
.locals m i t (int32 V_0)
IL_0000: nop
IL_0001: ldarg.l
IL_0002: ldarg.2
IL_0003: add
IL_0004: stloc.O
IL_0005: br.s IL_0007
IL_0007: ldloc.O
IL_0008: ret
} // end of method Calc::Add
// конец метода Calc::Add
He стоит беспокоиться, если пока совершенно не понятно, что собой представляет
результирующий CIL-код этого метода, потому что в главе 17 будут рассматриваться
все необходимые базовые аспекты языка программирования CIL. Главное заметить, что
компилятор C# выдает CIL-код, а не ориентированные на определенную платформу ин­
струкции. Теперь напоминаем, что так себя ведут все .NET-компиляторы. Чтобы убе­
диться в этом, давайте попробуем создать то же самое приложение с использованием
языка Visual Basic, а не С#.
'Класс Calc.vb
Imports System
Namespace CalculatorExample
' В VB "модулем" называется класс, в котором
' содержатся только статические члены.
Module Program
Sub Main ()
Dim c As New Calc
Dim ans As Integer = c. Add (10, 84)
Console.WnteLine ("10 + 84 is {0}.", ans)
Console.ReadLine()
End Sub
End Module
Class Calc
58
Часть I. Общие сведения о языке C# и платформе .NET
Public Function Add(ByVal x As Integer, ByVal у As Integer) As Integer
Return x + у
End Function
End Class
End Namespace
В случае изучения CIL-кода этого метода Add () можно будет обнаружить похожие
инструкции (лишь слегка подправленные компилятором Visual Basic, vbc . exe):
.method public instance int32
Add(int32 x, int32 y) cil managed
// Code size
8 (0x8)
// Размер кода 8 (0x8)
.maxstack 2
.locals m i t (int32 V_0)
IL_0000: ldarg.1
IL_0001: ldarg.2
IL_0002: add.ovf
IL_0003: stloc.0
I L _ 0 0 0 4 : br.s IL_0006
IL_000 6: ldloc.0
IL 0007: ret
} // end of method Calc: :Add
// конец метода Calc::Add
Исходный код. Файлы с кодом C a lc . cs и C alc . vb доступны в подкаталоге Chapter 1.
Преимущества CIL
На этом этапе может возникнуть вопрос о том, какую выгоду приносит компиляция
исходного кода в CIL, а не напрямую в набор ориентированных на конкретную плат­
форму инструкций. Одним из самых важных преимуществ такого подхода является
интеграция языков. Как уже можно было увидеть, все компиляторы .NET генерируют
примерно одинаковые CIL-инструкции. Благодаря этому все языки могут взаимодейст­
вовать в рамках четко обозначенной двоичной “арены”.
Более того, поскольку CIL не зависит от платформы, .NET Framework тоже получа­
ется не зависящей от платформы, предоставляя те же самые преимущества, к которым
привыкли Java-разработчики (например, единую кодовую базу, способную работать во
многих операционных системах). На самом деле уже существует международный стан­
дарт языка С#, а также подмножество платформы .NET и реализации для многих опе­
рационных систем, отличных от Windows (более подробно об этом речь пойдет в конце
настоящей главы). В отличие от Java, однако, .NET позволяет создавать приложения на
предпочитаемом языке.
Компиляция C IL-кода в инструкции,
ориентированные на конкретную платформу
Из-за того, что в сборках содержатся CIL-инструкции, а не инструкции, ориенти­
рованные на конкретную платформу, CIL-код перед использованием должен обяза­
тельно компилироваться на лету. Объект, который отвечает за компиляцию CIL-кода
в понятные ЦП инструкции, называется оперативным (just-in-time — JIT) компилято­
ром. Иногда его “по-дружески” называют Jitter. Исполняющая среда .NET использует
JIT-компилятор в соответствии с целевым ЦП и оптимизирует его согласно лежащей в
основе платформе.
Глава 1. Философия .NET
59
Например, в случае создания .NET-приложения, предназначенного для развертыва­
ния на карманном устройству (например, на мобильном устройстве, функционирующем
под управлением Windows), соответствующий JIT-компилятор будет оптимизирован под
функционирование в среде с ограниченным объемом памяти, а в случае развертыва­
ния сборки на серверной системе (где объем памяти редко представляет проблему), на­
оборот — под функционирование в среде с большим объемом памяти. Это дает разра­
ботчикам возможность писать единственный блок кода, который будет автоматически
эффективно компилироваться JIT-компилятором и выполняться на машинах с разной
архитектурой.
Более того, при компиляции CIL-инструкций в соответствующий машинный код JITкомпилятор будет помещать результаты в кэш в соответствии с тем, как того требует це­
левая операционная система. То есть при вызове, например, метода PrintDocument ()
в первый раз соответствующие С11>инструкции будут компилироваться в ориентиро­
ванные на конкретную платформу инструкции и сохраняться в памяти для последую­
щего использования, благодаря чему при вызове PrintDocument () в следующий раз
компилировать их снова не понадобится.
На заметку! Можно также выполнять “ предварительную JIT-компиляцию” при инсталляции прило­
жения с помощью утилиты командной строки ngen.exe, которая поставляется в составе на­
бора .NET Framework 4.0 SDK. Применение такого подхода позволяет улучшить показатели по
времени запуска для приложений, насыщенных графикой.
Роль метаданных типов в .NET
Помимо СIL-инструкций, в сборке .NET содержатся исчерпывающие и точные мета­
данные, которые описывают каждый определенный в двоичном файле тип (например,
класс, структуру или перечисление), а также всех его членов (например, свойства, ме­
тоды или события). К счастью, за генерацию новейших и наилучших метаданных по
типам всегда отвечает компилятор, а не программист. Из-за того, что метаданные .NET
являются настолько детальными, сборки представляют собой полностью самоописываемые (self-describing) сущности.
Чтобы увидеть, как выглядит формат метаданных типов в .NET, давайте рассмот­
рим метаданные, которые были сгенерированы для приведенного выше метода Add ()
из класса Calc на языке C# (метаданные для версии метода Add () на языке Visual Basic
будут выглядеть похоже):
TypeDef #2 (02000003)
TypDefName: CalculatorExample.Calc (02000003)
Flags
: [NotPublic] [AutoLayout] [Class]
[AnsiClass] [BeforeFieldlnit] (00100001)
Extends
: 01000001 [TypeRef] System.Object
Method #1 (06000003)
MethodUame : Add (06000003)
Flags
: [Public] [HideBySig] [ReuseSlot]
PVA
: 0x00002090
ImplFlagc : [IL] [Managed] (00000000)
CallCnvntn : [DEFAULT]
hasThis
ReturnType: 14
2 Arguments
Argument #1: 14
Argument #2: 14
(00000086)
60
Часть I. Общие сведения о языке C# и платформе .NET
2 Parameters
(1) ParamToken : (08000001) Name : x flags: [none]
(2) ParamToken : (08000002) Name : у flags: [none]
(00000000)
(00000000)
Метаданные используются во многих операциях самой исполняющей среды .NET, а
также в различных средствах разработки. Например, функция IntelliSense, предлагае­
мая в таких средствах, как Visual Studio 2010, работает за счет считывания метаданных
сборки во время проектирования. Кроме того, метаданные используются в различных
утилитах для просмотра объектов, инструментах отладки и в самом компиляторе языка
С#. Можно с полной уверенностью утверждать, что метаданные играют ключевую роль
во многих .NET-технологиях, в том числе в Windows Communication Foundation (WCF),
рефлексии, динамическом связывании и сериализации объектов. Более подробно о роли
метаданных .NET будет рассказываться в главе 17.
Роль манифеста сборки
И, наконец, последним, но не менее важным моментом, о котором осталось вспом­
нить, является наличие в сборке .NET и таких метаданных, которые описывают саму
сборку (они формально называются манифестом). Помимо прочих деталей, в манифе­
сте документируются все внешние сборки, которые требуются текущей сборке для кор­
ректного функционирования, версия сборки, информация об авторских правах и т.д.
Как и за генерацию метаданных типов, за генерацию манифеста сборки тоже всегда
отвечает компилятор. Ниже приведены некоторые наиболее существенные детали ма­
нифеста, сгенерированного в результате компиляции приведенного ранее в этой главе
файла двоичного кода C a l c . cs (здесь предполагается, что компилятору было указано
назначить сборке имя C alc . ехе):
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4 :0 :0:0
}
.assembly Calc
{
.hash algorithm 0x00008004
.ver 1: 0 :0:0
}
.module Calc.exe
.imagebase 0x00400000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000001
В двух словах, в этом манифесте представлен список требуемых для C a lc .e x e внеш­
них сборок (в директиве . assem bly ex tern ), а также различные характеристики самой
сборки (наподобие номера версии, имени модуля и т.д.). О пользе данных манифеста
будет гораздо более подробно рассказываться в главе 14.
Что собой представляет общая система типов (CTS)
В каждой конкретной сборке может содержаться любое количество различающихся
типов. В мире .NET “тип” представляет собой просто общий термин, который приме­
няется для обозначения любого элемента из множества |класс, интерфейс, структура,
перечисление, делегат). При построении решений с помощью любого языка .NET, ско­
рее всего, придется взаимодействовать со многими из этих типов. Например, в сборке
Глава 1. Философия .NET
61
может содержаться один класс, реализующий определенное количество интерфейсов,
метод одного из которых может принимать в качестве входного параметра перечисле­
ние, а возвращать структуру.
Вспомните, что CTS (общая система типов) представляет собой формальную специ­
фикацию, в которой описано то, как должны быть определены типы для того, чтобы они
могли обслуживаться в CLR-среде. Внутренние детали CTS обычно интересуют только
тех, кто занимается разработкой инструментов и/или компиляторов для платформы
.NET. Абсолютно всем .NET-программистам, однако, важно уметь работать на предпочи­
таемом ими языке с пятью типами из CTS. Краткий обзор этих типов приведен ниже.
Типы классов
В каждом совместимом с .NET языке поддерживается, как минимум, понятие типа
класса (class type), которое играет центральную роль в объектно-ориентированном про­
граммировании (ООП). Каждый класс может включать в себя любое количество членов
(таких как конструкторы, свойства, методы и события) и точек данных (полей). В C#
классы объявляются с помощью ключевого слова c la s s .
// Тип класса C# с одним методом.
class Calc
{
public int Add(int x, int y)
{ return x + y; }
}
В главе 5 будет более подробно показываться, как можно создавать типы классов
CTS в С#, а пока в табл. 1.1 приведен краткий перечень характеристик, которые свой­
ственны типам классов.
Таблица 1.1. Характеристики классов CTS
Характеристика
классов
Описание
Запечатанные
Запечатанные (sealed), или герметизированные, классы не могут высту­
пать в роли базовых для других классов, т.е. не допускают наследования
Реализующие
интерфейсы
Интерфейсом (interface) называется коллекция абстрактных членов, кото­
рые обеспечивают возможность взаимодействия между объектом и поль­
зователем этого объекта. CTS позволяет реализовать в классе любое
количество интерфейсов
Абстрактные
или
конкретные
Экземпляры абстрактных (abstract) классов не могут создаваться напря­
мую, и предназначены для определения общих аспектов поведения для
производных типов. Экземпляры же конкретных (concrete) классы могут
создаваться напрямую
Степень видимости
Каждый класс должен конфигурироваться с атрибутом видимости
(visibility). По сути, этот атрибут указывает, должен ли класс быть доступ­
ным для использования внешним сборкам или только изнутри опреде­
ляющей сборки
Типы интерфейсов
Интерфейсы представляют собой не более чем просто именованную коллекцию оп­
ределений абстрактных членов, которые могут поддерживаться (т.е. реализоваться) в
данном классе или структуре. В C# типы интерфейсов определяются с помощью клю­
чевого слова in t e r fa c e , как, например, показано ниже:
62
Часть I. Общие сведения о языке C# и платформе .NET
// Тип интерфейса в C# обычно объявляется
// общедоступным, чтобы позволить типам в других
// сборках реализовать его поведение.
public interface IDraw
{
void Draw ();
}
Сами по себе интерфейсы мало чем полезны. Однако когда они реализуются в клас­
сах или структурах уникальным образом, они позволяют получать доступ к дополни­
тельным функциональным возможностям за счет добавления просто ссылки на них в
полиморфной форме. Тема программирования с использованием интерфейсов подробно
рассматривается в главе 9.
Типы структур
Понятие структуры тоже сформулировано в CTS. Тем, кому приходилось работать с
языком С, будет приятно узнать, что таким пользовательским типам удалось “выжить”
в мире .NET (хотя на внутреннем уровне они и ведут себя несколько иначе). Попросту
говоря, структура может считаться “облегченным” типом класса с основанной на ис­
пользовании значений семантикой. Более подробно об особенностях структур будет
рассказываться в главе 4. Обычно структуры лучше всего подходят для моделирования
геометрических и математических данных, и в C# они создаются с помощью ключевого
слова struct.
/ / Тип структуры в C#.
struct Point
{
/ / В структурах могут содержаться поля.
public int xPos, yPos;
/ / В структурах могут содержаться параметризованные конструкторы.
public Point (int х, int у) { xPos = x; yPos = y; }
// В структурах могут определяться методы.
public void PnntPosition ()
{
Console .WnteLine (" ({ 0 }, {1})", xPos, yPos);
}
Типы перечислений
Перечисления (enumeration) представляют собой удобную программную конструк­
цию, которая позволяет группировать данные в пары “имя-значение”. Например, пред­
положим, что требуется создать приложение видеоигры, в котором игроку бы позво­
лялось выбирать персонажа одной из трех следующих категорий: Wizard (маг), Fighter
(воин) или Thief (вор). Вместо того чтобы использовать и отслеживать числовые зна­
чения для каждого варианта, в этом случае гораздо удобнее создать соответствующее
перечисление с помощью ключевого слова enum:
// Тип перечисления С#.
public enum CharacterType
{
Wizard = 100,
Fighter = 200,
Thief = 300
}
Глава 1. Философия .NET
63
По умолчанию для хранения каждого элемента выделяется блок памяти, соответст­
вующий 32-битному целому, однако при необходимости (например, при программиро­
вании с расчетом на устройства, обладающие малыми объемами памяти, вроде мобиль­
ных устройств Windows) это значение можно изменить. Кроме того, в CTS необходимо,
чтобы перечислимые типы наследовались от общего базового класса System.Enum. Как
будет показано в главе 4, в этом базовом классе присутствует ряд весьма интересных
членов, которые позволяют извлекать, манипулировать и преобразовывать базовые
пары “имя-значение” программным образом.
Типы делегатов
Делегаты (delegate) являются .NET-эквивалентом безопасных в отношении ти ­
пов указателей функций в стиле С. Главное отличие заключается в том, что делегат в
.NET представляет собой класс, который наследуется от System. Multicast Delegate, а
не просто указатель на какой-то конкретный адрес в памяти. В C# делегаты объявляют­
ся с помощью ключевого слова delegate.
// Этот тип делегата в C# может 'указывать' на любой метод,
// возвращающий целое число и принимающий два целых
// числа в качестве входных данных.
public delegate int BinaryOp(int x, int y);
Делегаты очень удобны, когда требуется обеспечить одну сущность возможностью
перенаправлять вызов другой сущности и образовывать основу для архитектуры обра­
ботки событий .NET. Как будет показано в главах 11 и 19, делегаты обладают внутрен­
ней поддержкой для групповой адресации (т.е. пересылки запроса сразу множеству по­
лучателей) и асинхронного вызова методов (т.е. вызова методов во вторичном потоке).
Члены типов
Теперь, когда было приведено краткое описание каждого из сформулированных в
CTS типов, пришла пора рассказать о том, что большинство из этих типов способно
принимать любое количество членов (member). Формально в роли члена типа может
выступать любой элемент из множества (конструктор, финализатор, статический кон­
структор, вложенный тип, операция, метод, свойство, индексатор, поле, поле только для
чтения, константа, событие!.
В спецификации CTS описываются различные “характеристики”, которые могут
быть ассоциированы с любым членом. Например, каждый член может обладать ха­
рактеристикой, отражающей его доступность (т.е. общедоступный, приватный или за­
щищенный). Некоторые члены могут объявляться как абстрактные (для навязывания
полиморфного поведения производным типам) или как виртуальные (для определения
фиксированной, но допускающей переопределение реализации). Кроме того, почти все
члены также могут делаться статическими членами (привязываться на уровне класса)
или членами экземпляра (привязываться на уровне объекта). Более подробно о том, как
можно создавать членов, будет рассказываться в ходе нескольких следующих глав.
На заметку! Как будет описано в главе 10, в языке C# также поддерживается создание обобщен­
ных типов и членов.
Встроенные типы данных
И, наконец, последним, что следует знать о спецификации CTS, является то, что в
ней содержится четко определенный набор фундаментальных типов данных. Хотя в ка­
ждом отдельно взятом языке для объявления того или иного встроенного типа данных
64
Часть I. Общие сведения о языке C# и платформе .NET
из CTS обычно предусмотрено свое уникальное ключевое слово, все эти ключевые слова
в конечном итоге соответствуют одному и тому же типу в сборке mscorlib.dll.
В табл. 1.2 показано, как ключевые типы данных из CTS представляются в различ­
ных .NET-языках.
Таблица 1.2. Встроенные типы данных, описанные в CTS
Тип данных в CTS
Ключевое слово
в Visual Basic
Ключевое
слово в C#
Ключевое слово в С++и CLI
System.ByteByte
Byte
byte
unsigned char
System.SByteSByte
SByte
sbyte
signed char
System.Int16
Short
short
short
System.Int32
Integer
int
int или long
System.Int64
Long
long
System.Ulntl6
UShort
ushort
unsigned short
System.UInt32
UInteger
uint
unsigned int или
unsigned long
System.UInt64
ULong
ulong
unsigned
System.SingleSingle
Single
float
float
System.DoubleDouble
Double
double
double
System.Ob]ectObject
Obj ect
object
objectA
System.CharChar
Char
char
wchar t
System.Stringstring
String
String
StringA
int64
System.DecimalDecimal
Decimal
decimal
Decimal
System.BooleanBoolean
Boolean
bool
bool
int64
Из-за того факта, что уникальные ключевые слова в любом управляемом языке яв­
ляются просто сокращенными обозначениями реального типа из пространства имен
System, больше не нужно беспокоиться ни об условиях переполнения и потери значи­
мости (overflow/underflow) в случае числовых данных, ни о внутреннем представлении
строк и булевских значений в различных языках. Рассмотрим следующие фрагменты
кода, в которых 32-битные числовые переменные определяются в C# и Visual Basic с
использованием соответствующих ключевых слов из самих языков, а также формаль­
ного типа из CTS:
// Определение числовых переменных в С#.
int 1 = 0 ;
System.Int32 j = 0;
' Определение числовых переменных в VB.
Dim 1 As Integer = 0
Dim j As System.Int32 = 0
Что собой представляет общеязыковая
спецификация (CLS)
Как известно, в разных языках программирования одни и те же программные кон­
струкции выражаются своим уникальным, специфическим для конкретного языка об­
разом. Например, в C# конкатенация строк обозначается с помощью знака “плюс” (+),
Глава 1. Философия .NET
65
а в VB для этого обычно используется амперсанд (&). Даже в случае выражения в двух
отличных языках одной и той же программной идиомы (например, функции, не возвра­
щающей значения), очень высока вероятность того, что с виду синтаксис будет выгля­
деть очень по-разному:
//На возвращающий ничего метод в С#.
public void MyMethod()
{
// Какой-нибудь интересный к од ...
}
1 Не возвращакяций ничего метод в VB.
Public Sub MyMethod ()
1 Какой-нибудь интересный к од ...
End Sub
Как уже показывалось, подобные небольшие вариации в синтаксисе для исполняю­
щей среды .NET являются несущественными благодаря тому, что соответствующие
компиляторы (в данном случае — e s c . ехе и v b c . ехе) генерируют схожий набор CILинструкций. Однако языки могут еще отличаться и по общему уровню функциональных
возможностей. Например, в каком-то из языков .NET может быть или не быть ключево­
го слова для представления данных без знака, а также поддерживаться или не поддер­
живаться типы указателей. Из-за всех таких вот возможных вариаций было бы просто
замечательно иметь в распоряжении какие-то опорные требования, которым должны
были бы отвечать все поддерживающие .NET языки.
CLS (Common Language Specification — общая спецификация для языков програм­
мирования) как раз и представляет собой набор правил, которые во всех подробностях
описывают минимальный и полный комплект функциональных возможностей, которые
должен обязательно поддерживать каждый отдельно взятый .NET-компилятор для того,
чтобы генерировать такой программный код, который мог бы обслуживаться CLR и к
которому в то же время могли бы единообразным образом получать доступ все языки,
ориентированные на платформу .NET. Во многих отношениях CLS может считаться
просто подмножеством всех функциональных возможностей, определенных в CTS.
В конечном итоге CLS является своего рода набором правил, которых должны при­
держиваться создатели компиляторов при желании, чтобы их продукты могли без про­
блем функционировать в мире .NET. Каждое из этих правил имеет простое название
(например, “Правило CLS номер 6”) и описывает, каким образом его действие касается
тех, кто создает компиляторы, и тех, кто (каким-либо образом) будет взаимодействовать
с ними. Самым главным в CLS является правило I, гласящее, что правила CLS касаются
только тех частей типа, которые делаются доступными за пределами сборки, в которой
они определены.
Из этого правила можно (и нужно) сделать вывод о том, что все остальные правила в
CLS не распространяются на логику, применяемую для построения внутренних рабочих
деталей типа .NET. Единственными аспектами типа, которые должны соответствовать
CLS, являются сами определения членов (т.е. соглашения об именовании, параметры
и возвращаемые типы). В рамках логики реализации члена может применяться любое
количество и не согласованных с CLS приемов, поскольку для внешнего мира это не
будет играть никакой роли.
Для иллюстрации ниже приведен метод Add () на языке С#, который не отвечает
правилам CLS, поскольку в его параметрах и возвращаемых значениях используются
данные без знака (что не является требованием CLS):
class Calc
I
// Использование данных без знака внешним образом
// не соответствует правилам CLS!
66
Часть I. Общие сведения о языке C# и платформе .NET
public ulong A d d (ulong x, ulong y)
{ return x + y; }
Однако если бы мы просто использовали данные без знака внутренним образом, как
показано ниже:
class Calc
{
public int Add(int x,
int y)
{
// Поскольку переменная ulong используется здесь
// только внутренне, правила CLS не нарушаются.
ulong temp = 0;
return х + у;
}
тогда правила CLS были бы соблюдены и все языки .NET могли бы обращаться к дан­
ному методу Add ().
Разумеется, помимо правила 1 в CLS содержится и много других правил. Например,
в CLS также описано, каким образом в каждом конкретном языке должны представ­
ляться строки текста, оформляться перечисления (подразумевающие использование ба­
зового типа для хранения), определяться статические члены и т.д. К счастью, для того,
чтобы быть умелым разработчиком .NET, запоминать все эти правила вовсе не обяза­
тельно. Опять-таки, очень хорошо разбираться в спецификациях CTS и CLS необходимо
только создателям инструментов и компиляторов.
Забота о соответствии правилам CLS
Как можно будет увидеть в ходе прочтения настоящей книги, в C# на самом деле
имеется ряд программных конструкций, которые не соответствуют правилам CLS.
Хорошая новость, однако, состоит в том, что компилятор C# можно заставить выпол­
нять проверку программного кода на предмет соответствия правилам CLS с помощью
всего лишь единственного атрибута. NET:
// Указание компилятору C# выполнять проверку
/ / н а предмет соответствия CLS.
[assembly: System.CLSCompliant(true)]
Детали программирования с использованием атрибутов более подробно рассматри­
ваются в главе 15. А пока главное понять просто то, что атрибут [CLSCom pliant] за­
ставляет компилятор C# проверять каждую строку кода на предмет соответствия пра­
вилам CLS. В случае обнаружения нарушения каких-нибудь правил CLS компилятор
будет выдавать ошибку и описание вызвавшего ее кода.
Что собой представляет общеязыковая
исполняющая среда (CLR)
Помимо спецификаций CTS и CLS, для получения общей картины на данный мо­
мент осталось рассмотреть еще одну аббревиатуру — CLR, которая расшифровывается
как Common Language Runtime (общеязыковая исполняющая среда). С точки зрения
программирования под термином исполняющая среда может пониматься коллекция
внешних служб, которые требуются для выполнения скомпилированной единицы про­
граммного кода. Например, при использовании платформы MFC для создания нового
Глава 1. Философия .NET
67
приложения разработчики осознают, что их программе требуется библиотека времени
выполнения MFC (те. mfc4 2 . d l l ) . Другие популярные языки тоже имеют свою испол­
няющую среду: программисты, использующие язык VB6, к примеру, вынуждены при­
вязываться к одному или двум модулям исполняющей среды (вроде msvbvm60 . d l l ) , а
разработчики на Java — к виртуальной машине Java (JVM).
В составе .NET предлагается еще одна исполняющая среда. Отавное отличие между
исполняющей средой .NET и упомянутыми выше средами, состоит в том, что исполняю­
щая среда .NET обеспечивает единый четко определенный уровень выполнения, кото­
рый способны использовать все совместимые с .NET языки и платформы.
Основной механизм CLR ф изически им еет вид би бли отеки под названием
mscoree . d l l (и также называется общим механизмом выполнения исполняемого кода
объектов — Common Object Runtime Execution Engine). При добавлении ссылки на
сборку для ее использования загрузка библиотеки m scoree . d l l осуществляется авто­
матически и затем, в свою очередь, приводит к загрузке требуемой сборки в память.
Механизм исполняющей среды отвечает за выполнение целого ряда задач. Сначала,
что наиболее важно, он отвечает за определение места расположения сборки и обна­
ружение запрашиваемого типа в двоичном файле за счет считывания содержащихся
там метаданных. Затем он размещает тип в памяти, преобразует CIL-код в соответст­
вующие платформе инструкции, производит любые необходимые проверки на предмет
безопасности и после этого, наконец, непосредственно выполняет сам запрашиваемый
программный код.
Помимо загрузки пользовательских сборок и создания пользовательских типов, ме­
ханизм CLR при необходимости будет взаимодействовать и с типами, содержащимися
в библиотеках базовых классов .NET. Хотя вся библиотека базовых классов поделена на
ряд отдельных сборок, главной среди них является сборка m s c o r l i b . d l l . В этой сборке
содержится большое количество базовых типов, охватывающих широкий спектр типич­
ных задач программирования, а также базовых типов данных, применяемых во всех
языках .NET. При построении .NET-решений доступ к этой конкретной сборке будет
предоставляться автоматически.
На рис. 1.4 схематично показано, как выглядят взаимоотношения между исходным
кодом (предусматривающим использование типов из библиотеки базовых классов), ком­
пилятором .NET и механизмом выполнения .NET.
Различия между сборками,
пространствами имен и типами
Каждый из нас понимает важность библиотек программного кода. Главная цель та­
ких библиотек, как MFC, Java Enterprise Edition или ATL, заключается в предоставлении
разработчикам набора готовых, правильно оформленных блоков программного кода,
чтобы они могли использовать их своих приложениях. Язык С#, однако, не поставля­
ется с какой-либо специфической библиотекой кода. Вместо этого от разработчиков,
использующих С#, требуется применять нейтральные к языкам библиотеки, которые
поставляются в .NET. Для поддержания всех типов в библиотеках базовых классов в хо­
рошо организованном виде в .NET широко применяется понятие пространства имен
(namespace).
Под пространством имен понимается группа связанных между собой с семантиче­
ской точки зрения типов, которые содержатся в сборке. Например, в пространстве имен
System. 10 содержатся типы, имеющие отношение к операциям ввода-вывода, в про­
странстве имен System. Data — основные типы для работы с базами данных, и т.д.
68
Часть I. Общие сведения о языке C# и платформе .NET
Рис. 1.4. Механизм m s c o r e e .d ll в действии
Очень важно понимать, что в одной сборке (например, m s c o r l i b . d l l ) может содер­
жаться любое количество пространств имен, каждое из которых, в свою очередь, может
иметь любое число типов.
Чтобы стало понятнее, на рис. 1.5 показан снимок окна предлагаемой в Visual Studio
2010 утилиты Object Browser. Эта утилита позволяет просматривать сборки, на которые
имеются ссылки в текущем проекте, пространства имен, содержащиеся в каждой из
этих сборок, типы, определенные в каждом из этих пространств имени, и члены каж­
дого из этих типов. Важно обратить внимание на то, что в m s c o r l i b . d l l содержится
очень много самых разных пространств имен (вроде System. 10), и что в каждом из них
содержатся свои семантически связанные типы (такие как B in aryR eader) .
Глава 1. Философия .NET
69
I Object Bro*vse* > Ц
(I My Solut.on
1
С
¥ BinaryReader(SystemJO Stream System Text Encoding)
U
CSharpAdder
jJ
Microsoft (TSharp
»
♦
BinaiyReader(System IO.Stream)
♦
♦
CloseO
DisposeQ
Л - О msccrlib
{ } Microsoft.Win32
О Microsoft.Win32.SafeHandles
{ ) System
0
System.Collections
{ } System CollectionsCcncurrent
{ } System Collections Generic
* Dispose boclj
v F.IIBufferfintj
¥ PeeVCharQ
¥ Readfbyte[], inf, int)
¥ Read ,cha>[] int. int)
* ReadQ
^
Read7BitEncodedIntO
{ } System Collections.ObjectModel
♦
{ } System-Configuration.Assemblies
♦ ReadByteQ
¥ ReadBytes(int)
{ } System Deployment Internal
{ } System Diagnostics
0
System Diagnostics CodeAnalysis
* 1
ReadBooleanO
¥ ReadCharO
♦ ReadCbars(mt)
{ } System Diagnostics Contracts
О System-Diagnostics.Eventing
¥ ReadDouhleQ
<} System. Diagnostics SymbolStore
О System Globalization
*
{ ) System 10
Binary Writer
•t$ BufferedStream
public class
Member
B in a ry R e a d e r
o f S y s t* rn J Q
S u m m a ry .
Reads primitive data types as binary values in a specific encoding
Рис. 1.5. В одной сборке может содержаться произвольное количество пространств имен
Главное отличие между таким подходом и зависящими от конкретного языка библио­
теками вроде MFC состоит в том, что он обеспечивает использование во всех языках,
ориентированных на среду выполнения .NETT, одних и тех ж е пространств имен и одних
и тех же типов. Например, в трех приведенных ниже программах иллюстрируется соз­
дание постоянно применяемого примера “Hello World” на языках С#, VB и C++/CLI.
/ / H e ll o w o r ld н а я зы к е C#
using System;
public class MyApp
I
static void M a m ()
Console.WriteLine("Hi from C#");
' H e ll o w o r ld н а я зы к е VB
Imports System
Public Module MyApp
Sub Main ()
Console .WnteLine ("Hi from VB")
End Sub
End Module
/ / H e llo w o r ld н а язы ке C + + / C L I
#include "stdafx.h"
using namespace System;
int main (array<System: :String /‘> Лагдз)
{
Console::WnteLine ("Hi from C++/CLI") ;
return 0;
}
Обратите внимание, что в каждом из языков применяется класс Console, определенный
в пространстве имен System. Если отбросить незначительные синтаксические отличия, то
в целом все три программы выглядят очень похоже, как по форме, так и по логике.
70
Часть I. Общие сведения о языке C# и платформе .NET
Очевидно, что главной задачей любого планирующего использовать .NET разработ­
чика является освоение того обилия типов, которые содержатся в (многочисленных)
пространствах имен .NET Самым главным пространством имен, с которого следует на­
чинать, является System. В этом пространстве имен содержится набор ключевых ти­
пов, которые любому разработчику .NET нужно будет эксплуатировать снова и снова.
Фактически создание функционального приложения на C# невозможно без добавления
хотя бы ссылки на пространство имен System, поскольку все главные типы данных (вро­
де System. Int32, System. String и т.д.) содержатся именно здесь. В табл. 1.3 приведен
краткий список некоторых (но, конечно же, не всех) предлагаемых в .NET пространств
имен, которые были поделены на группы на основе функциональности.
Таблица 1.3. Некоторые пространства имен в .NET
Пространство имен в .NET
Описание
System
Внутри пространства имен System содержится множе­
ство полезных типов, позволяющих иметь дело с внут­
ренними данными, математическими вычислениями,
генерированием случайных чисел, переменными среды
и сборкой мусора, а также ряд наиболее часто приме­
няемых исключений и атрибутов
System.Collections
System.Collections.Generic
В этих пространствах имен содержится ряд контейнер­
ных типов, а также несколько базовых типов и интер­
фейсов, которые позволяют создавать специальные
коллекции
System.Data
System.Data.Common
System.Data.EntityClient
System.Data.SqlClient
Эти пространства имен применяются для взаимодейст­
вия с базами данных с помощью AD0.NET
System.10
System.10.Compression
System.10.Ports
В этих пространствах содержится много типов, предна­
значенных для работы с операциями файлового вводавывода, сжатия данных и манипулирования портами
System.Reflection
System.Reflection.Emit
В этих пространствах имен содержатся типы, которые
поддерживают обнаружение типов во время выполне­
ния, а также динамическое создание типов
System.Runtime.InteropServices
В этом пространстве имен содержатся средства, с по­
мощью которых можно позволить типам .NET взаимо­
действовать с “неуправляемым кодом” (например, DLLбиблиотеками на базе С и серверами СОМ) и наоборот
System.Drawing
System.Windows.Forms
В этих пространствах имен содержатся типы, применяе­
мые для построения настольных приложений с исполь­
зованием исходного набора графических инструментов
.NET (Windows Forms)
System.Windows
System.Windows.Controls
System.Windows.Shapes
Пространство System.Windows является корневым
среди этих нескольких пространств имен, которые
представляют собой набор графических инструментов
Windows Presentation Foundation (WPF)
System.Linq
System.Xml.Linq
System.Data.DataSetExtensions
В этих пространствах имен содержатся типы, применяе­
мые при выполнении программирования с использова­
нием API-интерфейса LINQ
System.Web
Это пространство имен является одним из многих, кото­
рые позволяют создавать веб-приложения ASP.NET
Глава 1. Философия .NET
71
Окончание табл. 1.3
Пространство имен в .NET
Описание
System.ServiceModel
Это пространство имен является одним из многих, кото­
рые позволяется применять для создания распределен­
ных приложений с помощью API-интерфейса Windows
Communication Foundation (WCF)
System.Workflow.Runtime
System.Workflow.Activities
Эти два пространства имен являются главными предста­
вителями многочисленных пространств имен, в которых
содержатся типы, применяемые для построения под­
держивающих рабочие потоки приложений с помощью
API-интерфейса Windows Workflow Foundation (WWF)
System.Threading
System.Threading.Tasks
В этом пространстве имен содержатся многочисленные
типы для построения многопоточных приложений, способ­
ных распределять рабочую нагрузку среди нескольких ЦП.
System.Security
Безопасность является неотъемлемым свойством мира
.NET. В относящихся к безопасности пространствах
имен содержится множество типов, которые позволяют
•иметь дело с разрешениями, криптографической защи­
той и т.д
System.Xml
В этом ориентированном на XML пространстве имен
содержатся многочисленные типы, которые можно при­
менять для взаимодействия с XML-данными
Роль корневого пространства M i c r o s o f t
При изучении перечня, приведенного в табл. 1.3, нетрудно было заметить, что про­
странство имен System является корневым для приличного количества вложенных
пространств имен (таких как System. 10, System. Data и т.д.). Как оказывается, однако,
помимо System в библиотеке базовых классов предлагается еще и ряд других корневых
пространств имен наивысшего уровня, наиболее полезным из которых является про­
странство имен Microsoft.
В любом пространстве имен, которое находится внутри пространства имен
Microsoft (как, например, Microsoft.CSharp, Microsoft.ManagementConsole и
Microsoft.Win32), содержатся типы, применяемые для взаимодействия исключи­
тельно с теми службами, которые свойственны только лишь операционной системе
Windows. Из-за этого не следует предполагать, что данные типы могут с тем же успе­
хом применяться и в других поддерживающих .NET операционных системах вроде Мае
OS X. В настоящей книге детали вложенных в Microsoft пространств имен подробно
рассматриваться не будут, поэтому заинтересованным придется обратиться к докумен­
тации .NET Framework 4.0 SDK.
На заметку! В главе 2 будет показано, как пользоваться документацией .NET Framework 4.0 SDK,
в которой содержатся детальные описания всех пространств имен, типов и членов, встречаю­
щихся в библиотеках базовых классов.
Получение доступа к пространствам имен программным образом
Не помешает снова вспомнить, что пространства имен представляют собой не более
чем удобный способ логической организации взаимосвязанных типов для упрощения
работы с ними. Давайте еще раз обратимся к пространству имен System. С человече­
ской точки зрения System.Console представляет класс по имени Console, который
72
Часть I. Общие сведения о языке C# и платформе .NET
содержится внутри пространства имен под названием System. Но с точки зрения ис­
полняющей .NET это не так. Механизм исполняющей среды видит только одну лишь
сущность по имени System. Console.
В C# ключевое слово using упрощает процесс добавления ссылок на типы, содержащие­
ся в определенном пространстве имен. Вот как оно работает. Предположим, что требуется
создать графическое настольное приложение с использованием API-интерфейса Windows
Forms. В главном окне этого приложения должна визуализироваться гистограмма с ин­
формацией, получаемой из базы данных, и отображаться логотип компании. Поскольку
для изучения типов в каждом пространстве имен требуются время и силы, ниже показаны
некоторые из возможных кандидатов на использование в такой программе:
// Все пространства имен, которые можно использовать
// для создания подобного приложения.
using System;
// Общие типы из библиотеки базовых классов.
using System.Drawing;
// Типы для визуализации графики.
// Типы для создания элементов пользовательского
using System.Windows.Forms;
// интерфейса с помощью Windows Forms.
using System.Data;
// Общие типы для работы с данными.
// Типы для доступа к данным MS SQL Server.
using System.Data.SqlClient;
После указания ряда необходимых пространств имен (и добавления ссылки на сбор­
ки, в которых они находятся), можно свободно создавать экземпляры типов, которые в
них содержатся. Например, при желании создать экземпляр класса Bitmap (определен­
ного в пространстве имен System. Drawing), можно написать следующий код:
// Перечисляем используемые в данном файле
// пространства имен явным образом.
using System;
using System.Drawing;
class Program
public void DisplayLogo ()
{
// Создаем растровое изображение
// размером 20*20 пикселей.
Bitmap companyLogo = new Bitmap(20, 20);
}
}
Благодаря импортированию в этом коде пространства имен System. Drawing, ком­
пилятор сможет определить, что класс Bitmap является членом данного пространства
имен. Если пространство имен System. Drawing не указать, компилятор сообщит об
ошибке. При желании переменные также можно объявлять с использованием полно­
стью уточненного имени:
// Пространство имен System.Drawing здесь не указано!
using System;
class Program
{
public void DisplayLogo ()
{
// Используем полностью уточненное имя.
System.Drawing.Bitmap companyLogo =
new System.Drawing.Bitmap (20, 20);
}
Глава 1. Философия .NET
73
Хотя определение типа с использованием полностью уточненного имени позволяет
делать код более удобным для восприятия, трудно не согласиться с тем, что примене­
ние поддерживаемого в C# ключевого слова using, в свою очередь, позволяет значи­
тельно сократить количество печатаемых знаков. Поэтому в настоящей книге мы будем
стараться избегать использования полностью уточненных имен (если только не будет
возникать необходимости в устранении какой-то очевидной неоднозначности) и стре­
миться пользоваться более простым подходом, т.е. ключевым словом using.
Важно помнить о том, что ключевое слово using является просто сокращенным спо­
собом указания полностью уточненного имени. Поэтому любой из этих подходов приво­
дит к получению одного и ттюго ж е CIL-кода (с учетом того факта, что в CIL-коде всегда
применяются полностью уточненные имена) и не сказывается ни на производительно­
сти, ни на размере сборки.
Добавление ссылок на внешние сборки
Помимо указания пространства имен с помощью поддерживаемого в C# ключевого
слова using, компилятору C# необходимо сообщить имя сборки, в которой содержит­
ся само CIL-onpeделение упоминаемого типа. Как уже отмечалось, многие из ключевых
пространств имен .NET находятся внутри сборки mscorlib.dll.
Класс System.Drawing.Bitmap, однако, содержится в отдельной сборке по имени
System. Drawing.dll. Подавляющее большинство сборок в .NET Framework размеще­
но в специально предназначенном для этого каталоге, который называется глобальным
кэшем сборок (Global Assembly Cache — GAC). На машине Windows по умолчанию GAC
может располагаться внутри каталога %windiг %\Assembly, как показано на рис. 1.6.
Favorites
В
Desktop
Recent Pkces
УЬ Libraries
* Documents
Assembly Name
Version
:ft System Data SqIXml
20.00
-ftSystem Deployment
b03f5f7flld50a3a
:ft System DnectoryServices
2 0.0 0
2J0.0J0
b03f5f7flld50a3a
b03f5f7fl 1d50a3a
rft System. Di 'ectoryServices A - countManagement
3.5.00
Ь77а5с561934е069
:ft System DirectoryServ ices. Protocols
2j0j0.0
b03f5f7flld50a3a
if t System Design
2.0.0.0
b03f5f7flld50a3a
System Drawing-Design
2j0 0 j0
b03f5f7flld50a3a
Videos
lift System EnterpriseServices
aft System EnterpriseServices
2000
2.0 00
b03f5f7flld50a3a
b03f5f7flld50a3a
3.00 0
Ь77а5с5б1934е089
.'ft System IdentityMcdel Selectors
3 0 0 j0
Ь77а5с561934е089
tft System JO lo g
f t System Management
3.000
200 0
b03f5f7fll d50a3a
b03f5f7flld50a3a
sft System ManagementAutorrwtion
1-000
31bf3856ad364e35
Computer
£
Ь77а5с561934е089
Pictures
*4 Hrmegiouf
^
Public Key Token
20 OX)
Music
В
Cut...
Mongo D»we (G)
-ftSystem IdentityModel
CD Drive (F.)
Рис. 1.6. Многие библиотеки .NET размещены в GAC
В зависимости от того, какое средство применяется для разработки приложений
.NET, на выбор может оказываться доступными несколько различных способов для уве­
домления компилятора о том, какие сборки требуется включить в цикл компиляции. Эти
способы подробно рассматриваются в следующей главе, а здесь их детали опущены.
На заметку! С выходом версии .NET 4.0 в Microsoft решили выделить под сборки .NET 4.0 специ­
альное место, находящееся отдельно от каталога С : \Windows\Assembly. Более подробно
об этом будет рассказываться в главе 14.
74
Часть I. Общие сведения о языке C# и платформе .NET
Изучение сборки с помощью утилиты ild a s m .e x e
Тем, кого начинает беспокоить мысль о необходимости освоения всех пространств
имен в .NETT, следует просто вспомнить о том, что уникальным пространство имен
делает то, что в нем содержатся типы, которые как-то связаны между собой с семан­
тической точки зрения. Следовательно, если потребность в создании пользователь­
ского интерфейса, более сложного, чем у простого консольного приложения, отсутст­
вует, можно смело забыть о таких пространствах имен, как System. Windows.Forms,
System.Windows и System.Web (и ряда других), а при создании приложений для ри­
сования — о пространствах имен, которые касаются работы с базами данных. Как и в
случае любого нового набора уже готового кода, опыт приходит с практикой.
Утилита ildasm.exe (Intermediate Language Disassembler — дизассемблер проме­
жуточного языка), которая поставляется в составе пакета .NET Framework 4.0 SDK,
позволяет загружать любую сборку .NET и изучать ее содержимое, в том числе ассо­
циируемый с ней манифест, CIL-код и метаданные типов. По умолчанию эта утилита
установлена в каталоге С : \Program Files\Microsoft SDKs\Windows\v7.0A\bin (если
здесь ее нет, поищите на компьютере файл по имени ildasm.exe).
На заметку! Утилиту ildasm.exe легко запустить, открыв в Visual Studio 2010 окно C om m and
P rom pt (Командная строка), введя в нем слово ildasm и нажав клавишу <Enter>.
После запуска этой утилиты нужно выбрать
в меню File (Файл) команду Open (Открыть) и
найти сборку, которую требуется изучить. Для
целей иллюстрации здесь предполагается, что
нужно изучить сборку Calc.ехе, которая была
сгенерирована на основе приведенного ранее в
этой главе файла Calc, сs (рис. 1.7). Утилита
ildasm.exe представляет структуру любой
сборки в знакомом древовидном формате.
Просмотр CIL-кода
Помимо содержащ ихся в сборке про­
странств имен, типов и членов, утилита
ет просматривать содержащийся внутри
ildasm.ехе также позволяет просматривать и
сборки .NET CIL-код, манифест и метадан­
CIL-инструкции, которые лежат в основе каж­
ные типов
дого конкретного члена. Например, в результа­
те двойного щелчка на методе Main () в классе Program открывается отдельное окно с
CIL-кодом, лежащим в основе этого метода, как показано на рис. 1.8.
Рис. 1.7. Утилита ildasm.exe позволя­
Просмотр метаданных типов
Для просмотра метаданных типов, которые содержатся в загруженной в текущий
момент сборке, необходимо нажать комбинацию клавиш <Ctrl+M>. На рис. 1.9 показаны
метаданные метода Calc.Add ( ) .
Просмотр метаданных сборки (манифеста)
И, наконец, чтобы просмотреть содержимое манифеста сборки, необходимо дважды
щелкнуть на значке MANIFEST (рис. 1.10).
Глава 1. Философия .NET
75
Рис. 1.8. Просмотр лежащего в основе CIL-кода
Рис. 1.9. Просмотр метаданных типов с помощью ild a s m . e x e
Рис. 1.10. Просмотр данных манифеста с помощью i ld a s m . e x e
Несомненно, утилита ild a s m . e x e обладает большим, чем было показано здесь коли­
чеством функциональных возможностей; все остальные функциональные возможности
этой утилиты будут демонстрироваться позже в книге при рассмотрении соответствую­
щих аспектов.
Изучение сборки с помощью утилиты Reflector
Хотя утилита i ld a s m . е х е и применяется очень часто для просмотра деталей дво­
ичного файла .NET, одним из ее недостатков является то, что она позволяет просмат­
ривать только лежащий в основе CIL-код, но не реализацию сборки с использованием
76
Часть I. Общие сведения о языке C# и платформе .NET
предпочитаемого управляемого языка. К счастью, в Интернете для загрузки доступно
множество других утилит для просмотра и декомпиляции объектов .NET, в том числе и
популярная утилита Reflector.
Эта утилита распространяется бесплатно и доступна по адресу http://www.
red-gate.com/products/ref lector. После распаковки из ZIP-архива ее можно за­
пускать и подключать к любой представляющей интерес сборке, выбирая в меню File
(Файл) команду Open (Открыть). На рис. 1.11 показано ее применение на примере при­
ложения Calc.exe.
Рис. 1.11. Утилита Reflector является очень популярной программой для просмотра объектов
Важно обратить внимание на то, что в утилите reflector.exe поддерживается
окно Dissembler (Дизассемблер), которое можно открыть нажатием клавиши пробела, а
также элемент раскрывающегося списка, который позволяет просматривать лежащую в
основе кодовую базу на желаемом языке (разумеется, в том числе и на CIL).
Остальные интри1ую щ ие функциональные возможности этой утилиты предлагается
изучить самостоятельно.
На заметку! Следует иметь в виду, что в остальной части настоящей книги для иллюстрации различ­
ных концепций будет применяться как утилита ildasm.exe, так и утилита reflector.exe.
Поэтому загрузите утилиту Reflector, если это еще не было сделано.
Развертывание исполняющей среды .NET
Нетрудно догадаться, что сборки .NET могут выполняться только на той машине,
на которой установлена платформа .NET Framework. Для разработчиков программного
обеспечения .NET это не должно оказываться проблемой, поскольку их машина надле­
жащим образом конфшурируется еще во время установки распространяемого бесплат­
но пакета NET Framework 4.0 SDK (а также таких коммерческих сред для разработки
.NET-приложений, как Visual Studio 2010).
В случае развертывания сборки на компьютере, на котором платформа .NET не была
установлена, сборка запускаться не будет. Для таких ситуаций Microsoft предлагает спе­
циальный установочный пакет dotNetFx4 0_Full_x8 6.exe, который может бесплатно
Глава 1. Философия .NET
77
поставляться и устанавливаться вместе со специальным программным обеспечением.
Этот пакет доступен для загрузки на сайте Microsoft в общем разделе загружаемых про­
дуктов (http://www.microsof t .com/downloads).
После установки пакета dotNetFx40_Full_x86.exe на целевой машине появятся
необходимые библиотеки базовых классов .NET, исполняющая среда .NET (mscoree.dll)
и дополнительная инфраструктура .NET (такая как GAC).
На заметку! Операционные системы Windows Vista и Windows 7 изначально сконфигурированы с
необходимой инфраструктурой исполняющей среды .NET. В случае развертывания приложения
в среде какой-то другой операционной системы производства Microsoft, например, Windows ХР,
нужно будет позаботиться об установке и настройке на целевой машине среды .NET.
Клиентский профиль исполняющей среды .NET
Установочная программа dotNetFx40_Full_x86.exe имеет объем примерно
77 Мбайт. Если для конечного пользователя обеспечивается возможность развертывать
приложение с компакт-диска, это не будет представлять проблемы, поскольку устано­
вочная программа сможет просто запускать исполняемый файл тогда, когда машина не
сконфигурирована надлежащим образом.
Если пользователь должен будет загружать dotNetFx4 0_Full_x8 6 .ехе по медлен­
ному соединению с Интернетом, ситуация несколько усложняется. Для разрешения по­
добной проблемы в Microsoft разработали альтернативную установочную программу —
так называемый клиентский профиль (dotNetFx4 0_Client_x8 6.exe), который тоже
доступен для бесплатной загрузки на сайте Microsoft.
Эта установочная программа, как не трудно догадаться по ее названию, предусмат­
ривает выполнение установки подмножества библиотек базовых классов .NET в допол­
нение к необходимой инфраструктуре исполняющей среды. Поскольку она имеет го­
раздо меньший размер (примерно 34 Мбайт), установку тех же самых библиотек, что
появляются при полной установке .NET, на целевой машине она не обеспечивает. При
желании не охватываемые ею сборки могут быть добавлены на целевую машину при
выполнении пользователем обновления Windows (с помощью службы Windows Update).
На заметку! Как у полного, так и у клиентского профиля исполняющей среды имеются 64-разрядные аналоги, которые называются, соответственно, dotNetFx40_Full_x86_x64.exe и
dotNetFx40 Client х86 x64.exe.
Не зависящая от платформы природа .NET
В завершение настоящей главы хотелось бы сказать несколько слов о не зависящей
от платформы природе .NET. К удивлению большинства разработчиков, сборки .NET
могут разрабатываться и выполняться в средах операционных систем производства не
Microsoft, в частности — в Mac OS X, различных дистрибутивах Linux, Solaris, а так­
же на устройствах типа iPhone производства Apple (через API-интерфейс MonoTbuch).
Чтобы понять, что делает подобное возможным, необходимо рассмотреть еще одну ис­
пользуемую в мире .NET аббревиатуру — CLI, которая расшифровывается как Common
Language Infrastructure (Общеязыковая инфраструктура).
Вместе с языком программирования C# и платформой .NET в Microsoft был также
разработан набор официальных документов с описанием синтаксиса и семантики язы­
ков C# и CIL, формата сборок .NET, ключевых пространств имен и технических деталей
работы гипотетического механизма исполняющей среды .NET (названного виртуальной
системой выполнения — Virtual Execution System (VES)).
78
Часть I. Общие сведения о языке C# и платформе .NET
Все эти документы были поданы в организацию Ecma International (http: //www.
ecma-international.org) и утверждены в качестве официальных международных
стандартов. Среди них наибольший интерес представляют:
• документ ЕСМА-334, в котором содержится спецификация языка С#;
• документ ЕСМА-335, в котором содержится спецификация общеязыковой инфра­
структуры (CLI).
Важность этих документов становится очевидной с пониманием того факта, что они
предоставляют третьим сторонам возможность создавать дистрибутивы платформы
.NET для любого количества операционных систем и/или процессоров. Среди этих двух
спецификаций документ ЕСМА-335 является более “объемным”, причем настолько, что
был разбит на шесть разделов, которые перечислены в табл. 1.4.
Таблица 1.4. Разделы спецификации CLI
Разделы документа
ЕСМА-335
Предназначение
Раздел 1. Концепции
и архитектура
В этом разделе описана общая архитектура CLI, в том числе правила
CTS и CLS и технические детали функционирования механизма среды
выполнения .NET
Раздел II. Определение
метаданных и семантика
В этом разделе описаны детали метаданных и формат сборок в .NET
Раздел III. Набор
инструкций CIL
В этом разделе описан синтаксис и семантика кода CIL
Раздел IV. Профили
и библиотеки
В этом разделе дается общий обзор тех минимальных и полных биб­
лиотек классов, которые должны поддерживаться в дистрибутиве
.NET
Раздел V.
В этом разделе описан формат обмена деталями отладки
Раздел VI. Дополнения
В этом разделе представлена коллекция дополнительных и более кон­
кретных деталей, таких как указания по проектированию библиотек
классов и детали по реализации компилятора CIL
Следует иметь в виду, что в разделе IV (Профили и библиотеки) описан лишь мини­
мальный набор пространств имен, в которых содержатся ожидаемые от дистрибутива
CLI службы (наподобие коллекций, консольного ввода-вывода, файлового ввода-вывода,
многопоточной обработки, рефлексии, сетевого доступа, ключевых средств защиты и
возможностей для манипулирования XML-данными). Пространства имен, которые упро­
щают разработку веб-приложений (ASP.NET), доступ к базам данных (ADO.NET) и созда­
ние настольных приложений с графическим пользовательским интерфейсом (Windows
Forms/Windows Presentation Fbundation) в CLI не описаны.
Хорошая новость состоит в том, что в главных дистрибутивах .NET библиотеки CLI
дополняются совместимыми с Microsoft эквивалентами ASP.NET, ADO.NET и Windows
Forms, чтобы предоставлять полнофункциональные платформы для разработки при­
ложений производственного уровня. На сегодняшний день популярностью пользуются
две основных реализации CLI (помимо самого предлагаемого Microsoft и рассчитанного
на Windows решения). Хотя настоящая книга и посвящена главным образом созданию
.NET-приложений с помощью поставляемого Microsoft дистрибутива .NET, в табл. 1.5
приведена краткая информация касательно проектов Mono и Portable.NET.
Глава 1. Философия .NET
79
Таблица 1.5. Дистрибутивы .NET, распространяемые с открытым исходным кодом
Дистрибутив
Описание
h t t p ://www.mono-project. com
Проект Mono представляет собой распространяемый
с открытым исходным кодом дистрибутив CLI, который
ориентирован на различные версии Linux (например,
openSuSE, Fedora и т.п.), а также Windows и устройства
Mac OS X и iPhone
h ttp : //www,dotgnu. org
Проект Portable.NET представляет собой еще один
распространяемый с открытым исходным кодом ди­
стрибутив CLI, который может работать в целом ряде
операционных систем. Он нацелен охватывать как мож­
но больше операционных систем (Windows, AIX, BeOS,
Mac OS X, Solaris и все главные дистрибутивы Linux)
Как в Mono, так и в Portable.NET предоставляется ЕСМА-совместимый компиля­
тор С#, механизм исполняющей среды .NET, примеры программного кода, документа­
ция, а также многочисленные инструменты для разработки приложений, которые по
своим функциональным возможностям эквивалентны поставляемым в составе .NET
Framework 4.0 SDK. Более того, Mono и Portable.NET поставляются с компиляторами
VB.NET, Java и С.
На заметку! Описание приемов создания межплатформенных .NET-приложений с помощью Mono
можно найти в приложении Б.
Резюме
Целью этой главы было предоставить базовые теоретические сведения, необходимые
для изучения остального материала настоящей книги. Сначала были рассмотрены ог­
раничения и сложности, которые существовали в технологиях, предшествовавших по­
явлению .NET, а потом показано, как .NET и C# упрощают существующее положение
вещей.
Главную роль в .NET, по сути, играет механизм выполнения (m scoree . d l l ) и библио­
тека базовых классов ( m s c o r l i b . d l l вместе с сопутствующими файлами). Общеязы­
ковая среда выполнения (CLR) способна обслуживать любой двоичный файл .NET (сбор­
ку), который отвечает правилам управляемого программного кода. Как было показано в
этой главе, в каждой сборке (помимо метаданных типов и манифеста) содержатся C1Lинструкции, которые с помощью JIT-компилятора преобразуются в инструкции, ори­
ентированные на конкретную платформу. Помимо этого, здесь рассматривалась роль
общеязыковой спецификации (CLS) и общей системы типов (CTS).
После этого было рассказано о таких полезных утилитах для просмотра объектов,
как ild a s m .e x e и r e f l e c t o r . e x e , а также о том, как сконфигурировать машину для
обслуживания приложений .NET с помощью полного и клиентского профилей. И, нако­
нец, напоследок было вкратце упомянуто о преимуществах не зависящей от платформы
природы C# и .NET, о чем более подробно пойдет речь в приложении Б.
ГЛАВА
2
Создание приложений
на языке C#
рограммисту, использующему язык С#, для разработки .NET-приложений на
выбор доступно много инструментов. Целью этой главы является совершение
краткого обзорного тура по различным доступным средствам для разработки .NET
приложений, в том числе, конечно же, Visual Studio 2010. DiaBa начинается с рассказа
о том, как работать с компилятором командной строки C# (esc . ехе), и самым простей­
шим из всех текстовых редакторов Notepad (Блокнот), который входит в состав опера­
ционной системы Microsoft Windows, а также приложением Notepad++, доступным для
бесплатной загрузки.
Хотя для изучения приведенного в настоящей книге материала вполне хватило бы
компилятора c s c . e x e и простейшего текстового редактора, читателя наверняка за­
интересует использование многофункциональных интегрированных сред разработки
(Integrated Development Environment — IDE). В этой главе также описана бесплатная
IDE-среда с открытым исходным кодом SharpDevelop, предназначенная для разработ­
ки приложений .NET. Как будет показано, по своим функциональным возможностям эта
IDE-среда не уступает многим коммерческим аналогам. Кроме того, в главе кратко рас­
сматривается IDE-среда Visual C# 2010 Express (распространяемая бесплатно), а также
ключевая функциональность Visual Studio 2010.
П
На заметку! В ходе этой главы будут встречаться синтаксические конструкции С#, которые пока
еще не рассматривались. Официальное изучение языка C# начнется в главе 3
Роль комплекта .NET Framework 4 .0 SDK
Одним из мифов в области разработки .NET-приложений является то, что програм­
мистам якобы обязательно требуется приобретать копию Visual Studio для того, что­
бы разрабатывать программы на С#. На самом деле, создавать .NET-программу любого
рода можно с помощью распространяемого бесплатно и доступного для загрузки ком­
плекта инструментов для разработки программного обеспечения .NET Framework 4.0
SDK (Software Development Kit). В этом пакете поставляются многочисленные управляе­
мые компиляторы, утилиты командной строки, примеры кода, библиотеки классов .NET
и полная справочная система.
На заметку! Программа установки .NET Framework 4.0 SDK (dotnet f х4 O f u ll setu p. ехе) доступ­
на на странице загрузки .NET по адресу h t t p : //msdn . m i c r o s o f t . com/netframework.
Глава 2. Создание приложений на языке C#
81
Тем, кто планирует использовать Visual Studio 2010 или Visual C# 2010 Express, сле­
дует иметь в виду, что в установке .NET Framework 4.0 SDK нет никакой необходимости.
При установке любого из упомянутых продуктов этот пакет SDK устанавливается авто­
матически и сразу же предоставляет все необходимое.
Если использование IDE-среды от Microsoft для проработки материала настоящей
книги не планируется, обязательно установите .NET Framework 4.0 SDK, прежде чем
двигаться дальше.
Окно командной строки в Visual Studio 2010
При установке .NET Framework 4.0 SDK, Visual Studio 2010 или Visual C# 2010
Express на локальном жестком диске создается набор новых каталогов, в каждом из
которых содержатся разнообразные инструменты для разработки .NET-приложений.
Многие из этих инструментов работают в режиме командной строки, и чтобы использо­
вать их в любом каталоге, нужно сначала соответствующим образом зарегистрировать
пути к ним в операционной системе.
Для этого можно обновить переменную среды PATH вручную, но лучше пользоваться
предлагаемым в Visual Studio окном командной строки (Command Prompt). Чтобы от­
крыть это окно (рис. 2.1), необходимо выбрать в меню Start (Пуск) пункт All Program s1^
Microsoft Visual Studio 2010*=$Visual Studio Tools (Все программы1^ Microsoft Visual Studio
2010^Инструменты Visual Studio).
Рис. 2.1. Окно командной строки в Visual Studio 2010
Преимущество применения именно этого окна командной строки связано с тем,
что оно уже сконфи1урировано на предоставление доступа к каждому из инструментов
для разработки .NET-приложений. При условии, что на компьютере развернута среда
разработки .NET, можно попробовать ввести следующую команду и нажать клавишу
<Enter>:
C S C
- ?
Если все в порядке, появится список аргументов командной строки, которые мо­
жет принимать работающий в режиме командной строки компилятор C# (esc означает
C-sharp compiler).
Создание приложений на C#
с использованием e s c . е х е
В действительности необходимость в создании крупных приложений с использова­
нием одного лишь компилятора командной строки C# может никогда не возникнуть,
тем не менее, важно понимать в общем, как вручную компилировать файлы кода.
Существует несколько причин, по которым освоение этого процесса может оказаться
полезным.
82
Часть I. Общие сведения о языке C# и платформе .NET
• Самой очевидной причиной является отсутствие Visual Studio 2010 или какой-то
другой графической ШЕ-среды.
• Работа может выполняться в университете, где использование инструментов для
генерации кода и IDE-сред обычно запрещено.
• Планируется применение автоматизированных средств разработки, таких как
m sb u ild .ex e, которые требуют знать опции командной строки для используемых
инструментов.
• Возникло желание углубить свои познания в С#. В графических IDE-средах в ко­
нечном итоге все заканчивается предоставлением компилятору c s c .e x e инструк­
ций относительно того, что следует делать с входными файлами кода С#. В этом
отношении изучение происходящего “за кулисами” позволяет получить необходи­
мые знания.
Еще одно преимущество подхода с использованием одного лишь компилятора
e s c . ехе состоит в том, что он позволяет обрести навыки и чувствовать себя более уве­
ренно при работе с другими инструментами командной строки, входящими в состав
.NET Framework 4.0 SDK. Как будет показано далее, целый ряд важных утилит работает
исключительно в режиме командной строки.
Чтобы посмотреть, как создавать .NET-приложение без IDE-среды, давайте по­
строим с помощью компилятора C# и текстового редактора Notepad простую ис­
полняемую сборку по имени T e s tA p p .e x e . Сначала необходимо подготовить исход­
ный код. Откройте программу Notepad (Блокнот), выбрав в меню Start (Пуск) пункт
All Program s1^ A cce sso rie s1^ Notepad (Все программы1^ Стандартные1^ Блокнот), и введи­
те следующее типичное определение класса на С#:
// Простое приложение на языке С#.
using System;
class TestApp
{
static void Main()
{
Console.WriteLine("Testing! 1, 2, 3");
}
После окончания ввода сохраните файл (например, в каталоге С: \CscExample) под име­
нем T estA p p . cs. Теперь давайте ознакомимся с ключевыми опциями компилятора С#.
На заметку! По соглашению всем файлам с кодом на C# назначается расширение * . cs. Имя фай­
ла не нуждается в специальном отображении на имя какого-либо типа или типов.
Указание целевых входных и выходных параметров
Первым делом важно разобраться с тем, как указывать имя и тип создаваемой сбор­
ки (т.е., например, консольное приложение по имени M y S h e ll.e x e , библиотека кода
по имени M a t h L ib . d ll или приложение Windows Presentation Foundation по имени
Halo8 .ех е). Каждый из возможных вариантов имеет соответствующий флаг, который
нужно передать компилятору esc . ехе в виде параметра командной строки (табл. 2.1).
На заметку! Параметры, передаваемые компилятору командной строки (а также большинству дру­
гих утилит командной строки), могут сопровождаться префиксом в виде символа дефиса (- )
или символа косой черты ( /) .
Глава 2. Создание приложений на языке C#
83
Таблица 2.1. Выходные параметры, которые может принимать компилятор C #
Параметр
Описание
/out
Этот параметр применяется для указания имени создаваемой сбор­
ки По умолчанию сборке присваивается то же имя, что у входного
файла * . с s
/ t a r g e t : ехе
Этот параметр позволяет создавать исполняемое консольное прило­
жение. Сборка такого типа генерируется по умолчанию, потому при
создании подобного приложения данный параметр можно опускать
/ ta rg e t: lib r a r y
Этот параметр позволяет создавать однофайловую сборку * . d l l
/ t a r g e t m odule
Этот параметр позволяет создавать модуль. Модули являются эле­
ментами многофайловых сборок (и будут более подробно рассматри­
ваться в главе 14)
/ t a r g e t : winexe
Хотя приложения с графическим пользовательским интерфейсом
можно создавать с применением параметра / t a r g e t : ехе, параметр
/ t a r g e t : winexe позволяет предотвратить открытие окна консоли
под остальными окнами
Чтобы скомпилировать T es tA p p .cs в консольное приложение T ex tA p p .ex e, перей­
дите в каталог, в котором был сохранен файл исходного кода:
cd C:\CscExample
Введите следующую команду (обратите внимание, что флаги должны обязательно
идти перед именем входных файлов, а не после):
esc /target:exe TestApp.es
Здесь флаг /out не был указан явным образом, поэтому исполняемый файл получит
имя T estA p p . ехе из-за того, что именем входного файла является TestApp. Кроме того,
для почти всех принимаемых компилятором C# флагов поддерживаются сокращенные
версии написания, наподобие /t вместо / t a r g e t (полный список которых можно уви­
деть, введя в командной строке команду esc -?).
esc /t:exe TestApp.es
Более того, поскольку флаг I t : ехе используется компилятором как выходной пара­
метр по умолчанию, скомпилировать T e s tA p p .c s также можно с помощью следующей
простой команды:
esc TestApp.es
Теперь можно попробовать запустить приложение T es tA p p . ехе из командной стро­
ки, введя имя его исполняемого файла, как показано на рис. 2.2.
Рис. 2.2. Приложение
T estA p p
в действии
84
Часть I. Общие сведения о языке C# и платформе .NET
Добавление ссылок на внешние сборки
Давайте посмотрим, как скомпилировать приложение, в котором используются
типы, определенные в отдельной сборке .NET. Если осталось неясным, каким образом
компилятору C# удалось понять ссылку на тип System. Console, вспомните из главы 1,
что во время процесса компиляции происходит автоматическое добавление ссылки на
mscorlib.dll (если по какой-то необычной причине нужно отключить эту функцию,
следует передать компилятору csc.exe параметр /nostdlib).
Модифицируем приложение TestApp так, чтобы в нем открывалось окно сообще­
ния Windows Forms. Для этого откройте файл TestApp. cs и измените его следующим
образом:
using System;
// Добавить эту строку:
using System.Windows.Forms;
class TestApp
{
static void Mai n ()
{
Console .WnteLine ("Testing ! 1,
// И добавить эту строку:
MessageBox.Show("Hello...");
2,
3");
}
}
Обратите внимание на импорт пространства имен System.
Windows.Forms с помощью поддерживаемого в C# ключевого сло­
ва using (о котором рассказывалось в главе 1). Вспомните, что яв­
Рис. 2.3. Первое
приложение
Windows Forms
ное перечисление пространств имен, которые используются внутри
файла * .cs, позволяет избегать необходимости указывать полно­
стью уточненные имена типов.
Далее в командной строке нужно проинформировать компилятор
csc.exe о том, в какой сборке содержатся используемые простран­
ства имен. Поскольку применялся класс MessageBox из пространст­
ва имен System.Windows .Forms, значит, нужно указать компилято­
ру на сборку System. Windows .Forms .dll, что делается с помощью
флага /reference (или его сокращенной версии /г):
esc /г:System.Windows.Forms.dll TestApp.es
Если теперь снова попробовать запустить приложение, то помимо консольного вы­
вода в нем должно появиться еще и окно с сообщением, как показано на рис. 2.3.
Добавление ссылок на несколько внешних сборок
Кстати, как поступить, когда необходимо указать csc.exe несколько внешних сбо­
рок? Для этого нужно просто перечислить все сборки через точку с запятой. В рассмат­
риваемом примере ссылаться на несколько сборок не требуется, но ниже приведена ко­
манда, которая иллюстрирует перечисление множества сборок:
esc /г:System.Windows.Forms.dll;System.Drawing.dll *.cs
На заметку! Как будет показано позже в настоящей главе, компилятор C# автоматически добав­
ляет ссылки на ряд ключевых сборок .NET (таких как System.Windows .Forms .dll), даже
если они не указаны с помощью флага / г .
Глава 2. Создание приложений на языке C#
85
Компиляция нескольких файлов исходного кода
В текущем примере приложение TestApp.ехе создавалось с использованием единст­
венного файла исходного кода * . cs. Хотя определять все типы .NET в одном файле * . cs
вполне допустимо, в большинстве случаев проекты формируются из нескольких файлов
* .cs для придания кодовой базе большей гибкости. Чтобы стало понятнее, давайте соз­
дадим новый класс и сохраним его в отдельном файле по имени HelloMsg.cs.
// Класс HelloMessage
using System;
using System.Windows.Forms;
class HelloMessage
{
public void Speak()
{
MessageBox.Show("Hello
}
}
Изменим исходный класс TestApp так, чтобы в нем использовался класс этого ново­
го типа, и закомментируем прежнюю логику Windows Forms:
using System;
// Эта строка больше не нужна:
// using System.Windows. Forms;
class TestApp
{
static void Main ()
{
Console.WriteLine("Testing 1 1, 2, 3”);
// Эта строка тоже больше не нужна:
// MessageBox.Show( ”H e llo .. . ”) ;
// Используем класс HelloMessage:
HelloMessage h = new HelloMessage();
h .Speak();
}
}
Чтобы скомпилировать файлы исходного кода на C# , необходимо их явно перечис­
лить как входные файлы:
esc /г:System.Windows.Forms.dll TestApp.es HelloMsg.es
В качестве альтернативного варианта компилятор C# позволяет использовать груп­
повой символ (*) для включения в текущую сборку всех файлов * . cs, которые содержат­
ся в каталоге проекта:
esc /г:System.Windows.Forms.dll *.cs
Вывод, получаемый после запуска этой программы, идентичен предыдущей про­
грамме. Единственное отличие между этими двумя приложениями связано с разнесе­
нием логики по нескольким файлам.
Работа с ответными файлами в C#
Как не трудно догадаться, для создания сложного приложения C# из командной
строки потребовалось бы вводить утомительное количество входных параметров для
уведомления компилятора о том, как он должен обрабатывать исходный код. Для облег­
86
Часть I. Общие сведения о языке C# и платформе .NET
чения этой задачи в компиляторе C# поддерживается использование так называемых
ответных файлов (response files).
В ответных файлах C# размещаются все инструкции, которые должны использо­
ваться в процессе компиляции текущей сборки. По соглашению эти файлы имеют рас­
ширение * .rsp (сокращение от response — ответ). Чтобы посмотреть на них в действии,
давайте создадим ответный файл по имени TestApp.rsp, содержащий следующие аргу­
менты (комментарии в данном случае обозначаются символом #):
# Это ответный файл для примера
# TestApp.exe ив главы 2.
# Ссылки на внешние сборки:
/г:System.Windows.Forms.dll
# Параметры вывода и подлежащие компиляции файлы
# (здесь используется групповой символ):
/target:ехе /outrTestApp.exe *.cs
Теперь при условии сохранения данного файла в том же каталоге, где находятся под­
лежащие компиляции файлы исходного кода на С#, все приложение можно будет соз­
дать следующим образом (обратите внимание на применение символа @):
esc @TestApp.rsp
В случае необходимости допускается также указывать и несколько ответных
*.rsp файлов в качестве входных параметров (например, esc @FirstFile.rsp
@SecondFile .rsp @ThirdFile .rsp). При таком подходе, однако, следует иметь в
виду, что компилятор обрабатывает параметры команд по мере их поступления.
Следовательно, аргументы командной строки, содержащиеся в поступающем позже
файле * . rsp, могут переопределять параметры из предыдущего ответного файла.
Еще важно обратить внимание на то, что все флаги, перечисляемые явным образом
перед ответным файлом, будут переопределяться настройками, которые содержатся в
этом файле. То есть в случае ввода следующей команды:
esc /out:MyCoolApp.ехе @TestApp.rsp
имя сборки будет по-прежнему выглядеть как TestApp.ехе (а не MyCoolApp.ехе) из-за
того, что в ответном файле TestApp. rsp содержится флаг /outiTestApp.exe. В слу­
чае перечисления флагов после ответного файла они будут переопределять настройки,
содержащиеся в этом файле.
На заметку! Действие флага /reference является кумулятивным. Где бы не указывались внеш­
ние сборки (перед, после или внутри ответного файла), в конечном итоге каждая из них все
равно будет добавляться к остальным.
Используемы й по умолчанию ответный файл (e s c . r s p )
Последним моментом, связанным с ответными файлами, о котором необходимо упо­
мянуть, является то, что с компилятором C# ассоциирован ответный файл с sc. rsp,
который используется по умолчанию и размещен в том же самом каталоге, что и файл
esc .ехе (обычно это С :\Windows\Microsof t .NET\Framework\<BepcMH>, где на месте
элемента <версия> идет номер конкретной версии платформы). Открыв файл esc.rsp
в программе Notepad (Блокнот), можно увидеть, что в нем с помощью флага /г : указано
множество сборок .NET, в том числе различные библиотеки для разработки веб-приложений, программирования с использованием технологии LINQ и обеспечения доступа к
данным и прочие ключевые библиотеки (помимо, конечно же, самой главной библиоте­
ки mscorlib.dll).
Глава 2. Создание приложений на языке C#
87
При создании программ на C# с применением esc . ехе ссылка на этот ответный файл
добавляется автоматически, даже когда указан специальный файл * . rsp. Из-за нали­
чия такого ответного файла по умолчанию, рассматриваемое приложение T e s tA p p . ехе
можно скомпилировать и помощью следующей команды (поскольку в e s c . rsp уже со­
держится ссылка на System . Windows . Forms . d l l ) :
esc /outiTestApp.exe *.cr.
Для отключения функции автоматического чтения файла e s c . rsp укажите опцию
/ n ocon fig:
esc @TestApp.rsp /noconfig
На заметку! В случае добавления (с помощью опции / г) ссылок на сборки, которые на самом деле
не используются, компилятор их проигнорирует Поэтому беспокоиться по поводу “разбухания
кода" не нужно.
Понятно, что у компилятора командной строки C# имеется множество других пара­
метров, которые можно применять для управления генерацией результирующей сборки
.NET. Другие важные возможности будут демонстрироваться по мере необходимости да­
лее в книге, а полные сведения об этих параметрах можно всегда найти в документации
.NET Framework 4.0 SDK
Исходный код. Код приложения CscExample доступен в подкаталоге Chapter 2.
Создание приложений .NET
с использованием Notepad++
Еще одним текстовым редактором, о котором следует кратко упомянуть, является
распространяемое с открытым исходным кодом бесплатное приложение Notepad++.
Загрузить его можно по адресу http://not epad-plus .sou гсе forge .net/. В отличие
от простого редактора Notepad (Блокнот), поставляемого в составе Windows, приложе­
ние Notepad++ позволяет создавать код на множестве различных языков и поддержи­
вает установку разнообразных дополнительных подключаемых модулей. Помимо этого,
Notepad++ обладает рядом других замечательных достоинств, в том числе:
• изначальной поддержкой для использования ключевых слов C# (и их кодирования
цветом включительно);
• поддержкой для свертывания синтаксиса (syntax folding), позволяющей свора­
чивать и разворачивать группы операторов в коде внутри редактора (и подобной
той, что предлагается в Visual Studio 2010/C# 2010 Express);
• возможностью увеличивать и уменьшать масштаб отображения текста с помо­
щью колесика мыши (имитирующего действие клавший <Ctrl>);
• настраиваемой функцией автоматического завершения (autocompletion) различ­
ных ключевых слов C# и названий пространств имен .NET.
Чтобы активизировать поддержку функции автоматического завершения кода на C#
(рис. 2.4), необходимо одновременно нажать клавиши <Ctrl> и пробела.
На заметку! Список вариантов, предлагаемых для автоматического завершения кода в отображае­
мом окне, можно изменять и расширять. Для этого необходимо открыть файл С : \ Program
Files\Notepad+ +\plugins\APIs\cs .xml для редактирования и добавить в него любые
дополнительные записи.
88
Часть I. Общие сведения о языке C# и платформе .NET
Рис. 2.4. Использование функции автоматического завершения кода в Notepad++
Более подробно о приложении Notepad++ здесь рассказываться не будет. Чтобы узнать
о нем больше, воспользуйтесь предлагаемым в его меню ? пунктом Help (Справка).
Создание приложений.NET
с помощью SharpDevelop
Нельзя не согласиться с тем, что написание кода C# в приложении Notepad++, несо­
мненно, является шагом в правильном направлении по сравнению с использованием
редактора Notepad (Блокнот) и командной строки. Тем не менее, в Notepad++ отсутству­
ют богатые возможности IntelliSense, визуальные конструкторы для построения графи­
ческих пользовательских интерфейсов, шаблоны проектов, инструменты для работы с
базами данных и многое другое. Для удовлетворения перечисленных потребностей боль­
ше подходит такой рассматриваемый далее продукт для разработки .NET-приложений,
как SharpDevelop (также называемый #Develop).
Продукт SharpDevelop представляет собой распространяемую с открытым исходным
кодом многофункциональную IDE-среду, которую можно применять для создания .NETсборок с помощью C# ,VB, CIL, а также Python-образного .NET-языка под названием
Воо. Помимо того, что эта IDE-среда предлагается совершенно бесплатно, интересно
обратить внимание на тот факт, что она сама реализована полностью на С#. Для ус­
тановки среды SharpDevelop необходимо загрузить и скомпилировать ее файлы * . cs
вручную или запустить готовую программу setup.ехе. Оба дистрибутива доступны по
адресу http://www.sharpdevelop.сот/.
IDE-среда SharpDevelop обладает массой достоинств в плане улучшения продуктив­
ности. Наиболее важными из них являются:
• поддержка для множества языков и типов проектов .NET;
• функция IntelliSense, завершение кода и возможность использования только оп­
ределенных фрагментов кода;
• диалоговое окно Add Reference (Добавление ссылки), позволяющее легко добав­
лять ссылки на внешние сборки, в том числе и те, что находятся в глобальном
кэше сборок (Global Assembly Cache — GAC);
• визуальный конструктор Windows Forms;
• встроенные утилиты для просмотра объектов и определения кода;
• визуальные утилиты для проектирования баз данных;
• утилита для преобразования кода на C# в код на VB (и наоборот).
Глава 2. Создание приложений на языке C#
89
Впечатляюще для бесплатной IDE-среды, не так ли? Ниже кратко рассматриваются
некоторые наиболее интересные достоинства из перечисленных выше.
На заметку! На момент написания книги в текущей версии SharpDevelop пока не поддерживались
средства C# 2010 / .NET 4.0. Периодически заглядывайте на веб-сайт SharpDevelop, чтобы про­
верить, не появились ли следующие выпуски среды.
Создание простого тестового проекта
После установки SharpDevelop за счет выбора в меню File (Файл) пункта New1^Solution
(Создать1
^ Решение) можно указывать, какой тип проекта требуется сгенерировать
(и на каком языке .NETT). Например, предположим, что нужно создать проект по имени
MySDWinApp типа Windows Application (Приложение Windows) на языке C# (рис. 2.5).
Рис. 2.5. Диалоговое окно создания нового проекта в SharpDevelop
Как и в Visual Studio, в SharpDevelop предлагается окно элементов управления для
конструктора графических пользовательских интерфейсов Windows Forms (позволяющее
перетаскивать элементы управления на поверхность конструктора) и окно Properties
(Свойства), позволяющее настраивать внешний вид и поведение каждого из элементов
графического пользовательского интерфейса. На рис. 2.6 показан пример настройки
элемента управления Button (Кнопка); обратите внимание, что для этого был выпол­
нен щелчок на вкладке Design (Конструктор), отображаемой в нижней части открытого
файла кода.
После щелчка на вкладке Source (Исходный код) в нижней части окна конструктора
форм, как не трудно догадаться, будет предлагаться функция IntelliSense, функция за­
вершения кода и встроенная справка (рис. 2.7).
В дизайне SharpDevelop имитируются многие функциональные возможности, ко­
торые предоставляются в IDE-средах .NET производства Microsoft (и о которых пойдет
речь далее). Поэтому более подробно здесь эта IDE-среда SharpDevelop рассматривать­
ся не будет. Для получения дополнительной информации можно воспользоваться меню
Help (Справка).
90
Часть I. Общие сведения о языке C# и платформе NET
Рис. 2.6. Конструирование приложения типа Windows Forms графическим образом в SharpDevelop
MainForm.cs" |
1 ■«’jjMySDWinopp MamForm
t h is |
rtf AliowTransparerc>
ibl c v rtual bool AutoS crol
o* sets a value indurating whether the loim enables autovcrofcng
|
Apply AutoScaimfl
3
J *AotoSca(e
-ute Sc at eBase& ze
AotoScaleDi mens ion ?
j J f AutoScal eF artor
J*AjtGScaleMode
Soo-ce
D e»pi
Рис. 2.7. В SharpDevelop поддерживается много утилит для генерации кода
Создание приложений .NET с использованием
Visual C# 2010 Express
Летом 2004 г. компания Microsoft представила совершенно новую линейку ЮЕ-сред
по общим названием “Express” (h tt p : //m sdn .m icrosoft.com / express). На сегодняш­
ний день на рынке предлагается несколько членов этого семейства (все они распростра­
няются бесплатно и поддерживаются и обслуживаются компанией Microsoft).
• Visual Web Developer 2010 Express. “Облегченный” инструмент для разработки ди­
намических веб-сайтов ASP.NET и служб WCF.
•
Visual Basic 2010 Express. Упрощенный инструмент для программирования, иде­
ально подходящий для программистов-новичков, которые хотят научиться соз­
давать приложения с применением дружественного к пользователям синтаксиса
Visual Basic.
Глава 2. Создание приложений на языке C#
91
• Visual C# 2010 Express и Visual C++ 2010 Express. IDE-среды, ориентированные
специально на студентов и всех прочих желающих обучиться основам програм­
мирования с использованием предпочитаемого синтаксиса.
• SQL Server Express. Система для управления базами данных начального уровня,
предназначенная для любителей, энтузиастов и учащихся-разработчиков.
Некоторые уникальные функциональные
возможности Visual C# 2010 Express
В целом продукты линейки Express представляют собой усеченные версии своих
полнофункциональных аналогов в линейке Visual Studio 2010 и ориентированы глав­
ным образом на любителей .NET и занимающихся изучением .NET студентов. Как и в
SharpDevelop, в Visual C# 2010 Express предлагаются разнообразные инструменты для
просмотра объектов, визуальный конструктор Windows Fbrms, диалоговое окно Add
R e fe ren ces (Добавление ссылок), возможности IntelliSense и шаблоны для расширения
программного кода.
Помимо этого в Visual C# 2010 Express доступно несколько (важных) функциональ­
ных возможностей, которые в SharpDevelop в настоящее время отсутствуют. К их числу
относятся:
• развитая поддержка для создания приложений Window Presentation Foundation
(WPF) с помощью XAML;
• функция IntelliSense для новых синтаксических конструкций C# 2010, в том числе
именованных аргументов и необязательных параметров;
• возможность загружать дополнительные шаблоны, позволяющие разрабатывать
приложения ХЬох 360, приложения WPF с интеграцией TWitter, и многое другое.
На рис. 2.8 показан пример, иллюстрирующий создание с помощью Visual C# Express
XAML-разметки для проекта WPF.
Common WPF Controls
. АО WPF Controls
J Solution V/pfApp&cBtionl (1 prorecV;
a
3 Wpl Application 1
>lt
ad
*
не
There ire no usable controls in this group
Drag in item onto this text to add it to the
U design
U
в XAML
Indoto • C la s s -“W p fA p p lic B tio n l M Л
№ *J n s «"h ttp ;//s ch o »a s re ic r t
x n b -.: x » " h ttp : //s c h e * a s .i» i; ; « f
i itii'.-H a ln W in d o w " Heigh О '-» ,
cOrld>
Properties
Rele ences
App-xaml
M a n W n d o v jam l
Calendar
Click
ClickMode
-ц ,
т -
< « u tto n » :*tai»e«=”« y b u tto n "
c/uindoM )
100% - • __
1 Grid Window,'Ond
Рис. 2.8. Visual C# Express обладает встроенной поддержкой API-интерфейсов .NET 4.0
92
Часть I. Общие сведения о языке C# и платформе .NET
Поскольку по внешнему виду и поведению IDE-среда Visual C# 2010 Express очень
похожа на Visual Studio 2010 (и, в некоторой степени, на SharpDevelop), более подробно
она здесь рассматриваться не будет. Ее вполне допустимо использовать для проработ­
ки дальнейшего материала книги, но при этом обязательно следует иметь в виду, что
в ней не поддерживаются шаблоны проектов для создания веб-сайтов ASP. NETT. Чтобы
иметь возможность строить веб-приложения, необходимо загрузить продукт Visual
Web Developer 2010, который также доступен на сайте h t t p : //msdn . m i c r o s o f t . сот/
ex p re ss.
Создание приложений .NET
с использованием Visual Studio 2010
Профессиональные разработчики программного обеспечения .NET наверняка рас­
полагают самым серьезным в этой сфере продуктом производства Microsoft, который
называется Visual Studio 2010 и доступен по адресу http :/ /msdn.microsoft.сот/
vstudio. Э тот продукт представляет собой самую функционально насыщенную и
наиболее приспособленную под использование на предприятиях IDE-среду из всех,
что рассматривались в настоящей главе. Такая мощь, несомненно, имеет свою цену,
которая варьируется в зависимости от версии Visual Studio 2010. Как не трудно дога­
даться, каждая версия поставляется со своим уникальным набором функциональных
возможностей.
На заметку! Количество версий в семействе продуктов Visual Studio 2010 очень велико. В остав­
шейся части книги предполагается, что в качестве предпочитаемой IDE-среды используется
версия Visual Studio 2010 Professional.
Хотя далее предполагается наличие копии Visual Studio 2010 Professional, это вовсе
не обязательно для проработки излагаемого в настоящей книге материала. В худшем
случае может встретиться описание опции, которая отсутствует в используемой IDEсреде. Однако весь приведенный в книге код будет прекрасно компилироваться, какой
бы инструмент не применялся.
На заметку! После загрузки кода для настоящей книги можно открывать любой пример в Visual
Studio 2010 (или Visual C# 2010 Express), дважды щелкая на соответствующем файле решения
* . s in . Если на машине не установлено ни Visual Studio 2010, ни С#20010 Express, файлы * . cs
потребуется вставлять вручную в рабочую область проекта внутри используемой ЮЕ-среды.
Некоторые уникальные функциональные
возможности Visual Studio 2010
Как не трудно догадаться, Visual Studio 2010 также поставляется с графическими
конструкторами, поддержкой использования отдельных фрагментов кода, средствами
для работы с базами данных, утилитами для просмотра объектов и проектов, а так­
же встроенной справочной системой. Но, в отличие от многих из уже рассмотренных
IDE-сред, в Visual Studio 2010 предлагается множество дополнительных возможностей,
наиболее важные из которых перечислены ниже:
• графические редакторы и конструкторы XML;
• поддержка разработки программ Windows, ориентированных на мобильные
устройства;
Глава 2. Создание приложений на языке C#
93
• поддержка разработки программ Microsoft Office;
• поддержка разработки проектов Windows Workflow Foundation;
• встроенная поддержка рефакторинга кода;
• инструменты визуального конструирования классов.
По правде говоря, в Visual Studio 2010 предлагается настолько много возможностей,
что для их полного описания понадобилась бы отдельная книга. В настоящей книге та­
кие цели не преследуются. Тем не менее, в нескольких следующих разделах наиболее
важные функциональные возможности рассматриваются чуть подробнее, а другие по
мере необходимости будут описаны далее в книге.
Ориентирование на .NET Framework в диалоговом окне New Project
Те, кто следует указаниям этой главы, сейчас могут попробовать создать новое кон­
сольное приложение на C# (по имени Vs2010Exam ple), выбрав в меню File (Файл) пункт
N e w ^ P ro je c t (Создать1^Проект). Как можно увидеть на рис. 2.9, в Visual Studio 2010
поддерживается возможность выбора версии .NET Framework (2.0, 3.x или 4.0), для ко­
торой должно создаваться приложение, с помощью раскрывающегося списка, отобра­
жаемого в правом верхнем углу диалогового окна N ew P ro je c t (Новый проект). Для всех
описываемых в настоящей книге проектов, в этом списке можно оставлять выбранным
предлагаемый по умолчанию вариант .NET F ra m e w o rk 4.0.
MET Framework 4
* j : • f l i |j Search Installed Tem plate.
^N^^ramew^lTo
■ Visual С *
NET Framework 3.0
NET Framework ЗД
Д
[plication
Visual C *
Type: Visual C *
j
A project for creating a com m and-li
application
W indows
Visual C *
Web
Office
Cloud Service
Reporting
SharePoint
jf i
WPF Application
Visual C *
WPF Browser Application
Visual C *
Console Application
Visual C *
WPF Custom Control Library
Visual C *
Empty Project
Visual C *
|]
1И
if
SiUerlight
Test
WCF
W oikflow
.
Other Languages
C|l |
le cunentfy not aUcwed to load
Nam e
location
Vs2010Example
c .user, ar drew troel.en doc u m e n t'1v sual studio lONProjects
1 VivO lOlcomple_______________________________________________
Create tfcrectory For solution
Add to source control
Рис. 2.9. В Visual Studio 2010 позволяется выбирать определенную целевую
версию .NET Framework
Использование утилиты Solution Explorer
Утилита Solution Explorer (Проводник решений), доступная через меню View (Вид),
позволяет просматривать набор всех файлов с содержимым и ссылаемых сборок, кото­
рые входят в состав текущего проекта (рис. 2.10).
Обратите внимание, что внутри папки R e fe re n ce s (Ссылки) в окне Solution Explorer
отображается список всех сборок, на которые в проекте были добавлены ссылки. В за­
висимости от типа выбираемого проекта и целевой версии .NET Framework, этот список
выглядит по-разному.
94
Часть I. Общие сведения о языке C# и платформе .NET
-
S o lu tio n E x p lo r e r
Ck.
□ X
3
>>
* 3 S o lu tio n 'V s 2 0 1 0 E x a m p le Д p r o je c t ;
л
^
V s 2 0 1 0 E x a m p le
^
л
P r o p e r tie s
R e fe r e n c e s
•C J M ic r o s o f t.C S H a r p
4 J S y s te m
■Л S y s t e m .C e r e
4 J S y s t e m .D a t a
- О S y s t e m .D a t a D a ta S e tE x te n s io n s
-O
S y s t e m .X m l
-_J S y s t e m .X m l.L m q
^
P r o g r a m .e s
Рис. 2.10. Окно утилиты Solution Explorer
Д обавление ссы лок на внешние сборки
Если необходимо сослаться на дополнительные сборки, щелкните напалке R e fe ren ces
правой кнопкой мыши и выберите в контекстном меню пункт A dd R e fe ren ce (Добавить
ссылку). После этого откроется диалоговое окно, позволяющее выбрать желаемые сборки
(в Visual Studio это аналог параметра /reference в компиляторе командной строки). На
вкладке .NET этого окна, показанной на рис. 2.11, отображается список наиболее час­
то используемых сборок .NET; на вкладке B ro w se (Обзор) предоставляется возможность
найти сборки .NET, которые находятся на жестком диске; на вкладке R e cen t (Недавние)
приводится перечень сборок, на которые часто добавлялись ссылки в других проектах.
A d a R e fe re n c e
I NET
^CO M
J P ro jects f B ro w s * | R ecen t
Com ponent Nam e
Version
R u n tim e
P ath
S y s te m .ld e n tity M o d e l
4 0 .0 .0
v4.0,21006
C :> P ro g ram
S ystem .Id e n tity M o d e l. Sele
4 0 .0 0
Л 021306
C : \P ie g r a m
S ystem . M a n a g e m e n t
4.023.0
4*4.0.21006
C A P ro g ra m
4 0 .0 .0
v 4 .0 2 1 006
C A P ro g ra m
S y s te m .M e s s a g in g
4 .0 Д 0
v 4 .0.21006
C A P ro g ra m
I S v s te m .M a n a g e m e n tln s tr
S y s te m .N e t
4.О .0Л
Л 0.21 0 0 6
C A P ro g ra m
S ystem . N u m e ric s
4.0JD.0
4*4X1.21006
C A P ro g ra m
S y s te m .P rin tin g
4.043.0
v 4 .0.21006
C A P ro g ra m
S y s te m .R u n tim e
4 0 .0 .0
v 4 .0 2 1 0 0 6
C A P ro g ra m
■ S ystem P u n tim e .R e m c tin g
4 .0 0 .0
v*4.0.2 1 0 0 6
C A P ro g ra m
1 S ystem R u n tim e S e n a liia ti..
4.0.0.0
Л , 0.2 1 0 0 6
C : \P to g r a m
Рис. 2.11. Диалоговое окно A dd R e fe ren ce
Просмотр свойств проекта
И, наконец, напоследок важно обратить внимание на наличие в окне утилиты
Solution Explorer пиктограммы P ro p e rtie s (Свойства). Двойной щелчок на ней приво­
дит к открытию редактора конфигурации проекта, окно которого называется P ro je ct
P ro p e rtie s (Свойства проекта) и показано на рис. 2.12.
Глава 2. Создание приложений на языке C#
95
Рис. 2.12. Окно P ro je c t P ro p e rtie s
Различные возможности, доступные в окне P ro je c t P ro p e rtie s , более подробно рас­
сматриваются по ходу данной книги. В этом окне можно устанавливать различные
параметры безопасности, назначать сборке надежное имя, развертывать приложение,
вставлять необходимые для приложения ресурсы и конфигурировать события, которые
должны происходить перед и после компиляции сборки.
Утилита Class View
Следующей утилитой, с которой необходимо
познакомиться, является Class View (Просмотр
классов), доступ к которой тоже можно получать
через меню View. Эта утилита позволяет просмат­
- a
V s 2 0 1 0 E )u m p te
&
P ro je c t R eferen ces
ривать все типы, которые присутствуют в теку­
i { } V s 2 0 1 0 E x a m p le
щем проекте, с объектно-ориентированной точки
л
P ro g ra m
* ;__j Base T y p e s
зрения (а не с точки зрения файлов, как это позво­
*1
ляет делать утилита Solution Explorer). В верхней
панели утилиты Class View отображается список
f * - O b je c tO
пространств имен и их типов, а в нижней пане­
♦ E q u a ls (o b je c t, o b je c t)
□
ли — члены выбранного в текущий момент типа,
• E q u a ls (o b je c t)
1
как показано на рис. 2.13.
♦ G e tH a s h C o d e O
♦ G e tT y p e O
В результате двойного щелчка на типе или члене
4 * M e m b e rv w s e C lo n e O
..................................J
типа в окне утилиты Class View в Visual Studio бу­
дет автоматически открываться соответствующий Рис. 2.13. Окно утилиты Class View
файл кода С#, с размещением курсора мыши на
соответствующем месте. Еще одной замечательной функциональностью утилиты Class
View в Visual Studio 2010 является возможность открывать любую ссылаемую сборку и
просматривать содержащиеся внутри нее пространства имен, типы и члены (рис. 2.14).
Утилита Object Browser
В Visual Studio 2010 доступна еще одна утилита для изучения множества сборок, на
которые имеются ссылки в текущем проекте. Называется эта утилита Object Browser
(Браузер объектов) и получить к ней доступ можно, опять-таки, через меню View.
После открытия ее окна останется просто выбрать сборку, которую требуется изучить
(рис. 2.15).
96
Часть I. Общие сведения о языке C# и платформе .NET
4
V s20 10 E x a m p le
J
P ro je c t R eferences
•J
M ic ro s o ft.C S h a rp
* ч э m sc o rlib
( } M ic ro s o ft.W in 3 2
{ } M ic ro s o ft.W in 3 2 .S s fe H fln d le s
л О
>
System
A c tio n
«L
A c c e s s V io la tio n E x c e p tio n (S y s te m .R u n tim e .S e rie liia tio n .S e ria ln a tio n In fo ,
•
A ccessV io la tio n E x c e p tio n (s tn n g , S ystem .E xception!
V A ccess V io la tio n E x c e p tio n (s tn n g )
•
A c c e ssV io latio n E xce p tio n Q
Class V ie w
J •5? Solutaon Expkiiff
Рис. 2.14. Утилита Class View может также применяться
для просмотра ссылаемых сборок
Рис. 2.15. Окно утилиты Object Browser
Встроенная поддержка рефакторинга программного кода
Одной из главных функциональных возможностей Visual Studio 2010 является встро­
енная поддержка для проведения рефакторинга существующего кода. Если объяснять
упрощенно, то под рефакторингом (refactoring) подразумевается формальный механи­
ческий процесс улучшения существующего кода. В прежние времена рефакторинг тре­
бовал приложения массы ручных усилий. К счастью, теперь в Visual Studio 2010 можно
достаточно хорошо автоматизировать этот процесс.
За счет использования меню R e fa c to r (Рефакторинг), которое становится доступным
при открытом файле кода, а также соответствующих клавиатурных комбинаций быст­
рого вызова, смарт-тегов (smart tags) и/или вызывающих контекстные меню щелчков,
можно существенно видоизменять код с минимальным объемом усилий. В табл. 2.2
перечислены некоторые наиболее распространенные приемы рефакторинга, которые
распознаются в Visual Studio 2010.
Глава 2. Создание приложений на языке C#
97
Таблица 2.2. Приемы рефакторинга, поддерживаемые в Visual Studio 2010
Прием рефакторинга
Описание
Extract Method
(Извлечение метода)
Позволяет определять новый метод на основе выбирае­
мых операторов программного кода
Encapsulate Field
(Инкапсуляция поля)
Позволяет превращать общедоступное поле в приватное,
инкапсулированное в форму свойство C#
Extract Interface
(Извлечение интерфейса)
Позволяет определять новый тип интерфейса на основе
набора существующих членов типа
Reorder Parameters
(Переупорядочивание параметров)
Позволяет изменять порядок следования аргументов в
члене
Remove Parameters
(Удаление параметров)
Позволяет удалять определенный аргумент из текущего
списка параметров
Rename
(Переименование)
Позволяет переименовывать используемый в коде метод,
поле, локальную переменную и т.д. по всему проекту
Чтобы увидеть процесс рефакторинга в действии, давайте модифицируем метод
М а ш (), добавив в него следующий код:
static void Main(string[] args)
{
// Настройка консольного интерфейса (C U I).
Console.Title = "My Rocking App";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.BackgroundColor = ConsoleColor.Blue;
Console WnteLine ("***************************************") •
Console. WnteLine ("***** Welcome to My Rocking App! *******");
Console WriteLine ( " * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * " ) •
Console.BackgroundColor = ConsoleColor.Black;
// Ожидание нажатия клавиши <Enter>.
Console.ReadLine() ;
}
В таком, как он есть виде, в этом коде нет ничего неправильного, но давайте пред­
ставим, что возникло желание сделать так, чтобы данное приветственное сообщение
отображалось в различных местах по всей программе. В идеале вместо того, чтобы за­
ново вводить ту же самую отвечающую за настройку консольного интерфейса логику,
было бы неплохо иметь вспомогательную функцию, которую можно было бы вызывать
для решения этой задачи. С учетом этого, попробуем применить к существующему коду
прием рефакторинга E x tra c t M e th o d (Извлечение метода).
Для этого выделите в окне редактора все содержащиеся внутри Main () операто­
ры, кроме последнего вызова Console.ReadLine ( ) , и щелкните на выделенном коде
правой кнопкой мыши. Выберите в контекстном меню пункт R e fa c to r ^ E x tra c t M e th o d
(Рефакторинг1
^Извлечь метод), как показано на рис. 2.16.
В открывшемся далее окне назначьте новому методу имя ConfigureCUI () . После
этого метод Main() станет вызывать новый только что сгенерированный метод
ConfigureCUI (), внутри которого будет содержаться выделенный ранее код:
class Program
{
static void Main(string[] args)
{
ConfigureCUI ();
// Ожидание нажатия клавиши <Enter>.
98
Часть I. Общие сведения о языке C# и платформе .NET
Console.ReadLine();
}
private static void ConfigureCUl()
{
//
Н астрой ка к о н сольн ого и н терф ей са
(C U I).
Console.Title = "My Rocking App";
Console.Title = "Мое приложение";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.BackgroundColor = ConsoleColor.Blue;
Console.WriteLine("***************************************" );
Console.WnteLine ("**** * Welcome to My Rocking App! *******") •
Console WnteLine ^*********** *****************•»<•************ ««j .
Console.BackgroundColor = ConsoleColor.Black;
}
}
Этот лиш ь один простой пример применения предлагаемых в Visual Studio 2010
встроенных возможностей для рефакторинга, и по ходу настоящей книги будет встре­
чаться еще немало подобных примеров.
*i
^JVsZOlGExample.Program
^*Wain{string[] args)
*
static void H ain (vtrin gf] args)
{
. f t up C o n s o le U I (C U ] )
• . T i t l e - "My R o c k iri
. F o re g ro u n d C o lo r - . B a c k g ro u n d C o lo r » C j
• * . W r it e L in e ( " * * * •* * “
o l e . W r i t e L i n e ( " ' *•** w
o le . W r it e L in e (" * * * " * • *
1 . B a c k g ro u n d C o lo r * J
la it f o r t n t f r ke> to b
. R e a d L in e O ;
Refactor
s
Organize Usings
a
</
Renam e..
F2
&
Extract Method...
C trkR ,M
Encapsulate Field-
Ctrl+R, E
Extract Interface.
Ctrl-» R, J
Remove Parameters...
Ctrl-» R. V
Reorder Parameters...
O ik ^ O
Create Unit Test*...
Generate Sequence Diagram...
Insert Snippet..
Ctrl-*-ic x
Surround With...
C trM C S
Go To Definition
F12
Find All References
Ctrl+K, R
View Call Hierarchy
C trkK. T
a.b
Breakpoint
41
Run T о Cursor
Ctrl+F10
A
Cut
C txbX
Am
Copy
C tr k C
Л
Paste
Ctrl* У
Outlining
Рис. 2.16. Активизация рефакторинга кода
Возможности для расширения и окружения кода
В Visual Studio 2010 (а также в Visual C# 2010 Express) можно вставлять готовые
блоки кода C# выбором соответствующих пунктов в меню, вызовом контекстных меню
по щелчку правой кнопкой мыши и/или использованием соответствующих клавиатур­
ных комбинаций быстрого вызова. Число доступных шаблонов для расширения кода
впечатляет. В целом их можно поделить на две основных группы.
• Шаблоны для вставки фрагментов кода (code snippet). Эти шаблоны позволяют
вставлять общие блоки кода в месте расположения курсора мыши.
• Шаблоны для окружения кода (Surround With). Эти шаблоны позволяют помещать
блок избранных операторов в рамки соответствующего контекста.
Чтобы посмотреть на эту функциональность в действии, давайте предположим, что
требуется обеспечить проход по поступающим в метод Main () параметрам в цикле
foreach.
Глава 2. Создание приложений на языке C#
99
Вместо того чтобы вводить необходимый код вручную, можно активизировать фраг­
мент кода f oreach. После выполнения этого действия IDE-среда поместит шаблон кода
fo rea ch в месте, где в текущий момент находится курсор мыши.
Поместим курсор мыши после первой открывающей фигурной скобки в методе
Main (). Одним йз способов активизации фрагмента кода является выполнение щелчка
правой кнопкой мыши и выбора в контекстном меню пункта Insert Snippet (Вставить
фрагмент кода) (или Surround With (Окружить с помощью)). Это приводит к отображению
списка всех относящихся к данной категории фрагментов кода (для закрытия контек­
стного меню достаточно нажать клавишу <Esc>). В качестве клавиатурной комбинации
быстрого вызова можно просто ввести имя интересующего фрагмента кода, которым в
данном случае является fo rea ch . На рис. 2.17 видно, что пиктограмма, представляю­
щая фрагмент кода, внешне немного напоминает клочок бумаги.
Рис. 2.17. Активизация фрагмента кода
Отыскав фрагмент кода, который требуется активизировать, нажмите два раза кла­
вишу <ТаЬ>. Это приведет к автоматическому завершению всего фрагмента кода и ос­
тавлению ряда меток-заполнителей, в которых останется только ввести необходимые
значения, чтобы фрагмент был готов. Нажимая клавишу <ТаЬ>, можно переходить от
одной метки-заполнителя к другой и заполнять пробелы (по завершении нажмите кла­
вишу <Esc> для выхода из режима редактирования фрагмента кода).
В результате щелчка правой кнопкой мыши и выбора в контекстном меню пункта
Surround With (Окружить с помощью) будет тоже появляться список возможных вари­
антов. При использовании средства Surround With обычно сначала выбирается блок
операторов кода для представления того, что должно применяться для их окружения
(например, блок try / c a tc h ). Обязательно уделите время изучению предопределенных
шаблонов расширения кода, поскольку они могут радикально ускорить процесс разра­
ботки программ.
На заметку! Все шаблоны для расширения кода представляют собой XML-описания кода, подлежа­
щие генерации в IDE-среде. В Visual Studio 2010 (а также в Visual C# 2010 Express) можно соз­
давать и собственные шаблоны кода. Дополнительные сведения доступны в статье “ Investigating
Code Snippet Technology” (“ Исследование технологии применения фрагментов кода” ) на сайте
h t t p : // m s d n .m ic ro s o ft. com.
100
Часть I. Общие сведения о языке C# и платформе .NET
Утилита Class Designer
S o lu tio n Explorer
В Visual Studio 2010 имеется возможность кон­
струировать классы визуальным образом (в Visual
ft J h P ro p e rtie s
C# 2010 Express такой возможности нет). Для этого
a o j / R e ferences
4 1 M i c ro s o ft S h jr p
в составе Visual Studio 2010 поставляется утилита
• J S y s te m
под названием Class Designer (Конструктор клас­
4 3 S y s te m .C o re
сов), которая позволяет просматривать и изменять
4 3 S y s te m .D a ta
*a3 S y s te m .D a ta .D a ta S e tE x te n s io n s
отношения между типами (классами, интерфейса­
4 3 System JC m l
ми, структурами, перечислениями и делегатами) в
• O S y s te m .X m l.L in q
J j P ro g ra m .e s
проекте. С помощью этой утилиты можно визуально
добавлять или удалять члены из типа с отражением
^^^Solutior^xploreJ
этих изменений в соответствующем файле кода на
С#, а также в диаграмме классов.
Рис. 2.18 Вставка файла диаграммы
Для работы с этой утилитой сначала необходимо
классов
вставить новый файл диаграммы классов. Делать
это можно несколькими способами, одним из которых является щелчок на кнопке View
C la ss Diagram (Просмотр диаграммы классов) в правой части окна Solution Explorer,
как показано на рис. 2.18 (при этом важно, чтобы в окне был выбран проект, а не
решение).
После выполнения этого действия появляются пиктограммы, представляющие клас­
сы, которые входят в текущий проект. Щелкая внутри них на значке с изображением
стрелки для того или иного типа, можно отображать или скрывать члены этого типа
(рис. 2.19).
"33 S o lu tio n 'Vs2(J V ie w C lass D ra g ra m I
а 5 у»2010Ь* ,Р Г----------1
На заметку! С помощью панели Class Designer (Конструктор классов) можно настраивать парамет­
ры отображения поверхности конструктора желаемым образом.
C l a s s D ia q r a m l. c d *
V s2 0 1 0 E x a m p le *
X И
P r o g r a m .e s *
P ro gra m
Class
с
В M e th o d s
Л *
\
*
C o n fig u r e C U I
M am
Ml
>
Рис. 2.19. Просмотр диаграммы классов
Эта утилита работает вместе с двумя другими средствами Visual Studio 2010 — ок­
ном C la ss Details (Детали класса), которое можно открыть путем выбора в меню View
(Вид) пункта Other Windows (Другие окна), и панелью C lass Designer Toolbox (Элементы
управления конструктора классов), которую можно отобразить выбором в меню View
(Вид) пункта Toolbox (Элементы управления). В окне C lass Details не только отображают­
ся детали выбранного в текущий момент элемента в диаграмме, но также можно изме­
нять его существующие члены и вставлять новые на лету, как показано на рис. 2.20.
101
Глава 2. Создание приложений на языке C#
Рис. 2.20. Окно C lass D e ta ils
Что касается панели Class Designer Toolbox, которую,
как уже было сказано, можно активизировать через меню
View (Вид), то она позволяет вставлять в проект новые типы
(и создавать между ними желаемые отношения) визуальным
образом (рис. 2.21). (Следует иметь в виду, что для просмотра
этой панели требуется, чтобы окно диаграммы классов было
активным.) По мере выполнения этих действий IDE-среда ав­
томатически создает незаметным образом соответствующие
новые определения типов на С#.
Для примера давайте перетащим из панели C lass D e sig n e r
Toolbox в окно C lass D e sig n e r новый элемент C lass (Класс), в
отрывшемся окне назначим ему имя Саг, а затем с помощью
окна C lass D e ta ils добавим в него общедоступное поле типа
string по имени PetName (рис. 2.22).
Взглянув на С#-определение класса Саг после этого, мож­
но увидеть, что оно было соответствующим образом обновле­
но (добавленные комментарии не считаются):
T o o lb o x
Class D e s ig n e r
*
P o in te r
Class
E
Enum
In te rfa c e
•
A b s tra c t Class
си
S tru ct
о
D e le g a te
4-
In h e rita n c e
A s s o c ia tio n
Com m ent
G en eral
Рис. 2.21. Панель Class
D esigner Toolbox
public class Car
/ / И с п о л ь з о в а т ь о б щ ед о сту п н ы е дан н ы е обы чно
//н е
р е к о м е н д у е т с я , но з д е с ь э т о уп р о щ ает пр и м ер .
public string petName;
Теперь давайте активизируем утилиту Class Designer еще раз и перетащим на по­
верхность конструктора новый элемент типа C lass, присвоив ему имя SportsCar. Затем
выберем в C lass D e s ig n e r Toolbox пиктограмму In h e rita n c e (Наследование) и щелкнем в
верхней части пиктограммы SportsCar. Далее, не отпуская левую кнопку мыши, перета­
щим курсор мыши на поверхность пиктограммы класса Саги отпустим ее. Правильное
выполнение всех перечисленных выше действий приведет к тому, что класс SportsCar
станет наследоваться от класса Саг, как показано на рис. 2.23.
’
Class Details - C a r
j
V *
Nam e
ЛУ
a
^
Type
M o d ifie r
S u m m a ry
? x
H id e
P r o p e r tie s
" 5 * - a d d p rope rty
* Fields
j p e tN a m e
*
♦
I a
I
dd t
H
H
strm q
p u b lic
Id
E v e n ts
J
--------
Рис. 2.22. Добавление поля с помощью окна C lass D e ta ils
ш
я ш
ш
и ж
* \
102
Часть I. Общие сведения о языке C# и платформе .NET
КЯ 1Я 1
C la s s D ia q ra m l. cd*
X
O b je c t Brow ser
Vs201OExample*
Prog a m .e s "
*
*
P rogram
i
Class
*3 M e th o d s
-
Fields
C o n fig u re C U I
l
"
p e tN a r^ e
'
M a in
___________ J
gF )
Cm
Class
1
L
T
__ ____ i ___;_____
S portsCar
I Class
•j -* Car
<
Г
~
if-
■V ‘ П1^
. . -1
Рис. 2.23. Визуальное наследования одного класса от другого
Чтобы заверш ить данный пример, осталось обновить сгенерированный класс
SportsCar, добавив в него общедоступный метод с именем GetPetName ():
public class SportsCar : Car
{
public string GetPetName()
{
petName = "Fred";
return petName;
}
}
Все эти (и другие) визуальные инструменты Visual 2010 придется еще много раз ис­
пользовать в книге. Однако уже сейчас должно появиться чуть большее понимание ос­
новных возможностей этой IDE-среды.
На заметку! Концепция наследования подробно рассматривается в главе 6.
Интегрируемая система документации .NET Framework 4.0
И, наконец, последним средством в Visual Studio 2010, которым необходимо обяза­
тельно уметь пользоваться с самого начала, является полностью интегрируемая спра­
вочная система. Поставляемая с .NET Framework 4.0 SDK документация представляет
собой исключительно хороший, очень понятный и насыщенный полезной информацией
источник. Из-за огромного количества предопределенных типов .NET (насчитывающих
тысячи), необходимо погрузиться в исследование предлагаемой документации. Не же­
лающие делать это обрекают себя как разработчика .NET на длительное, мучительное
и болезненное существование.
При наличии соединения с Интернетом просматривать документацию .NET
Framework 4.0 SDK можно в онлайновом режиме по следующему адресу:
http://msdn.microsoft.com/library
Разумеется, при отсутствии постоянного соединения с Интернетом такой подход
оказывается не очень удобным. К счастью, ту же самую справочную систему мож­
но установить локально на своем компьютере. Имея уже установленную копию Visual
Studio 2010, необходимо выбрать в меню Start (Пуск) пункт All Program s1^ Microsoft
Глава 2. Создание приложений на языке C#
103
Visual S tu d io 2 0 1 0 c=>Visual S tu d io T o o ls '^ Manage Help (Все программы1
^ Microsoft Visual
Studio 2010 ^Утилиты Visual Studio ^Управление настройками справочной системы).
Затем можно приступать к добавлению интересующей справочной документации, как
показано на рис. 2.24 (если на диске хватает места, имеет смысл добавить всю возмож­
ную документацию).
Рис. 2.24. В окне Help Library Manager (Управление библиотекой
справочной документации) можно загрузить локальную копию
документации .NET Framework 4.0 SDK
На заметку! Начиная с версии .NET Framework 4.0, просмотр справочной системы осуществля­
ется через текущий веб-браузер, даже в случае локальной установки документации .NET
Framework 4.0 SDK.
После локальной установки справочной системы простейшим способом для взаимо­
действия с ней является выделение интересующего ключевого слова С#, имени типа
или имени члена в окне представления кода внутри Visual Studio 2010 и нажатие кла­
виши <F1>. Это приводит к открытию окна с документацией, касающейся конкретного
выбранного элемента. Например, если выделено ключевое слово s t r i n g в определении
класса Саг, после нажатия клавиши <F1> появится страница со справочной информа­
цией об этом ключевом слове.
Еще одним полезным компонентом справочной системы является доступное для ре­
дактирования поле Search (Искать), которое отображается в левой верхней части эк­
рана. В этом поле можно вводить имя любого пространства имен, типа или члена и
тем самым сразу же переходить в соответствующее место в документации. При попыт­
ке найти подобным образом пространство имен System . R e f l e c t i o n , например, можно
будет узнать о деталях этого пространства имен, изучить содержащиеся внутри него
типы, просмотреть примеры кода с ним и т.д. (рис. 2.25).
В каждом узле внутри дерева описаны типы, которые содержатся в данном простран­
стве имен, их члены и параметры этих членов. Более того, при просмотре страницы
справки по тому или иному типу всегда сообщается имя сборки и пространства имен, в
котором содержится запрашиваемый тип (соответствующая информация отображается
в верхней части данной страницы). В остальной части настоящей книги ожидается, что
читатель будет заглядывать в эту очень важную справочную систему и изучать допол­
нительные детали рассматриваемых сущностей.
104
Часть I. Общие сведения о языке C# и платформе .NET
System.Reflection Namespace
Л
V isual S tu d io
library Но it *
VisualywlvOx'OlO
NET Frvmewoif 4
NET FrameworkClass library
System. Reflection Namespace
AmfcnguousMatchExcepticin I lass
Assembly Class
Assembly AJqor. I hoTjdAttnbine C lass
A sse m b le omparw Allnoute Class
AssemMyCoofiguratawiAttnbsJfe Clot1
Assem blyCopyghtAH rAiute Class
AsseinMyCuHureAttnbute Class
AssemblylMauNAliasAtlribof e C lass
Ass errbtyD cla ySrg n Attn b ut e Class
AssemblyDescriplioiiAttribute Class
Avsembt/filrVcrsicnAttnbute Class
Assemb4*n*<l'Attiibijte Class
Assembly Inf ormaticnalC ervionAtt rib j
AssembMeyFileAttribute Class
Aasembl,Kc,N.iroeAttr«buff Class
Assembly Name Class
Aii*m blyN ainaTia^s Enumeration
Send Feedback
The Syste m .R e fle ctio n namespace contains types that retrieve information about assemblies, modules
members parameters, and other entities m managed code by examining their metadata. These types also can
be used to manipulate instances erf loaded types, for example to hook up events or to invoke methods. To
йупаггжаНу create types, use the System Reflectrar.Emi’. namespace
Classes
C la n
D esc rip tio n
AmbwuousMalchtxcepticn
The exception that is thrown when binding to a
member results in more tnan one member matching
the binding c-itena. This class cannot be inherited.
Represents an assembly which is a reusable,
versionable and aelf-descnbing building block of a
common language runtime application.
Рис. 2.25. Поле Search позволяет быстро находить представляющие интерес элементы
На заметку! Не лишним будет напомнить еще раз о важности использования документации .NET
Framework 4.0 SDK. Ни одна книга, какой бы объемной она ни была, не способна охватить все
аспекты платформы .NET. Поэтому необходимо научиться пользоваться справочной системой;
впоследствии это окупится с лихвой.
Резюме
Итак, нетрудно заметить, что у разработчиков появилась масса новых “игрушек”.
Целью настоящей главы было проведение краткого экскурса в основные средства, кото­
рые программист на C# может использовать во время разработки. В начале было рас­
сказано о том, как генерировать сборки .NET с применением бесплатного компилятора
C# и редактора “Блокнот” (Notepad). Затем было приведено краткое описание прило­
жения Notepad++ и показано, как его использовать для редактирования и компиляции
файлов кода * . cs.
И, наконец, здесь были рассмотрены три таких многофункциональных ШЕ-среды,
как SharpDevelQp (распространяется с открытым исходным кодом), Microsoft Visual
C# 2010 Express и Microsoft Visual Studio 2010 Professional. Функциональные возможно­
сти каждого из этих продуктов в настоящей главе были описаны лишь кратко, поэтому
каждый волен заняться более детальным изучением избранной IDE-среды в свободное
время (многие дополнительные функциональные возможности Visual Studio 2010 будут
рассматриваться далее в настоящей книге).
ЧАСТЬ
DiaBHbie конструкции
программирования
на C#
В этой ч а сти ...
Глава 3. Главные конструкции программирования на С#: часть I
Глава 4. Главные конструкции программирования на С#: часть II
Глава 5. Определение инкапсулированных типов классов
Глава 6. Понятия наследования и полиморфизма
Глава 7. Структурированная обработка исключений
Глава 8. Время жизни объектов
ГЛАВА 3
DiaBHbie конструкции
программирования
на С#: часть I
настоящей главе начинается формальное исследование языка программирования
С#. Здесь предлагается краткий обзор отдельных тем, в которых необходимо ра­
зобраться, чтобы успешно изучить платформу .NET Framework. В первую очередь пояс­
няется то, как создавать объект приложения и как должна выглядеть структура метода
Main ( ), который является входной точкой в любой исполняемой программе. Далее рас­
сматриваются основные типы данных в C# (и их аналоги в пространстве имен System),
в том числе типы классов System. String и System. Text.StringBuilder.
После представления деталей основных типов данных в .NET рассказывается о ряде
методик, которые можно применять для их преобразования, в том числе об операциях
сужения (narrowing) и расширения (widening), а также использовании ключевых слов
checked и unchecked.
Кроме того, в этой главе описана роль ключевого слова v a r в языке С#, которое
позволяет неявно определять локальную переменную. И, наконец, в главе приводится
краткий обзор ключевых операций, итерационных конструкций и конструкций приня­
тия решений, применяемых для создания рабочего кода на С#.
В
Разбор простой программы на C#
В языке C# вся логика программы должна содержаться внутри определения какогото типа (в главе 1 уже говорилось, что тип представляет собой общий термин, которым
обозначается любой элемент из множества (класс, интерфейс, структура, перечисле­
ние, делегат}). В отличие от многих других языков, в C# не допускается создавать ни
глобальных функций, ни глобальных элементов данных. Вместо этого требуется, чтобы
все члены данных и методы содержались внутри определения типа. Для начала давай­
те создадим новый проект типа C o n so le A p p lic a tio n (Консольное приложение) по имени
SimpleCSharpAppB. Как показано ниже, в исходном коде Program, cs ничего особо при­
мечательного нет:
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Text
namespace SimpleCSharpApp
Глава 3. Главные конструкции программирования на С#: часть I
107
class Program
{
static void Main(string[] args)
Имея такой код, далее модифицируем метод Маш () в классе Program, добавив в него
следующие операторы:
class Program
{
static void Main(string[] args)
{
// Вывод простого сообщения пользователю.
Console.WriteLine ("***** My First C# App *****");
Console .WnteLine ("Hello World! ");
.Console.WriteLine();
// Ожидание нажатия клавиши <Enter>
// перед завершением работы.
Console.ReadLine();
В результате получилось определение типа класса, поддерживающее единственный
метод по имени Main (). По умолчанию классу, в котором определяется метод Main (),
в Visual Studio 2010 назначается имя Program; при желании это имя легко изменить.
Класс, определяющий метод Main ( ), должен обязательно присутствовать в каждом ис­
полняемом приложении на C# (будь то консольная программа, настольная программа
для Windows или служба Windows), поскольку он применяется для обозначения точки
входа в приложение.
Формально класс, в котором определяется метод М а ш (), называется объектом при­
ложения. Хотя в одном исполняемом приложении допускается иметь более одного тако­
го объекта (это может быть удобно при проведении модульного тестирования), при этом
обязательно необходимо информировать компилятор о том, какой из методов Main ()
должен использоваться в качестве входной точки. Для этого нужно либо указать опцию
main в командной строке, либо выбрать соответствующий вариант в раскрывающемся
списке на вкладке Application (Приложение) окна редактора свойств проекта в Visual
Studio 2010 (см. главу 2).
Обратите внимание, что в сигнатуре метода Main () присутствует ключевое слово
static, которое более подробно рассматривается в главе 5. Пока достаточно знать, что
область действия статических (static) членов охватывает уровень всего класса (а не
уровень отдельного объекта) и потому они могут вызываться без предварительного соз­
дания нового экземпляра класса.
На заметку! C# является чувствительным к регистру языком программирования. Следовательно,
Main и main или Readline и ReadLine будут представлять собой далеко не одно и то же.
Поэтому необходимо запомнить, что все ключевые слова в C# вводятся в нижнем регистре
(например, public, lock, class, dynamic), а названия пространств имен, типов и членов
всегда начинаются (по соглашению) с заглавной буквы, равно как и любые вложенные в них
слова (как, например, Console .WriteLine, System.Windows .Forms .MessageBox и
System. Data.SqlClient). Как правило, при каждом получении от компилятора ошибки,
связанной с "неопределенными символами” , требуется проверить регистр символов.
108
Часть II. Главные конструкции программирования на C#
Помимо ключевого слова static, данный метод Main () имеет еще один параметр,
который представляет собой массив строк (string [ ] args). Хотя в текущий момент
этот массив никак не обрабатывается, в данном параметре в принципе может содер­
жаться любое количество аргументов командной строки (процесс получения доступа к
которым будет описан чуть ниже). И, наконец, данный метод М а ш () был сконфигури­
рован с возвращаемым значением void, которое свидетельствует о том, что решено не
определять возвращаемое значение явным образом с помощью ключевого слова return
перед выходом из области действия данного метода.
Логика Program содержится внутри самого метода Main ( ). Здесь используется класс
Console из пространства имен System. В число его членов входит статический метод
WriteLine (), который позволяет отправлять строку текста и символ возврата карет­
ки на стандартное устройство вывода. Кроме того, здесь вызывается метод Console.
ReadLine ( ) , чтобы окно командной строки, запускаемое в IDE-среде Visual Studio 2010,
оставалось видимым во время сеанса отладки до тех пор, пока не будет нажата клавиша
<E nter>.
Варианты метода M a in ()
По умолчанию в Visual Studio 2010 будет генерироваться метод Маш() с возвра­
щаемым значением void и массивом типов string в качестве единственного входного
параметра. Однако такой метод Main () является далеко не единственно возможным
вариантом. Вполне допускается создавать собственные варианты входной точки в при­
ложение с помощью любой из приведенных ниже сигнатур (главное, чтобы они содер­
жались внутри определения какого-то класса или структуры на С#):
// Возвращаемый тип in t и массив строк в качестве параметра.
static in t Main(s t r in g [] args)
{
// Должен обязательно возвращать значение перед выходом!
return 0;
}
//Ни возвращаемого типа, ни параметров.
static void Main()
// Возвращаемый тип in t , но никаких параметров.
static in t Main ()
{
// Должен обязательно возвращать значение перед выходом!
return 0;
}
На заметку! Метод Main () может также определяться как общедоступный (public), а не приват­
ный (private), каковым он считается, если не указан конкретный модификатор доступа. В Visual
Studio 2010 метод Main () автоматически определяется как неявно приватный. Это гарантирует
отсутствие у приложений возможности напрямую обращаться к точке входа друг друга.
Очевидно, что выбор способа создания метода Main () зависит от двух моментов.
Во-первых, он зависит от того, нужно ли, чтобы системе после окончания выполнения
метода Main () и завершения работы программы возвращалось какое-то значение; в
этом случае необходимо возвращать тип данных int, а не void. Во-вторых, он зависит
от необходимости обработки предоставляемых пользователем параметров командной
строки; в этом случае они должны сохраняться в массиве strings. Рассмотрим все воз­
можные варианты более подробно.
Глава 3. Главные конструкции программирования на С#: часть I
109
Спецификация кода ошибки в приложении
Хотя в большинстве случаев методы Main () возвращают void в качестве возвра­
щаемого значения, способность возвращать int из Main () позволяет согласовать C#
с другими языками на базе С. По соглашению, возврат значения 0 свидетельствует о
том, что выполнение программы прошло успешно, а любого другого значения (напри­
мер, -1) — что в ходе выполнения программы произошла ошибка (следует иметь в виду,
что значение 0 возвращается автоматически даже в случае, если метод Main () возвра­
щает void).
В операционной системе Windows возвращаемое приложением значение сохраня­
ется в переменной среды по имени %ERRORLEVEL%. В случае создания приложения, в
коде которого предусмотрен запуск какого-то исполняемого модуля (см. главу 16), по­
лучить значение %ERRORLEVEL% можно с помощью статического свойства System.
Diagnostics.Process.ExitCode.
Из-за того, что возвращаемое приложением значение в момент завершения его ра­
боты передается системе, приложение не может получать и отображать свой конечный
код ошибки во время выполнения. Однако просмотреть код ошибки по завершении вы­
полнения программы все-таки можно. Чтобы увидеть, как это делается, модифицируем
метод Main () следующим образом:
// Обратите внимание, что теперь возвращается in t, а не void.
static int Main(string [] args)
{
// Вывод сообщения и ожидание нажатия клавиши <Enter>.
Console.WriteLine (''***** Му First C# App *****");
Console.WriteLine("Hello World!");
Console.WriteLine();
Console.ReadLine();
// Возврат произвольного кода ошибки.
return -1;
}
Теперь давайте обеспечим перехват возвращаемого Main () значения с помощью ко­
мандного файла. Для этого перейдите в окне проводника Windows в каталог, где хра­
нится скомпилированное приложение (например, в C:\SimpleCSharpApp\bin\Debug),
создайте в нем новый текстовый файл (по имени SimpleCSharpApp.bat) и добавьте в
него следующие инструкции:
0echo off
rem Командный файл для приложения SimpleCSharpApp.exe,
геш перехватывающий возвращаемое им значение.
SimpleCSharpApp
0if "%ERRORLEVEL%" == "0" goto success
:fail
echo This application has failed1
rem Выполнение этого приложения не удалось !
echo return value = %ERRORLEVEL%
goto end
:success
echo This application has succeeded!
rem Выполнение этого приложения прошло успешно!
echo return value = %ERRORLEVEL%
goto end
:end
echo All Done.
,
rem Все сделано.
110
Часть II. Главные конструкции программирования на C#
Теперь откройте окно командной строки в Visual Studio 2010 и перейдите в ката­
лог, где находится исполняемый файл приложения и созданный только что файл * .bat.
Запустите командный файл, набрав его имя и нажав <Enter>. После этого на экране
должен появиться вывод, подобный показанному на рис. 3.1, поскольку метод Main ()
сейчас возвращает значение -1. Если бы он возвращал значение 0, в окне консоли поя­
вилось бы сообщение This application has succeeded!.
Рис. 3.1. Перехват значения, возвращаемого приложением,
с помощью командного файла
В большинстве приложений на C# (если не во всех) в качестве возвращаемого М а ш ()
значения будет использоваться void, которое, как уже известно, неявно подразумевает
возврат кода ошибки 0. Из-за этого все демонстрируемые далее в настоящей книге ме­
тоды Main () будут возвращать именно void (никакие командные файлы для перехвата
кода возврата в последующих проектах не используются).
Обработка аргументов командной строки
Теперь, когда стало более понятно, что собой представляет возвращаемое значение
метода Main ( ) , давайте посмотрим на входной массив данных string. Предположим,
что теперь требуется обновить приложение так, чтобы оно могло обрабатывать любые
возможные параметры командной строки. Одним из возможных способов для обеспе­
чения такого поведения является использование поддерживаемого в C# цикла for (все
итерационные конструкции C# более подробно рассматриваются далее в главе):
static int Main(string[] args)
{
// Обработка любых входящих аргументов.
for(int i = 0; i < args.Length; i++)
Console .WnteLine ("Arg: {0}", argsfi]);
Console.ReadLine() ;
return -1;
}
Здесь с помощью свойства Length типа System.Array производится проверка, со­
держатся ли в массиве string какие-то элементы. Как будет показано в главе 4, все
массивы в C# на самом деле относятся к классу System. Array и потому имеют общий
набор членов. При проходе по массиву значение каждого элемента выводится в окне
консоли. Процесс предоставления аргументов в командной строке сравнительно прост
и показан на рис. 3.2.
В качестве альтернативы, вместо стандартного цикла for для прохода по входному
массиву строк можно также использовать ключевое слово fо reach.
Глава 3. Главные конструкции программирования на С#: часть I
111
Рис. 3.2. Предоставление аргументов в командной строке
Ниже приведен соответствующий пример:
// Обратите внимание, что в случае использования
// цикла foreach проверять размер массива
//не требуется.
static int Main(string[] args)
{
// Обработка любых входящих аргументов с помощью foreach.
foreach (string arg in args)
Console .WnteLine ("Arg: {0}", arg);
Console.ReadLine();
return -1;
И, наконец, получать доступ к аргументам командной строки можно с помощью ста­
тического метода GetCommandLineArgs () , принадлежащего типу System .Environm ent.
Возвращаемым значением этого метода является массив строк (s tr in g ). В первом эле­
менте этого массива содержится имя самого приложения, а во всех остальных — отдель­
ные аргументы командной строки. Важно обратить внимание, что при этом определять
метод Main () так, чтобы он принимал в качестве входного параметра массив s t r in g ,
больше не требуется, хотя никакого вреда от этого не будет.
public static int Main(string [] args)
{
// Получение аргументов с использованием System.Environment.
string[] theArgs = Environment.GetCommandLineArgs();
foreach(string arg in theArgs)
Console .WnteLine ("Arg: {0}", arg);
Console.ReadLine();
return -1;
}
Разумеется, то, на какие аргументы командной строки должна реагировать програм­
ма (если вообще должна), и в каком формате они должны предоставляться (например, с
префиксом - или /), можно выбирать самостоятельно. В приведенном выше коде про­
сто передавался набор опций, которые выводились прямо в командной строке. Но что
если бы создавалась новая видеоигра, и приложение программировалось на обработку
опции, скажем, -godmode? Например, при запуске пользователем приложения с этим
флагом можно было бы предпринимать в его отношении соответствующие меры.
Указание аргументов командной строки в Visual Studio 2010
В реальном мире конечный пользователь имеет возможность предоставлять аргу­
менты командной строки при запуске программы. Для целей тестирования приложения
в процессе разработки также может потребоваться указывать возможные флаги команд­
112
Часть II. Главные конструкции программирования на C#
ной строки. В Visual Studio 2010 для этого необходимо дважды щелкнуть на пиктограм­
ме P ro p e rtie s (Свойства) в окне Solution Explorer, выбрать в левой части окна вкладку
D e b u g (Отладка) и указать в текстовом поле C o m m a n d line a rg u m e n ts (Аргументы ко­
мандной строки) желаемые значения (рис. 3.3).
Рис. 3.3. Указание аргументов командной строки в Visual Studio 2010
Указанные аргументы командной строки будут автоматически передаваться методу
Main () при проведении отладки или запуске приложения в IDE-среде Visual Studio.
Интересное отклонение от темы:
некоторые дополнительные члены
класса System.Environment
В классе Environment помимо GetCommandLineArgs () предоставляется ряд других
чрезвычайно полезных методов. В частности, этот класс позволяет с помощью различ­
ных статических членов получать детальные сведения, касающиеся операционной систе­
мы, под управлением которой в текущий момент выполняется .NET-приложение. Чтобы
оценить пользу от класса System.Environment, модифицируем метод Main () так, что­
бы в нем вызывался вспомогательный метод по имени ShowEnvironmentDetails ():
static int Main(string [] args)
»
{
// Вспомогательный метод для класса Program.
ShowEnvironmentDetails();
Console.ReadLine();
return -1;
}
Теперь реализуем этот метод в классе Program, чтобы в нем вызывались различные
члены класса Environment:
static void ShowEnvironmentDetails ()
{
// Отображение информации о дисковых устройствах
/ / на данной машине и прочих интересных деталей.
Глава 3. Главные конструкции программирования на С#: часть I
113
foreach (string drive in Environment.GetLogicalDnves () )
Console.WriteLine("Drive: {0}", drive);
// диски
Console .WnteLine ("OS : {0 }" , Environment.OSVersion) ; // OC
Console.WriteLine("Number of processors: {0}",
Environment.ProcessorCount);
// количество процессоров
Console.WriteLine(".NET Version: {0}",
Environment.Version);
// версия .NET
}
На рис. 3.4 показано возможное тестовое выполнение данного метода. Если на
вкладке Debug в Visual Studio 2010 аргументы командной строки не указаны, в окне
консоли они, соответственно, тоже появляться не будут.
g С Wmdo*»
;mrie»e
***** Му First C# App *****
Hello World!
Arg: -godmode
Arg: -argl
Arg: -arg2
Drive: C:\
Drive: D:\
Drive: E:\
Drive: F:\
Drive: H:\
jO S : Microsoft Windows NT 6.1.7600.0
Number of processors: 4
.NFT Version: 4.0.20506.1
Рис. 3.4. Отображение переменных среды
Помимо показанных в предыдущем примере, у класса Environment имеются и дру­
гие члены. В табл. 3.1 перечислены некоторые наиболее интересные из них; полный
список со всеми деталями можно найти в документации .NET Framework 4.0 SDK.
Таблица 3.1. Некоторые свойства S y s te m . E n v ir o n m e n t
Свойство
Описание
ExitCode
Позволяет получить или установить код возврата приложения
MachineName
Позволяет получить имя текущей машины
NewLine
Позволяет получить символ новой строки, поддерживаемый
в текущей среде
StackTrace
Позволяет получить текущие данные трассировки стека для
приложения
SystemDirectory
Возвращает полный путь к системному каталогу
UserName
Возвращает имя пользователя, который запустил данное приложение
Исходный код. Проект SimpleCSharpApp доступен в подкаталоге Chapter 3.
Класс System. Console
Почти во всех примерах приложений, создаваемых в начальных главах книги, будет
интенсивно использоваться класс System.Console. И хотя в действительности консоль­
ный пользовательский интерфейс (Console User Interface — CUI) не является настолько
114
Часть II. Главные конструкции программирования на C#
привлекательным, как графический (Graphical User Interface — GUI) или веб-интерфейс,
ограничение первых примеров консольными программами, позволяет уделить больше
внимания синтаксису C# и ключевым характеристикам платформы .NET, а не сложным
деталям построения графических пользовательских интерфейсов или веб-сайтов.
Класс Console инкапсулирует в себе возможности, позволяющие манипулировать
вводом, выводом и потоками ошибок в консольных приложениях. В табл. 3.2 перечис­
лены некоторые наиболее интересные его члены.
Таблица 3.2. Некоторые члены класса S y s te m .C o n s o le
Член
Описание
Beep ()
Этот метод вынуждает консоль подавать звуковой сигнал определен­
ной частоты и длительности
BackgroundColor
ForegroundColor
Эти свойства позволяют задавать цвет изображения и фона для теку­
щего вывода. В качестве значения им может присваиваться любой из
членов перечисления ConsoleColor
BufferHeight
BufferWidth
Эти свойства отвечают за высоту и ширину буферной области консоли
Title
Это свойство позволяет устанавливать заголовок для текущей консоли
WindowHeight
WindowWidth
WindowTop
WindowLeft
Эти свойства позволяют управлять размерами консоли по отношению
к установленному буферу
Clear()
Этот метод позволяет очищать установленный буфер и область изо­
бражения консоли
Базовый ввод-вывод с помощью класса C o n s o l e
Помимо членов, перечисленных в табл. 3.2, в классе Console имеются методы, ко­
торые позволяют захватывать ввод и вывод; все они являются статическими и пото­
му вызываются за счет добавления к имени метода в качестве префикса имени самого
класса (Console). К их числу относится уже показанный ранее метод WriteLine () , ко­
торый позволяет вставлять в поток вывода строку текста (вместе с символом возврата
каретки); метод Write (), который позволяет вставлять в поток вывода текст без симво­
ла возврата каретки; метод ReadLine ( ) , который позволяет получать информацию из
потока ввода вплоть до нажатия клавиши <Enter>; и метод Read ( ) , который позволяет
захватывать из потока ввода одиночный символ.
Рассмотрим пример выполнения базовых операций ввода-вывода с использовани­
ем класса Console, для чего создадим новый проект типа C o n so le A p p lic a tio n по имени
BasicConsolelO и модифицируем метод Main () внутри него так, чтобы в нем вызывал­
ся вспомогательный метод GetUserData ():
class Program
{
static void Main(string[] args)
}
Console.WriteLine ("***** Basic Console I/O *****");
GetUserData ();
Console.ReadLine();
Глава 3. Главные конструкции программирования на С#: часть I
1 15
Теперь реализуем этот метод в классе Program вместе с логикой, приглашающей
пользователя вводить некоторые сведения и отображающей их на стандартном устрой­
стве вывода. Для примера у пользователя будет запрашиваться имя и возраст (который
для простоты будет трактоваться как текстовое значение, а не привычное числовое).
static void GetUserData ()
{
// Получение информации об имени и возрасте.
Console.Write("Please enter your name: " ); // Запрос на ввод имени
string userName = Console.ReadLine();
Console.Write("Please enter your age: " );
// Запрос на ввод возраста
string userAge = Console.ReadLine() ;
// Изменение цвета изображения, просто ради интереса.
ConsoleColor prevColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
// Отображение полученных сведений в окне консоли.
Console.WnteLine ("Hello {0}! You are {1} years old.", userName, userAge);
// Восстановление предыдущего цвета.
Console.ForegroundColor = prevColor;
После запуска этого приложения входные данные будут выводиться в окне консоли
(с использованием указанного специального цвета).
Форматирование вывода, отображаемого в окне консоли
В ходе первых нескольких глав можно было заметить, что внутри различных строко­
вых литералов часто встречались обозначения вроде { 0 } и { 1 }. Дело в том, что в .NET
для форматирования строк поддерживается стиль, немного напоминающий стиль опе­
ратора printf () в С. Попросту говоря, при определении строкового литерала с сегмен­
тами данных, значения которых остаются неизвестными до этапа времени выполнения,
внутри него допускается указывать метку-заполнитель с использованием синтаксиса в
виде фигурных скобок. Во время выполнения на месте каждой такой метки-заполните­
ля подставляется передаваемое в Console.WriteLineO значение (или значения).
В качестве первого параметра методу W r it e L in e O всегда передается строковый
литерал, в котором могут содержаться метки-заполнители вида { 0 } , { 1 } , { 2 } и т.д.
Следует запомнить, что отсчет в окружаемых фигурными скобками метках-заполните­
лях всегда начинается с нуля. Остальными передаваемыми W r it e L in e O параметрами
являются просто значения, которые должны подставляться на месте соответствующих
меток -заполнителей.
На заметку! Если количество пронумерованных уникальным образом заполнителей превышает чис­
ло необходимых для их заполнения аргументов, во время выполнения будет генерироваться
исключение, связанное с форматом.
Метка-заполнитель может повторяться в пределах одной и той же строки. Например,
для создания строки " 9, Number 9, Number 9 " можно было бы написать такой код:
// Вывод строки "9, Number 9, Number 9"
Console.WriteLine("{0}, Number {0}, Number {0}", 9);
Также следует знать о том, что каждый заполнитель допускается размещать в любом
месте внутри строкового литерала, и вовсе не обязательно, чтобы следующий после него
заполнитель имел более высокий номер. Например:
// Отображает: 20, 10, 30
Console.WriteLine ("{1}, {0}, {2}", 10, 20, 30);
116
Часть II. Главные конструкции программирования на C#
Форматирование числовых данных
Если требуется использовать более сложное форматирование для числовых данных,
в каждый заполнитель можно включить различные символы форматирования, наибо­
лее полезные из которых перечислены в табл. 3.3.
Таблица 3.3. Символы для форматирования числовых данных в .NET
Символ
форматирования
Описание
С или с
Применяется для форматирования денежных значений. По умолчанию этот
флаг идет перед символом локальной культуры (например, знаком доллара
[ $ ] , если речь идет о культуре US English)
D или d
Применяется для форматирования десятичных чисел. Этот флаг может так­
же задавать минимальное количество цифр для представления значения
Е или е
Применяется для экспоненциального представления. Регистр этого флага
указывает, в каком регистре должна представляться экспоненциальная кон­
станта — в верхнем (Е) или в нижнем (е)
F или f
Применяется для представления числовых данных в формате с фиксирован­
ной точкой. Этот флаг может также задавать минимальное количество цифр
для представления значения
G или g
Расшифровывается как g e n e ra l (общий (формат)). Этот символ может при­
меняться для представления числа в фиксированном или экспоненциальном
формате
N или п
Применяется для базового числового форматирования (с запятыми)
X или X
Применяется для представления числовых данных в шестнадцатеричном
формате. В случае использования символа X в верхнем регистре, в шестна­
дцатеричном представлении будут содержаться символы верхнего регистра
Все эти символы форматирования присоединяются к определенной метке-заполни­
телю в виде суффикса после двоеточия (например, { 0 : С}, {1 : d }, {2 : X}.). Для примера
изменим метод Main () так, чтобы в нем вызывалась новая вспомогательная функция
по имени FormatNumericalData (), и затем реализуем его в классе Program для обеспе­
чения форматирования значения с фиксированной точкой различными способами.
// Использование нескольких дескрипторов формата.
static void FormatNumericalData()
Console .WnteLine
Console .WnteLine
Console.WriteLine
Console.WriteLine
Console.WriteLine
("The value 99999 in various formats: ) ;
("c format: {0 :c }" , 99999);
("d9 format: {0:d9}", 99999)
("f3 format: {0:f3}", 99999)
("n format: {0:n }", 99999);
}
// Обратите внимание, что использование X и х
// определяет, будут символы отображаться
/ / в верхнем или нижнем регистре.
Console.WriteLine ("Е format: {0:Е}", 99999);
Console.WriteLine ("е format: {0:е}", 99999);
Console.WriteLine ("X format: {0:X}", 99999);
Console.WriteLine ("x format: {0:x}", 99999);
}
На рис. 3.5 показан вывод этого приложения.
Глава 3. Главные конструкции программирования на С#: часть I
117
Помимо символов, позволяющих управлять
1 С A in d ; * ; ’
форматированием числовых данных, в .NET
***** Basic Console I/O
Please enter your name: Saku
поддерживается несколько лексем, которые
Please enter your age: 1
, Hello Saku!
You are 1 years old.
можно использовать в строковых литералах и
The
value
99999
in various formats:
которые позволяют управлять позиционирова­
c format: 199,999.00
d9
format:
000099999
нием содержимого и добавлением в него про­
fB format: 99999.000
белов. Более того, лексемы, применяемые для
n format: 99,999.00
E format: 9.999900E+004
числовых данных, допускается применять и
e format: 9.999900e+004
1869F
для форматирования других типов данных (на­ | Xx format:
format: 1869f
пример, для перечислений или типа DateTime).
Press any key to continue . . . _
Вдобавок можно создавать специальный класс
(или структуру) и определять в нем специаль­
J
ную схему форматирования за счет реализации
Рис. 3.5. Базовый консольный ввод-вывод
интерфейса ICustomFormatter.
(с форматированием строк .NET)
По ходу настоящей книги будут встречаться
и другие примеры форматирования; тем, кто всерьез заинтересовался темой форма­
тирования строк в .NET, следует обязательно изучить посвященный форматированию
строк раздел в документации .NET Framework 4.0.
Исходный код. Проект BasicConsolelO доступен в подкаталоге Chapter 3.
Форматирование числовых данных
в приложениях, отличных от консольных
Напоследок хотелось бы отметить, что символы форматирования строка .NET могут ис­
пользоваться не только в консольных приложениях. Тот же синтаксис форматирования
можно применять и в вызове статического метода string . Format ( ) . Это может быть
удобно при генерации во время выполнения текстовых данных, которые должны исполь­
зоваться в приложении любого типа (например, в настольном приложении с графиче­
ским пользовательским интерфейсом, в веб-приложении ASP.NET или веб-службах XML).
Для примера предположим, что требуется создать графическое настольное прило­
жение и применить форматирование к строке, отображаемой в окне сообщения внутри
него:
static void DisplayMessage ()
{
// Использование string.Form at() для форматирования строкового литерала.
string userMessage = string.Format("100000 in hex is {0:x}", 100000);
// Для компиляции этой строки кода требуется
// ссылка на System.Windows.Forms.dll1
System.Windows.Forms.MessageBox.Show(userMessage);
}
Обратите внимание, что string.Format () возвращает новый объект string, кото­
рый форматируется в соответствии с предоставляемыми флагами. После этого тексто­
вые данные могут использоваться любым желаемым образом.
Системные типы данных и их
сокращенное обозначение в C#
Как и в любом языке программирования, в C# поставляется собственный набор ос­
новных типов данных, которые должны применяться для представления локальных
переменных, переменных экземпляра, возвращаемых значений и входных параметров.
118
Часть II. Главные конструкции программирования на C#
Однако в отличие от других языков программирования, в C# эти ключевые слова пред­
ставляют собой нечто большее, чем просто распознаваемые компилятором лексемы.
Они, по сути, представляют собой сокращенные варианты обозначения полноценных
типов из пространства имен System. В табл. 3.4 перечислены эти системные типы дан­
ных вместе с охватываемыми ими диапазонами значений, соответствующими ключе­
выми словами на C# и сведениями о том, отвечают ли они требованиям общеязыковой
спецификации CLS (Common Language Specification).
На заметку! Как рассказывалось в главе 1, с
koaom .NET, отвечающим требованиям CLS, может ра­
ботать любой управляемый язык программирования. В случае включения в программы данных,
которые не соответствуют требованиям CSL, другие языки могут не иметь возможности ис­
пользовать их.
Таблица 3.4. Внутренние типы данных C#
Сокращенный
Отвечает
вариант
ли требова­ Системный тип
обозначения в C# ниям CLS
bool
Да
System.Boolean
sbyte
Нет
System.SByte
Диапазон значений
Описание
true или f^lse
Представляет
признак истинно­
сти или ложности
ДО
о т -128
127
8-битное число
со знаком
byte
Да
System.Byte
от 0
до 255
8-битное число
без знака
short
Да
System.Intl6
от -3 2 768
до 32 767
16-битное число
со знаком
ushort
Нет
System.UIntl6
от 0
до 65 535
16-битное число
без знака
int
Да
System.Int32
от -2 147 483 648
до 2 147 483 647
32-битное число
со знаком
uint
Нет
System.UInt32
отО
до 4 294 967 295
32-битное число
без знака
long
Да
System.Int64
от -9 223 372 036 854
775 808 до
9 223 372 036 854 775 807
64-битное число
со знаком
ulong
Нет
System.UInt64
от 0 до
18 446 744073 709 551 615
64-битное число
без знака
char
Да
System.Char
от U+0000
до U+ffff
Одиночный
16-битный символ
Unicode
float
Да
System.Single
о т +1,5x10 -45
до 3,4x1038
32-битное число с
плавающей точкой
double
Да
System.Double
от 5,0x10-324
до 1,7x10308
64-битное число с
плавающей точкой
decimal
Да
System.Decimal
от ±1,0x10е-28
до ±7,9x1028
96-битное число
со знаком
Глава 3. Главные конструкции программирования на С#: часть I
119
Окончание табл. 3.4
Сокращенный
Отвечает
вариант
ли требова- Системный тип
обозначения в C# . ниям CLS
Диапазон значений
Описание
string
Да
System.String
Ограничивается объе­
мом системной памяти
Представляет ряд
символов в фор­
мате Unicode
object
Да
System.Obj ect
Позволяет сохранять
любой тип в объектной
переменной
Служит базовым
классом для всех
типов в мире .NET
На заметку! По умолчанию число с плавающей точкой трактуется как относящееся к типу double.
Из-за этого для объявления переменной типа float сразу после числового значения должен
указываться суффикс f или F (например, 5.3F). Неформатированные целые числа по умолча­
нию трактуются как относящиеся к типу данных int. Поэтому для типа данных long необхо­
димо использовать суффикс 1 или L (например, 4L).
Каждый из числовых типов, такой как short или int, отображается на соответст­
вующую структуру в пространстве имен System. Структуры, попросту говоря, пред­
ставляют собой типы значений, которые размещаются в стеке. Типы string и object,
с другой стороны, являются ссылочными типами, а это значит, что данные, сохраняе­
мые в переменных такого типа, размещаются в управляемой куче. Более подробно о
типах значений и ссылочных типах будет рассказываться в главе 4. Пока что важно
просто понять то, что типы-значения могут размещаться в памяти очень быстро и об­
ладают фиксированным и предсказуемым временем жизни.
,
Объявление и инициализация переменных
При объявлении локальной переменой (например, переменной, действующей в пре­
делах какого-то члена) должен быть указан тип данных, за которым следует имя самой
переменной. Чтобы посмотреть, как это выглядит, давайте создадим новый проект типа
C onsole A p p lic a tio n по имени BasicDataTypes и модифицируем класс Program так, что­
бы в нем использовался следующий вспомогательный метод, вызываемый в Main () :
static void LocalVarDeclarations ()
{
Console.WriteLine ("=> Data Declarations:");
// Объявления данных
// Локальные переменные объявляются следующим образом:
// типДанных имяПеременной;
int mylnt;
string myStnng;
Console.WriteLine() ;
)
Следует иметь в виду, что в случае использования локальной переменной до при­
сваивания ей начального значения компилятор сообщит об ошибке. Поэтому рекомен­
дуется всегда присваивать начальные значения локальным элементам данных во время
их объявления. Делать это можно как в одной строке, так и в двух, разнося объявление
и присваивание на два отдельных оператора кода.
static void LocalVarDeclarations()
{
Console.WriteLine("=> Data Declarations:");
// Объявления данных
120
Часть II. Главные конструкции программирования на C#
// Локальные переменные объявляются и инициализируются следующим образом:
// тилДанных имяПеременной = начальноеЗначение;
int mylnt = 0;
// Объявлять локальные переменные и присваивать им начальные
// значения можно также в двух отдельных строках.
string myString;
my S t n n g = "This is my character data";
Console.WriteLine ();
}
Допускается объявление сразу нескольких переменных одинакового базового типа в
одной строке кода, как показано ниже на примере трех переменных типа bool:
static void LocalVarDeclarations ()
{
Console.WriteLine ("=> Data Declarations:");
int mylnt = 0;
string myString;
myString = "This is my character data";
// Объявление трех переменных типа bool в одной строке.
bool Ы = true, Ь2 = false, ЬЗ = Ы ;
Console.WriteLine();
}
Поскольку ключевое слово bool в C# является сокращенным вариантом обозначе­
ния такой структуры из пространства имен System, как Boolean, размещать любой тип
данных можно также с использованием его полного имени (естественно, это же касает­
ся всех остальных ключевых слов, представляющих типы данных в С#). Ниже приведе­
на окончательная версия реализации LocalVarDeclarations ( ) .
static void LocalVarDeclarations ()
{
Console.WriteLine ("=> Data Declarations:");
Console.WriteLine("=> Объявления данных:");
// Локальные переменные объявляются и инициализируются следующим образом:
// типДанных имяПеременной = начальноеЗначение;
int mylnt = 0;
string myString;
myString = "This is my character data";
// Объявление трех переменных типа bool в одной строке.
bool Ы = true, Ь2 = false, ЬЗ = Ы ;
// Использование типа данных System для объявления переменной bool.
System .'Boolean Ь4 = false;
Console.WriteLine("Your data: {0}, {1}, {2}, {3}, {4}, {5}",
mylnt, myString, Ы , Ь2, ЬЗ, Ь4);
Console.WriteLine();
}
Внутренние типы данных и операция new
Все внутренние (intrinsic) типы данных поддерживают так называемый конструк­
тор по умолчанию (см. главу 5). Это позволяет создавать переменные за счет использо­
вания ключевого слова new и тем самым автоматически устанавливать для них значе­
ния, которые являются принятыми для них по умолчанию:
Глава 3. Главные конструкции программирования на С#: часть I
121
• значение f a l s e для переменных типа b o o l;
• значение 0 для переменных числовых типов (или 0. О для типов с плавающей
точкой);
• одиночный пустой символ для переменных типа s t r in g ;
• значение 0 для переменных типа B ig ln t e g e r ;
• значение 1/1/0001 12 : 00 : 00 AM для переменных типа DateTime;
• значение n u ll для переменных типа объектных ссылок (включая s t r in g ).
На заметку! Упомянутый в предыдущем списке тип данных Biglnteger является нововведением
в .NET 4.0 и будет более подробно рассматриваться далее в главе.
Хотя код на C# в случае использования ключевого слова new при создании перемен­
ных базовых типов получается более громоздким, синтаксически он вполне корректен,
как, например, приведенный ниже код:
static void NewingDataTypes ()
{
Console.WriteLine ("=> Using new to create variables:");
// Использование ключевого слова
// new для создания переменных
bool b = new bool ();
// Установка в false,
int i = new int();
// Установка в 0.
double d = new double ();
// Установка в 0.
DateTime dt = new DateTimeO;
// Установка в 1/1/0001 12:00:00 AM
Console.WriteLine ("{0 }, {1}, {2}, {3}", b, l, d, dt) ;
Console.WriteLine ();
}
Иерархия классов типов данных
Очень интересно отметить то, что даже элементарные типы данных в .NET имеют
вид иерархии классов. Если вы не знакомы с концепциями наследования, ищите всю
необходимую информацию в главе 6. Пока важно усвоить лишь то, что типы, которые
находятся в самом верху иерархии, обеспечивают некоторое поведение по умолчанию,
которое передается унаследованным от них типам. На рис. 3.6 схематично показаны
отношения между ключевыми системными типами.
Обратите внимание, что каждый из этих типов в конечном итоге наследуется от
класса System.Object, в котором содержится набор методов (таких как ToString (),
Equals () и GetHashCode ()), являющихся общими для всех поставляемых в библиоте­
ках базовых классов .NET типов (все эти методы подробно рассматриваются в главе 6).
Также важно отметить, что многие из числовых типов данных унаследованы от
класса System.ValueType. Потомки ValueType автоматически размещаются в стеке
и потому обладают очень предсказуемым временем жизни и являются довольно эф­
фективными. Типы, у которых в цепочке наследования не присутствует класс System.
ValueType (вроде System.Type, System.String, System.Array, System.Exception и
System. Delegate), в стеке не размещаются, а попадают в кучу и подвергаются автома­
тической сборке мусора.
Не погружаясь глубоко в детали классов System.Ob ject и System.ValueType,
сейчас главное уяснить то, что поскольку любое ключевое слово в C# (например, int)
представляет собой сокращенный вариант обозначения соответствующего системного
типа (в данном случае System. Int32), п р и в ед еты й ниже синтаксис является вполне
допустимым.
122
Часть II. Главные конструкции программирования на C#
Рис. 3.6. Иерархия классов системных типов
Причина в том, что тип System . In t32 (представляемый как in t в С#) в конечном
итоге все равно унаследован от класса S y stem .O b ject и, следовательно, в нем может
вызываться любой из его общедоступных членов с помощью такой вспомогательной
функции.
static void ObjectFunctionality()
{
Console .WnteLine ("=> System. Object Functionality:");
// Функциональные возможности System.Object
// Ключевое слово in t в C# в действительности представляет
// собой сокращенный вариант обозначения типа System.Int32,
// который наследует от System.Object следующие члены:
Console.WriteLine("12.GetHashCode() = {0}", 12.GetHashCode());
Console.WnteLine ("12 .Equals (23) = {0}", 12.Equals(23)) ;
Console .WriteLine (" 12 .ToStnng () = {0}", 12 .ToString () );
Console.WriteLine("12.GetType() = {0}", 12.GetType());
Console.WriteLine();
Глава 3. Главные конструкции программирования на С#: часть I
123
На рис. 3.7 показано, как будет выглядеть вывод в случае вызова данного метода в
Main () .
| С M ncb*i'jyitem 32\cm d,exe
***** Fun with Basic Data Types *****
!=> System.Object Functionality:
12.GetHashCodeO = 12
12.Equals(23) = False
12 .ToStringO = 12
L2.GetType() = System.Int32
Press any key to continue . . .
Рис. 3.7. Все типы (даже числовые) расширяют класс System.Object
Члены числовых типов данных
Продолжая обсуждение встроенных типов данных в С#, нельзя не упомянуть о том,
что числовые типы в .NET поддерживают свойства MaxValue и MinValue, которые по­
зволяют получать информацию о диапазоне значений, хранящихся в данном типе.
Помимо свойств MinValue/MaxValue каждый числовой тип может иметь и другие по­
лезные члены. Например, тип System.Double позволяет получать значения эпсилон
(бесконечно малое) и бесконечность (представляющие интерес для тех, кто занимается
решением математических задач). Ниже для иллюстрации приведена соответствующая
вспомогательная функция.
static void DataTypeFunctionality ()
{
Console .WnteLine ("=> Data type Functionality:");
// Функциональные возможности типов данных
// Максимальное значение типа int
Console .WnteLine ("Max of int: {0}", int .MaxValue) ;
// Минимальное значение типа int
Console .WnteLine ("Min of int: {0}", int.MinValue);
// Максимальное значение типа double
Console.WriteLine("Max of double: {0}", double.MaxValue);
// Минимальное значение типа double
Console.WriteLine("Min of double: {0}", double.MinValue) ;
// Значение эпсилон типа double
Console.WriteLine("double.Epsilon: {0}", double.Epsilon);
// Значение плюс бесконечность типа double
Console.WriteLine("double.Positivelnfinity: {0}",
double.Positivelnfinity) ;
// Значение минус бесконечность типа double
Console.WriteLine("double.NegativeInfinity: {0}",
double.Negativelnfinity);
Console.WriteLine();
}
Члены S y s t e m . B o o l e a n
Теперь рассмотрим тип данных System.Boolean. Единственными значениями, ко­
торые могут присваиваться типу bool в С#, являются true и false. Нетрудно догадать­
ся, что свойства MinValue и MaxValue в нем не поддерживаются, но зато поддержива­
ются такие свойства, как TrueString и FalseString (которые выдают, соответственно,
124
Часть II. Главные конструкции программирования на C#
строку "True" и "False"). Чтобы стало понятнее, давайте доб^авим во вспомогательный
метод DataTypeFunctionality () следующие операторы кода:
Console.WnteLine ("bool.Falsest ring: {0 }" , bool.Falsest ring) ;
Console .WnteLine ("bool.T r u e S t n n g : {0}"f bool.TrueStnng) ;
На рис. 3.8 показано, как будет вы глядеть вывод после вызова DataType
Functionality () BMain().
Рис. 3.8. Демонстрация избранных функциональных
возможностей различных типов данных
Члены S y s t e m . C h a r
Текстовые данные в C# представляются с помощью ключевых слов string и char,
которые являются сокращенными вариантами обозначения типов System. String и
System.Char (оба они основаны на кодировке Unicode). Как известно, string позволя­
ет представлять непрерывный набор символов (например, "Hello"), a char — только
конкретный символ в типе string (например, ' Н ').
Помимо возможности хранить один элемент символьных данных, тип System. Char
обладает массой других функциональных возможностей. В частности, с помощью его
статических методов можно определять, является данный символ цифрой, буквой, зна­
ком пунктуации или чем-то еще. Например, рассмотрим приведенный ниже метод.
static void CharFunctionality ()
{
Console .WnteLine ("=> char type Functionality:");
// Функциональные возможности типа char
char myChar = 'a';
Console .WnteLine ("char .IsDigit (' a ') : {0}",
char.IsDigit(myChar));
Console .WnteLine ("char .IsLetter ('a ') : {0}",
char.IsLetter(myChar));
Console .WnteLine ("char .IsWhiteSpace (1Hello There 1, 5): {0}",
char.IsWhiteSpace("Hello There", 5));
Console .WnteLine ("char .IsWhiteSpace ('Hello There ', 6): {0}",
char.IsWhiteSpace("Hello There", 6));
Console .WnteLine ("char.IsPunctuation ('?' ) : {0}",
char.IsPunctuation('?'));
Console .WnteLine () ;
}
Как видно в этом фрагменте кода, многие из членов System.Char могут вызывать­
ся двумя способами: с указанием конкретного символа и с указанием целой строки с
числовым индексом, ссылающимся на позицию, в которой находится проверяемый
символ.
Глава 3. Главные конструкции программирования на С#: часть I
125
Синтаксический разбор значений из строковых данных
Типы данных .NETT предоставляют возможность генерировать переменную лежаще­
го в их основе типа на основе текстового эквивалента (т.е. выполнять синтаксический
разбор). Эта возможность чрезвычайно полезна при преобразовании некоторых из пре­
доставляемых пользователем данных (например, значений, выбираемых в раскрываю­
щемся списке внутри графического пользовательского интерфейса). Ниже приведен
пример метода ParseFromStrings (), демонстрирующий выполнение синтаксического
разбора.
static void ParseFromStrings ()
{
Console.WriteLine ("=> Data type parsing:");
// Синтаксический разбор типов данных
bool b = bool.Parse("True");
Console.WriteLine("Value of b: {0}", b) ;
double d = double.Parse("99.884");
Console.WriteLine("Value of d: {0}", d) ;
int i = int.Parse("8") ;
Console.WriteLine("Value of i : {0}", l) ;
char c = Char.Parse("w") ;
Console.WriteLine("Value of c: {0}", c) ;
Console.WriteLine();
}
Типы S y s t e m . D a t e T i m e и S y s t e m . T im e S p a n
В пространстве имен System имеется несколько полезных типов данных, для кото­
рых в C# ключевых слов не предусмотрено. К этим типам относятся структуры DateTime
и TimeSpan (а также показанные на рис. 3.6 типы System. Guid и System. Void, изуче­
нием которых можете заняться самостоятельно).
В типе DateTime содержатся данные, представляющие конкретное значение даты
(месяц, день, год) и времени, которые могут форматироваться различными способами с
применением членов, доступных в этом типе.
static void UseDatesAndTimes()
{
Console.WriteLine ("=> Dates and Times:");
// Отображение значений даты и времени
// Этот конструктор принимает в качестве
// аргументов сведения о годе, месяце и дне.
DateTime dt = new DateTime (2010, 10, 17);
// Какой это день месяца?
Console.WriteLine("The day of {0} is {1}", dt.Date, d t .DayOfWeek);
// Сейчас месяц декабрь.
dt = dt .AddMonths(2);
Console.WriteLine("Daylight savings: {0}",
dt.IsDaylightSavingTime());
// Этот конструктор принимает в качестве аргументов
// сведения о часах, минутах и секундах.
TimeSpan ts = new TimeSpan (4, 30, 0) ;
Console.WriteLine(ts) ;
// Вычитаем 15 минут из текущего значения TimeSpan
//и отображаем результат.
Console.WriteLine(ts.Subtract(new TimeSpan (0, 15, 0) )) ;
}
126
Часть II. Главные конструкции программирования на C#
Пространство имен S y s t e m . N u m e r i c s в .NET 4.0
В версии .NET 4.0 предлагается новое пространство имен под названием System.
Numerics, в котором определена структура по имени Biglnteger. Тип данных
Biglnteger служит для представления огромных числовых значений (вроде националь­
ного долга США), не ограниченных ни верхней, ни нижней фиксированной границей.
На заметку! В пространстве System.Numerics доступна и вторая структура по имени Complex,
которая позволяет моделировать математически сложные числовые данные (такие как мнимые
и реальные числа, или гиперболические тангенсы). Дополнительные сведения об этой структу­
ре можно найти в документации .NET Framework 4.0 SDK.
Хотя в большинстве приложений .NET необходимость в использовании структуры
Biglnteger может никогда не возникать, если все-таки это случится, в первую очередь
в свой проект нужно добавить ссылку на сборку System.Numerics .dll. Выполните сле­
дующие шаги.
1. В Visual Studio выберите в меню P ro je c t (Проект) пункт A dd R e fe ren ce (Добавить
ссылку).
2. В открывшемся после этого окне перейдите на вкладку .NET.
3. Найдите и выделите сборку S y s t e m .Numerics в списке представленных
библиотек.
4. Щелкните на кнопке ОК.
После этого добавьте следующую директиву в файл, в котором будет использоваться
тип данных Biglnteger:
// Здесь определен тип B igln teger:
using System.Numerics;
Теперь можно создать переменную Biglnteger с использованием операции new.
Внутри конструктора можно указать числовое значение, в том числе с плавающей точ­
кой. Вспомните, что при определении целочисленный литерал (вроде 500) по умолчанию
трактуется исполняющей средой как относящийся к типу int, а литерал с плавающей
точкой (такой как 55.333) — как относящийся к типу double. Как же тогда установить
для Biglnteger большое значение, не переполняя типы данных, которые используются
по умолчанию для неформатированных числовых значений?
Простейший подход состоит в определении большого числового значения в виде
текстового литерала, который затем может быть преобразован в переменную типа
Biglnteger с помощью статического метода Parse ( ) . При необходимости можно так­
же передать байтовый массив непосредственно конструктору класса Biglnteger.
На заметку! После присваивания значения переменной Biglnteger изменять ее больше не раз­
решается, поскольку содержащиеся в ней данные являются неизменяемыми. Тем не менее, в
классе Biglnteger предусмотрено несколько членов, которые возвращают новые объекты
Biglnteger на основе модификаций над данными (вроде статического метода Multiply (),
который будет использоваться в следующем примере кода).
В любом случае после определения переменной Biglnteger обнаруживается, что в
этом классе доступны члены, очень похожие на членов других внутренних типов дан­
ных в C# (например, float и int). Помимо этого в классе Biglnteger еще есть несколь­
ко статических членов, которые позволяют применять базовые математические выра­
жения (такие как сложение и умножение) к переменным Biglnteger.
Глава 3. Главные конструкции программирования на С#: часть I
127
Ниже приведен пример работы с классом Biglnteger.
static void UseBiglnteger ()
Console .WnteLine ("=> Use Biglnteger:");
// Использование Biglnteger
Biglnteger biggy =
Biglnteger.Parse("9999999999999999999999999999999999999999999999");
Console .WnteLine ("Value of biggy is {0}", biggy);
// Значение переменной biggy
Console .WnteLine (" Is biggy an even value?: {0}", biggy.IsEven);
// Является ли значение biggy четным?
Console.WriteLine("Is biggy a power of two?: {0}", biggy.IsPowerOfTwo);
// Является ли biggy степенью двойки?
Biglnteger reallyBig = Biglnteger.Multiply(biggy,
Biglnteger.Parse("8888888888888888888888888888888888688888888")) ;
Console.WriteLine("Value of reallyBig is {0}", reallyBig);
// Значение переменной reallyBig
}
Важно обратить внимание, что к типу данных Biglnteger применимы внутренние
математические операции в С#, такие как +, -, и *. Следовательно, вместо того чтобы
вызывать метод Biglnteger .Multiply () для перемножения двух больших чисел, мож­
но использовать такой код:
Biglnteger reallyBig2 = biggy * reallyBig;
К этому моменту уже должно быть ясно, что ключевые слова, представляющие ба­
зовые типы данных в С#, обладают соответствующим типами в библиотеках базовых
классов .NETT, и что каждый из этих типов предоставляет определенные функциональ­
ные возможности. Подробные описания всех членов этих типов данных можно найти в
документации .NET Framework 4.0 SDK.
Исходный код. Проект BasicDataTypes доступен в подкаталоге Chapter 3.
Работа со строковыми данными
В System. String предоставляется набор методов для определения длины символь­
ных данных, поиска подстроки в текущей строке, преобразования символов из верхнего
регистра в нижний и наоборот, и т.д. В табл. 3.5 перечислены некоторые наиболее ин­
тересные члены этого класса.
Таблица 3.5. Избранные члены класса S y ste m . S t r i n g
Член
Описание
Length
Свойство, которое возвращает длину текущей строки
Compare()
Статический метод, который позволяет сравнить две строки
Contains ()
Метод, который позволяет определить, содержится ли в строке определен­
ная подстрока
Equals()
Метод, который позволяет проверить, содержатся ли в двух строковых объ­
ектах идентичные символьные данные
Format()
Статический метод, позволяющий сформатировать строку с использованием
других элементарных типов данных (например, числовых данных или других
строк) и обозначений типа { 0 } , о которых рассказывалось ранее в этой главе
128
Часть II. Главные конструкции программирования на C#
Окончание табл. 3.5
Член
Описание
Insert ()
Метод, который позволяет вставить строку внутрь другой определенной
строки
PadLeft()
PadRight()
Методы, которые позволяют дополнить строку какими-то символами,
соответственно, справа или слева
Remove()
Replace()
Методы, которые позволяют получить копию строки с соответствующими
изменениями (удалением или заменой символов)
Split ()
Метод, возвращающий массив string с присутствующими в данном экзем­
пляре подстроками внутри, которые отделяются друг от друга элементами из
указанного массива char или string
Trim()
Метод, который позволяет удалять все вхождения определенного набора
символов с начала и конца текущей строки
ToUpper()
ToLower()
Методы, которые позволяют создавать копию текущей строки в формате,
соответственно, верхнего или нижнего регистра
Базовые операции манипулирования строками
Работа с членами System. String выглядит так, как и следовало ожидать. Все, что
требуется — это просто объявить переменную типа string и воспользоваться предостав­
ляемой этим типом функциональностью через операцию точки. Однако при этом следу­
ет иметь в виду, что некоторые из членов System. String представляют собой статиче­
ские методы и потому должны вызываться на уровне класса (а не объекта). Для примера
давайте создадим новый проект типа C o n so le A p p lic a tio n по имени FunWithStrings, до­
бавим в него следующий метод и вызовем его в Маш () :
static void BasicStnngFunctionality ()
{
Console.WnteLine ("=> Basic String functionality:");
// Базовые функциональные возможности типа String
string firstName = "Freddy";
Console .WnteLine ("Value of firstName: {0}", firstName);
// Значение переменной firstName
Console.WriteLine("firstName has {0} characters.", firstName.Length);
// Длина значения переменной firstname
Console .WnteLine ("firstName in uppercase: {0}", {0}", firstName.ToUpper());
// Значение переменной firstName в верхнем регистре
Console .WnteLine (" firstName in lowercase: {0}", {0}", firstName .ToLower ()) ;
// Значение переменной firstName в нижнем регистре
Console .WnteLine (" firstName contains the letter y? : {0}", {0}",
// Содержится ли в значении firstName буква у
firstName.Contains("у"));
Console .WnteLine ("f irstName after replace: {0}", firstName.Peplace ("dy", "") );
// Значение firstName после замены
Console .WnteLine () ;
}
Здесь объяснять особо нечего: в приведенном методе на локальной переменной типа
string производится вызов различных членов вроде ToUpper ( ) и Contains () для по­
лучения различных форматов и выполнения различных преобразований. На рис. 3.9
показано, как будет выглядеть вывод.
Глава 3. Главные конструкции программирования на С#: часть I
129
|С W<rvck>*j»iyrtem&Z\crr\dе»е
***** Fun with Strings *****
=> Basic String functionality:
Value of firstName: Freddy
firstName has 6 characters,
jfirstName in uppercase: FREDDY
l firstName in lowercase: freddy
(firstName contains the letter y?: True
firstName after replace: Fred
Рис. 3.9. Базовые операции манипулирования строками
Вывод, получаемый в результате вызова метода R e p la c e ( ) , может привести в не­
которое замешательство. Переменная fir s tN a m e на самом деле не изменилась, а вме­
сто этого метод возвращает обратно новую строку в измененном формате. Мы еще вер­
немся к неизменяемой природе строк немного позже, после исследования ряда других
моментов.
Конкатенация строк
Переменные s t r in g могут сцепляться вместе для создания строк большего размера
с помощью такой поддерживаемой в C# операции, как +. Как известно, подобный прием
называется конкатенацией строк. Для примера рассмотрим следующую вспомогатель­
ную функцию:
static void StringConcatenation ()
Console.WriteLine ("=> String concatenation:");
// Конкатенация строк
string si = "Programming the ";
string s2 = "PsychoDrill (PTP)";
string s3 = si + s2;
Console.WriteLine (s3) ;
Console.WriteLine ();
}
Возможно, будет интересно узнать, что символ + в C# при обработке компилятором
приводит к добавлению вызова статического метода S t r i n g . Concat ( ) . После компиля­
ции приведенного выше кода и открытия результирующей сборки в утилите ild a s m . ехе
(см. главу 1) можно будет увидеть такой CIL-код, как показан на рис. 3.10.
Рис. 3.10. Операция + в C# приводит к добавлению вызова метода String.Concat ()
По этой причине конкатенацию строк также можно выполнять вызовом метода
String.Concat () напрямую (что на самом деле особых преимуществ не дает, а факти­
чески лишь увеличивает количество подлежащих вводу строк кода):
130
Часть II. Главные конструкции программирования на C#
static void StringConcatenation ()
{
Console .WnteLine ("=> String concatenation:");
string si = "Programming the ";
string s2 = "PsychoDrill (PTP)";
t
strin g s3 = S trin g .C o n c a t(s i, s2) ;
Console .WnteLine (s3) ;
Console.WriteLine();
}
Управляющие последовательности символов
Как и в других языках на базе С, в C# строковые литералы могут содержать раз­
личные управляющие последовательности символов (escape characters), которые по­
зволяют уточнять то, как символьные данные должны выводиться в выходном потоке.
Начинается каждая такая управляющая последовательность с символа обратной косой
черты, за которым следует интерпретируемый знак. В табл. 3.6 перечислены некоторые
часто применяемые управляющие последовательности.
Таблица 3.6. Управляющие последовательности, которые могут применяться
в строковых литералах
Управляющая
последовательность
Описание
V
Вставляет в строковый литерал символ одинарной кавычки
V
Вставляет в строковый литерал символ двойной кавычки
\\
Вставляет в строковый литерал символ обратной косой черты. Может
быть полезной при определении путей к файлам и сетевым ресурсам
\а
Заставляет систему выдавать звуковой сигнал, который в консольных при­
ложениях может служить своего рода звуковой подсказкой пользователю
\п
Вставляет символ новой строки (на платформах Windows)
\г
Вставляет символ возврата каретки
\t
Вставляет в строковый литерал символ горизонтальной табуляции
Например, если необходимо, чтобы в выводимой строке после каждого слова шел
символ табуляции, можно воспользоваться управляющей последовательностью \t. Если
нужно создать один строковый литерал с символами кавычек внутри, другой — с опре­
делением пути к каталогу и третий со вставкой трех пустых строк после вывода сим­
вольных данных, можно применить такие управляющие последовательности, как \", \\
и \п. Кроме того, ниже приведен еще один пример, в котором для привлечения внима­
ния каждый строковый литерал снабжен звуковым сигналом.
static void EscapeChars ()
{
Console.WriteLine ("=> Escape characters:\a");
string strWithTabs = "Model\tColor\tSpeed\tPet Name\a ";
Console.WriteLine(strWithTabs);
Console.WriteLine ("Everyone loves V'Hello World\"\a ");
Console .WnteLine ("C :\\MyApp\\bin\ \Debug\a ") ;
// Добавить 4 пустых строки и снова выдать звуковой сигнал.
Console.WriteLine("All finished.\n\n\n\a ");
Console.WriteLine ();
Глава 3. Главные конструкции программирования на С#: часть I
131
Определение дословных строк
За счет добавления к строковому литералу префикса @ можно создавать так назы­
ваемые дословные строки (verabtim string). Дословные строки позволяют отключать об­
работку управляющих последовательностей в литералах и выводить объекты string в
том виде, в каком они есть. Эта возможность наиболее полезна при работе со строками,
представляющими пути к каталогам и сетевым ресурсам. Таким образом, вместо ис­
пользования управляющей последовательности \ \ можно написать следующий код:
// Следующая строка будет воспроизводиться дословно,
// т .е. с отображением всех управляющих
// последовательностей символов.
Console.WriteLine(@"С:\MyApp\bin\Debug");
Также важно отметить, что дословные строки могут применяться для сбережения
пробелов в строках, разнесенных на несколько строк:
// При использовании дословных строк пробелы сохраняются.
string myLongStnng = @"This is a very
very
very
long string";
Console.WriteLine(myLongString);
С использованием дословных строк можно также напрямую вставлять в литералы
символы двойной кавычки, просто дублируя лексему ":
Console.WriteLine (@"Cerebus said ""Darrri Pret-ty sun-sets""");
Строки и равенство
Как будет подробно объясняться в главе 4, ссылочный тип (reference type) представ­
ляет собой объект, размещаемый в управляемой куче, которая подвергается автоматиче­
скому процессу сборки мусора. По умолчанию при выполнении проверки на предмет ра­
венства ссылочных типов (с помощью таких поддерживаемых в C# операций, как == и ! =)
значение true будет возвращаться в том случае, если обе ссылки указывают на один
и тот же объект в памяти. Хотя string представляет собой ссылочный тип, операции
равенства для него были переопределены так, чтобы давать возможность сравнивать
значения объектов string, а не сами объекты в памяти, на которые они ссылаются.
static void StnngEquality ()
{
Console.WriteLine("=>
//
string si = "Hello!";
string s2 = "Yo1";
Console.WriteLine ("si
Console.WriteLine ("s2
Console.WriteLine ();
String equality:");
Равенство строк
= {0}", si);
= {0}", s2) ;
// Выполнение проверки на предмет равенства данных строк.
Console.WriteLine ("si
== s2: {0}", si == s2);
Console.WriteLine ("si
== Hello!: {0}", si == "Hello!");
Console.WriteLine ("si
== HELLO!: {0}", si == "HELLO!");
Console.WriteLine ("si
== hello!: {0}", si == "hello!");
Console.WriteLine("si.Equals (s2) : {0}", si.Equals(s2)) ;
Console.WriteLine("Yo.Equals (s2) : {0}", "Y o !".Equals(s2));
Console.WriteLine();
}
132
Часть II. Главные конструкции программирования на C#
В C# операции равенства предусматривают выполнение в отношении объектов
string посимвольной проверки с учетом регистра. Следовательно, строки "Hello! ",
"HELLO ! " и "hello ! " не равны между собой. Кроме того, из-за наличия у string связи
с System. String, проверку на предмет равенства можно выполнять также с помощью
поддерживаемого классом String метода Equals () и других поставляемых в нем опе­
раций. И, наконец, поскольку каждый строковый литерал (например, "Y o ") является
самым настоящим экземпляром System.String, доступ к функциональным возможно­
стям для работы со строками можно получать также для фиксированной последователь­
ности символов.
Неизменная природа строк
Один из интересных аспектов System.String состоит в том, что после присваивания
объекту string первоначального значения символьные данные больше изменяться не
могут. На первый взгляд это может показаться заблуждением, ведь строкам постоянно
присваиваются новые значения, а в типе System. String доступен набор методов, кото­
рые, похоже, только то и делают, что позволяют изменять символьные данные тем или
иным образом (например, преобразовывать их в верхний или нижний регистр). Если,
однако, присмотреться внимательнее к тому, что происходит “за кулисами”, то можно
будет увидеть, что методы типа string на самом деле возвращают совершенно новый
объект string в измененном виде:
static void StringsArelmmutable ()
{
// Установка первоначального значения для строки.
string si = "This is my string.";
Console .WnteLine ("si = {0}", si) ;
// Преобразование s i в верхний регистр?
string upperstring = si.ToUpper();
Console .WnteLine ("upperstring = {0}", upperstring);
// Нет! s i по-прежнему остается в том же формате!
Console.WriteLine("si = {0}", si);
}
На рис. 3.11 показано, как будет выглядеть вывод приведенного выше кода, по кото­
рому легко убедиться в том, что исходный объект string (si) не преобразуется в верх­
ний регистр при вызове ToUpper ( ) , а вместо этого возвращается его копия в изменен­
ном соответствующим образом формате.
щ
С v W t r v d c ^ i ' о»ч1е»е
*****
Fun with Strings *****
si = This is my string,
upperstring = THIS IS MY STRING,
^sl = This is my string.
Press any key to continue . . . e
Рис. 3.11. Строки остаются неизменными
Тот же самый закон неизменности строк действует и при использовании в C# опе­
рации присваивания. Чтобы удостовериться в этом, давайте закомментируем (или уда­
лим) весь существующий код в StringsArelmmutable () (чтобы уменьшить объем гене­
рируемого CIL-кода) и добавим следующие операторы:
Глава 3. Главные конструкции программирования на С#: часть I
133
static void StringArelmmutable()
{
string s2 = "My other string";
s2 = "New string value";
}
Скомпилируем прилож ение и загрузим результирую щ ую сборку в ут и ли т у
ildasm.exe (см. главу 1). На рис. 3.12 показан CIL-код, который будет сгенерирован
для метода StringsArelmmutable ().
Рис. 3.12. Присваивание значения объекту string приводит
к созданию нового объекта string
Хотя низкоуровневые детали CIL пока подробно не рассматривались, важно обратить
внимание на наличие многочисленных вызов кода операции l d s t r (загрузка строки).
Этот код операции ldstr в CIL предусматривает выполнение загрузки нового объекта
string в управляемую кучу. В результате предыдущий объект, в котором содержалось
значение "Му other string", будет в конечном итоге удален сборщиком мусора.
Так что же конкретно необходимо вынести из всего этого? Если кратко: класс string
может оказываться неэффективным и приводить к “разбуханию” кода в случае непра­
вильного использования, особенно при выполнении конкатенации строк. Когда же не­
обходимо представлять базовые символьные данные, такие как номер карточки соци­
ального страхования, имя и фамилия или простые фрагменты текста, используемые
внутри приложения, он является идеальным вариантом.
В случае создания приложения, предусматривающего интенсивную работу с тексто­
выми данными, представление обрабатываемых данных с помощью объектов string
будет очень плохой идеей, поскольку практически наверняка (и часто не напрямую)
будет приводить к созданию ненужных копий данных string. Как тогда должен посту­
пать программист? Ответ на этот вопрос ищите ниже.
Тип S y s t e m . T e x t . S t r i n g B u i l d e r
Из-за того, что тип string может оказаться неэффективным при необдуманном ис­
пользовании, в библиотеках базовых классов .NET поставляется еще пространство имен
System.Text. Внутри этого (достаточно небольшого) пространства имен предлагается
класс по имени StringBuilder. Как и в классе System. String, в StringBuilder со­
держатся методы, которые позволяют, например, заменять и форматировать сегменты.
Чтобы использовать этот класс в файлах кода на С#, первым делом необходимо позабо­
титься об импорте следующего пространства имен:
// Здесь определен класс Strin gB u ilder:
using System.Text;
134
Часть II. Главные конструкции программирования на C#
Уникальным в StringBuilder является то, что при вызове его членов производит­
ся непосредственное изменение внутренних символьных данных объекта (что, конечно
же, более эффективно), а не получение копии этих данных в измененном формате. При
создании экземпляра StringBuilder начальные значения для объекта можно задавать
с помощью не одного, а нескольких конструкторов. Тем, кто не знаком с понятием кон­
структора, сейчас важно уяснить лишь то, что конструкторы позволяют создавать объ­
ект с определенным начальным состоянием за счет применения ключевого слова new.
Ниже приведен пример применения StringBuilder.
static void FunWithStringBuilder ()
{
Console .WnteLine ("=> Using the StringBuilder:");
StringBuilder sb = new StringBuilder("**** Fantastic Games ****");
sb.Append("\n");
sb.AppendLine("Half Life");
sb.AppendLine("Beyond Good and Evil");
sb.AppendLine("Deus Ex 2");
sb.AppendLine("System Shock");
Console.WriteLine (sb.ToStnng () );
sb.Replace ("2", "Invisible War");
Console .WriteLine (sb.ToStnng ());
Console.WriteLine("sb has {0} chars.", sb.Length); .
Console.WriteLine();
}
Здесь сначала создается объект StringBuilder с первоначальным значением
и * * * * Fantastic Games****". Далее можно добавлять символы к внутреннему буфе­
ру, а также заменять (или удалять) каким угодно образом. По умолчанию изначально в
StringBuilder может храниться строка длиной не более 16 символов (она автомати­
чески расширяется по мере необходимости), однако это исходное значение легко изме­
нить, передавая конструктору соответствующий дополнительный аргумент:
//
С о з д а н и е о б ъ е к т а S t s i n g B u i l d e r с и схо дн ы м
/ / р азм ер ом в 2 5 6 си м во л о в.
StringBuilder sb = new StringBuilder("**** Fantastic Games ****", 256);
В случае добавления большего количества символов, чем было указано в качестве
лимита, объект StringBuilder будет копировать свои данные в новый экземпляр и
создавать для него буфер размером, равным указанному лимиту. На рис. 3.13 показан
вывод приведенной выше вспомогательной функции.
"я С WindOA;\S)-item3?vOTxJ.e«
***** Fun with Strings *****
*j
>=> Using the StringBuilder:
**** Fantastic Games ****
Half Life
Beyond Good and Evil
Deus Ex 2
System Shock
**** Fantastic Games ****
Half Life
Beyond Good and Evil
Deus Ex Invisible War
System Shock
sb as 96 chars.
Рис. 3.13. Класс StringBuilder работает
более эффективно, чем string
Глава 3. Главные конструкции программирования на С#: часть I
135
Исходный код. Проект FunWithStrings доступен в подкаталоге Chapter 3.
Сужающие и расширяющие
преобразования типов данных
Теперь, когда известно, как взаимодействовать со встроенными типами данных, да­
вайте рассмотрим связанную тему — преобразование типов данных. Создадим новый
проект типа C o nso le A p p lic a tio n по имени TypeConversions и определим в нем следую­
щий класс:
class Program
{
static void Main(string[] args)
{
Console .WnteLine ("***** Fun with type conversions *****");
// Добавление двух переменных типа short
/ / и отображение результата.
short numbl = 9, numb2 = 10;
Console.WriteLine("{0} + {1} = {2}", numbl, numb2, Add(numbl, numb2));
Console.ReadLine();
static int Add(int x, int y)
{ return x + y; }
}
Обратите внимание на то, что метод Add () ожидает поступления двух параметров
типа int. Тем не менее, в методе Main j ) ему на самом деле передаются две переменных
типа short. Хотя это может показаться несоответствием типов, программа будет ком­
пилироваться и выполняться без ошибок и возвращать в результате, как и ожидалось,
значение 19.
Причина, по которой компилятор будет считать данный код синтаксически коррект­
ным, связана с тем, что потеря данных здесь невозможна. Поскольку максимальное
значение (32 767), которое может содержать тип short, вполне вписывается в рамки
диапазона типа int (максимальное значение которого составляет 2 147 483 647), ком­
пилятор будет неявным образом расширять каждую переменную типа short до типа
int. Формально термин “расширение” применяется для обозначения неявного восходя­
щего приведения (upward cast), которое не приводит к потере данных.
На заметку! Расширяющие и сужающие преобразования, поддерживаемые для каждого типа дан­
ных в С#, описаны в разделе “Type Conversion Tables” ("Таблицы преобразования типов” ) доку­
ментации .NET Framework 4.0 SDK.
Хотя в предыдущем примере подобное неявное расширение типов было полезно,
в других случаях оно может стать источником возникновения ошибок компиляции.
Например, давайте установим для numbl и numb2 значения, которые (при сложении
вместе) будут превышать максимальное значение short, а также сделаем так, чтобы
значение, возвращаемое методом Add ( ) , сохранялось в новой локальной переменной
short, а не просто напрямую выводилось в окне консоли:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with type conversions *****");
136
Часть II. Главные конструкции программирования на C#
// Следующий код вызовет ошибку компиляции!
short numbl = 30000, numb2 = 30000;
short answer = Add(numbl, numb2);
Console.WnteLine ("{0} + {1} = {2}", numbl, numb2, answer);
Console.ReadLine ();
В таком случае компилятор сообщит об ошибке:
Cannot implicitly convert type 'int' to 'short'. An explicit conversion exists
(are you missing a cast?)
He удается неявным образом преобразоват ь тип ' i n t ' в ’ s h o r t ' . Существует
возможность выполнения п реобразова н и я явным образом (не была ли пропущена
операция по приведению типов?)
Проблема в том, что хотя метод Add () способен возвращать переменную i n t со
значением 60000 (поскольку это значение вполне вписывается в диапазон допустимых
значений типа S ystem . In t3 2 ), сохранение этого значения в переменной типа sh o rt
невозможно, потому что оно выходит за рамки допустимого диапазона для этого типа.
Формально это означает, что CLR-среде не удастся применить операцию сужения. Как
не трудно догадаться, операция сужения представляет собой логическую противопо­
ложность операции расширения, поскольку предусматривает сохранение большего зна­
чения внутри переменной меньшего типа данных.
Важно отметить, что все сужающие преобразования приводят к выдаче компилято­
ром ошибки, даже когда имеются веские основания полагать, что операция сужающе­
го преобразования должна на самом деле пройти успешно. Например, следующий код
тоже приведет к генерации компилятором ошибки:
// Еще один код, при выполнении которого
// компилятор будет сообщать об ошибке!
static void NarrowingAttempt()
{
byte myByte = 0;
int mylnt = 200;
myByte = mylnt;
Console .WnteLine ("Value of myByte: {0}", myByte);
}
Здесь значение, содержащееся в переменной типа in t (по имени m ylnt), вписыва­
ется в диапазон допустимых значений типа b y te , следовательно, операция сужения
по идее не должна приводить к генерации ошибки во время выполнения. Однако из-за
того, что язык C# создавался с таким расчетом, чтобы он заботился о безопасности ти­
пов, компилятор все-таки сообщит об ошибке.
Если нужно уведомить компилятор о готовности мириться с возможной в результате
операции сужения потерей данных, необходимо применить операцию явного приведе­
ния типов, которая в C# обозначается с помощью ( ) . Ниже показан модифицирован­
ный код Program, а на рис. 3.14 — его вывод.
class Program
{
static void Main(string[] args)
{
Console .WnteLine ("***** Fun with type conversions *****");
short numbl = 30000, numb2 = 30000;
// Явное приведение in t к short (с разрешением потери данных) .
short answer = (short)Add(numbl, numb2);
Console .WnteLine ("{0} + {1} = {2}", numbl, numb2, answer);
Глава 3. Главные конструкции программирования на С#: часть I
137
NarrowingAttempt();
Console.ReadLine();
}
static int Add(int x, int y)
{ return x .+ y; }
static void NarrowingAttempt()
{
byte myByte = 0;
int mylnt = 200 ;
// Явное приведение in t к byte (без потери данных).
myByte = (byte)mylnt;
Console .WnteLine ("Value of myByte: {0}", myByte);
}
Рис. 3.14. При сложении чисел некоторые данные были потеряны
Перехват сужающих преобразований данных
Как было только что показано, явное указание операции приведения заставляет
компилятор производить операцию сужающего преобразования даже тогда, когда это
чревато потерей данных. В методе NarrowingAttempt () это не было проблемой, по­
скольку значение 200 вписывается в диапазон допустимых значений типа byte. Однако
при сложении двух значений типа short в методе Main () конечный результат оказался
совершенно не приемлемым (30 000 + 30 000 = -5536?). Для создания приложений, в
которых потеря данных должна быть недопустимой, в C# предлагаются такие ключевые
слова, как checked и unchecked, которые позволяют гарантировать, что потеря данных
не окажется незамеченной.
Чтобы посмотреть, как применяются эти ключевые слова, давайте добавим в Program
новый метод, суммирующий две переменных типа byte, каждой из которых присвоено
значение, не выходящее за пределы допустимого максимума (255 для данного типа). По
идее, после сложения значений этих двух переменных (с приведением результата int к
типу byte) должна быть получена точная сумма.
static void ProcessBytes ()
{
byte bl = 10 0;
byte Ь2 = 250;
byte sum = (byte) Add (bl, b2);
// В sum должно содержаться значение 350.
// Однако там оказывается значение 94!
Console .WnteLine ("sum = {0}", sum);
}
Удивительно, но при изучении вывода данного приложения обнаруживается, что в
sum содержится значение 94 (а не 350, как ожидалось). Объясняется это очень просто.
Из-за того, что в System.Byte может храниться только значение из диапазона от 0 до
255 включительно, в sum будет помещено значение переполнения (350 - 256 = 94).
138
Часть II. Главные конструкции программирования на C#
По умолчанию, в случае, когда не предпринимается никаких соответствующих ис­
правительных мер, условия переполнения (overflow) и потери значимости (underflow)
происходят без выдачи ошибки. Обрабатывать условия переполнения и потери значи­
мости в приложении можно двумя способами. Это можно делать вручную, полагаясь на
свои знания и навыки в области программирования.
Недостаток такого подхода в том, что даже в случае приложения максимальных усилий
человек все равно остается человеком, и какие-то ошибки могут ускользнуть от его глаз.
К счастью, в C# предусмотрено ключевое слово checked. Если оператор (или блок
операторов) заключен в контекст checked, компилятор C# генерирует дополнительные
CIL-инструкции, обеспечивающие проверку на предмет условий переполнения, которые
могут возникать в результате сложения, умножения, вычитания или деления двух чи­
словых типов данных.
В случае возникновения условия переполнения во время выполнения будет генери­
роваться исключение System.OverflowException. Детали обеспечения структуриро­
ванной обработки исключения и использования в связи с этим ключевых слов try и
catch будут даны в главе 7. А пока, не вдаваясь особо в детали, можно изучить пока­
занный ниже модифицированный код:
static void ProcessBytes ()
{
byte Ы = 100;
byte Ъ 2 = 250;
// На этот раз компилятору указывается добавлять CIL-код,
// необходимый для выдачи исключения в случае возникновения
// условий переполнения или потери значимости.
try
byte sum = checked ((byte) Add (bl, b2));
Console.WnteLine ("sum = {0}", sum);
}
catch (OverflowException ex)
{
Console .WnteLine (ex.Message) ;
}
}
Здесь следует обратить внимание на то, что возвращаемое значение метода Add ()
было заключено в контекст checked. Благодаря этому, в связи с выходом значения sum
за пределы диапазона допустимых значений типа byte во время выполнения теперь
будет генерироваться исключение, а через свойство Message выводиться сообщение об
ошибке, как показано на рис. 3.15.
С WindcAj\jystem32\cmd.e*e
Fun with type conversions * * * * *
30000 + 30000 = -5536
Value of myByte: 200
Arithmetic operation resulted in an overflow.
*****
Press any key to continue . . .
Рис. 3.15. Ключевое слово checked вынуждает CLR-среду
генерировать исключения в случае потери данных
Если проверка на предмет возникновения условий переполнения должна выполнять­
ся не для одного, а для целого блока операторов, контекст checked можно определить
следующим образом:
Глава 3. Главные конструкции программирования на С#: часть I
139
try
{
checked
{
byte sum = (byte) Add (Ы, b2) ;
Console.WriteLine("sum = {0}", sum);
}
}
catch (OvertlowException ex)
{
Console.WriteLine(ex.Message);
}
И в том и в другом случае интересующий код будет автоматически проверяться на
предмет возникновения возможных условий переполнения, и при обнаружении тако­
вых приводить к генерации соответствующего исключения.
Настройка проверки на предмет возникновения
условий переполнения в масштабах проекта
Если создается приложение, в котором переполнение никогда не должно проходить
незаметно, может выясниться, что обрамлять ключевым словом checked приходится
раздражающе много строк кода. На такой случай в качестве альтернативного вариан­
та в компиляторе C# поддерживается флаг /checked. При активизации этого флага
проверке на предмет возможного переполнения будут автоматически подвергаться все
имеющиеся в коде арифметические операции, без применения для каждой из них клю­
чевого слова checked. Обнаружение переполнения точно так же приводит к генерации
соответствующего исключения во время выполнения.
Для активизации этого флага в Visual Studio 2010 необходимо открыть страни­
цу свойств проекта, перейти на вкладку Build (Сборка), щелкнуть на кнопке Advanced
(Дополнительно) и в открывшемся диалоговом окне отметить флажок Check for arithmetic
overflow/underflow (Выполнять проверку на предмет арифметического переполнения и
потери значимости), как показано на рис. 3.16.
Рис. 3.16. Включение функции проверки на предмет переполнения
и потери значимости в масштабах всего проекта
Ключевое слово u n c h e c k e d
Теперь давайте посмотрим, что можно сделать, если функция проверки на предмет
переполнения и потери значимости в масштабах всего проекта включена, но есть ка-
140
Часть II. Главные конструкции программирования на C#
кой-то блок кода, в котором потеря данных является допустимой. Из-за того, что дей­
ствие флага /checked распространяется на всю арифметическую логику, в C# преду­
смотрено ключевое слово unchecked, которое позволяет отключить выдачу связанного
с переполнением исключения в отдельных случаях. Применяется это ключевое слово
похожим на checked образом, поскольку может быть указано как для одного оператора,
так и для целого блока:
// При условии, что флаг /checked активизирован, этот
// блок не будет приводить к генерации исключения во время выполнения.
unchecked
{
byte sum = (byte) (bl + b2) ;
Console.WriteLine ("sum = { 0} ", sum);
}
Итак, чтобы подвести итог по использованию в C# ключевых слов ch eck ed и
unchecked, следует отметить, что по умолчанию арифметическое переполнение в ис­
полняющей среде .NET игнорируется. Если необходимо обработать отдельные операто­
ры, то должно использоваться ключевое слово checked, а если нужно перехватывать
все связанные с переполнением ошибки в приложении, то понадобится активизировать
флаг /checked. Что касается ключевого слова unchecked, то его можно применять при
наличии блока кода, в котором переполнение является допустимым (и, следовательно,
не должно приводить к генерации исключения во время выполнения).
Роль класса S y s t e m . C o n v e r t
В завершении темы преобразования типов данных стоит отметить, что в простран­
стве имен System имеется класс по имени Convert, который тоже может применяться
для расширения и сужения данных:
static void NarrowWithConvert ()
{
byte myByte = 0;
int mylnt = 200; .
myByte = Convert.ToByte(m ylnt);
Console.WriteLine ("Value of myByte: {0}", myByte);
}
Одно из преимуществ подхода с применением класса S ystem . C o n vert связано с
тем, что он позволяет выполнять преобразования между типами данных нейтральным
к языку образом (например, синтаксис приведения типов в Visual Basic полностью от­
личается от предлагаемого для этой цели в С#). Однако, поскольку в C# есть операция
явного преобразования, использование класса C onvert для преобразования типов дан­
ных обычно является делом вкуса.
Исходный код. Проект T yp eC on version s доступен в подкаталоге Chapter 3.
Неявно типизированные локальные переменные
Вплоть до этого момента в настоящей главе при определении локальных перемен­
ных тип данных, лежащий в их основе, всегда указывался явно:
static void DeclareExplicitVars ()
{
// Явно типизированные локальные переменные
// объявляются следующим образом:
// dataType vanableName = initialValue;
Глава 3. Главные конструкции программирования на С#: часть I
int mylnt = 0;
bool myBool = true;
string myString = "Time, marches o
n
141
;
}
Хотя указывать явным образом тип данных для каждой переменной всегда считается
хорошим стилем, в C# также поддерживается возможность неявной типизации локаль­
ных переменных с помощью ключевого слова va r. Ключевое слово v a r можно исполь­
зовать вместо указания конкретного типа данных (такого как in t , b o o l или s t r in g ).
В этом случае компилятор автоматически выводит лежащий в основе тип данных на
основе первоначального значения, которое используется для инициализации локаль­
ных данных.
Чтобы посмотреть, как это выглядит на практике, давайте создадим новый проект
типа C o nso le A p p lic a tio n (Консольное приложение) по имени Im p lic itly T y p e d L o c a lV a rs
и объявим в нем те же локальные переменные, что использовались в предыдущем мето­
де, следующим образом:
static void DeclarelmplicitVars ()
{
// Неявно типизированные локальные переменные объявляются следующим образом:
// var variableName = m it ia lV a lu e ;
var mylnt = 0;
var myBool = true;
var myString = "Time, marches on..." ;
}
На заметку! На самом деле лексема var ключевым словом в C# не является. С ее помощью можно
объявлять переменные, параметры и поля и не получать никаких ошибок на этапе компиляции.
При использовании этой лексемы в качестве типа данных, однако, она по контексту восприни­
мается компилятором как ключевое слово. Поэтому ради простоты здесь будет применяться
термин “ключевое слово var”,а не более сложное понятие “контекстная лексема var”.
В этом случае компилятор имеет возможность вывести по первоначально присвоен­
ному значению, что переменная mylnt в действительности относится к типу System.
Int32, переменная myBool — к типу System.Boolean, а переменная myString — к типу
System. String. Чтобы удостовериться в этом, можно вывести имя типа каждой из этих
переменных посредством рефлексии. Как будет показано в главе 15, под рефлексией по­
нимается процесс определения состава типа во время выполнения. Например, с помо­
щью рефлексии можно определить тип данных неявно типизированной локальной пере­
менной. Для этого модифицируем наш метод, добавив в его код следующие операторы:
static void DeclarelmplicitVars ()
{
// Неявно типизированные локальные переменные.
var mylnt = 0;
var myBool = true;
var myString = "Time, marches o n ...";
// Вывод имен типов, лежащих в основе этих переменных.
Console .WnteLine ("mylnt is а: {0}", mylnt.GetType().Name);
Console.WriteLine("myBool is a: {0}", myBool.GetType().Name);
Console .WnteLine ("myString is a: {0}", myS tring.GetType () .Name) ;
}
На заметку! Следует иметь в виду, что такую неявную типизацию можно использовать для любых
типов, включая массивы, обобщенные типы (см. главу 10) и пользовательские специальные
типы. Далее в книге будут встречаться и другие примеры применения неявной типизации.
142
Часть II. Главные конструкции программирования на C#
Если теперь вызвать метод D e c la r e lm p lic it V a r s () в Main ( ) , то получится вывод,
показанный на рис. 3.17.
Рис. 3.17. Применение рефлексии в отношении
неявно типизированных локальных переменных
Ограничения, связанные с неявно типизированными переменными
Разумеется, с использованием ключевого слова v a r связаны различные ограничения.
Самое первое и важное из них состоит в том, что неявная типизация применима толь­
ко для локальных переменных в контексте какого-то метода или свойства. Применять
ключевое слово v a r для определения возвращаемых значений, параметров или данных
полей специального типа не допускается. Например, ниже показано определение клас­
са, которое приведет к выдаче сообщений об ошибках на этапе компиляции:
class ThisWillNeverCompile
{
// Ошибка! var не может применяться
// для определения полей!
private var mylnt = 10;
// Ошибка! var не может применяться
// для определения возвращаемого значения
// или типа параметра!
public var MyMethod (var x, var y) {}
}
Кроме того, локальным переменным, объявленным с помощью ключевого слова var,
обязательно должно быть присвоено начальное значение в самом объявлении, причем
присваивать в качестве начального значения null не допускается! Последние ограни­
чение вполне понятно, поскольку на основании одного лишь значения null компилятор
не сможет определить, на какой тип в памяти указывает переменная.
// Ошибка! Должно быть присвоено значение!
var myData;
// Ошибка! Значение должно присваиваться в самом объявлении1
var mylnt;
mylnt = 0;
// Ошибка! Присваивание n u ll в качестве
// начального значения не допускается!
var myObj = null;
Присваивание значения null локальной переменной с выведенным после началь­
ного присваивания типом вполне допустимо (при условии, переменная отнесена к ссы­
лочному типу):
// Все в порядке, поскольку SportsCar
// является переменной ссылочного типа!
var myCar = new SportsCar ();
myCar = null;
Глава 3. Главные конструкции программирования на С#: часть I
143
Более того, значение неявно типизированной локальной переменной может быть
присвоено другим переменным, причем как неявно, так и явно типизированным:
// Здесь тоже все в порядке!
var mylnt = 0;
var anotherlnt = mylnt;
string myStnng = "Wake up!";
var myData = myString;
'
Кроме того, неявно типизированную локальную переменную можно возвращать вы­
зывающему методу, при условии, что возвращаемый тип этого метода совпадает с тем,
что лежит в основе определенных с помощью var данных:
static int GetAnlntO
{
var retVal = 9;
return retVal;
}
И, наконец, последний, но от того не менее,важный момент: определять неявно ти­
пизированную локальную переменную как допускающую значение null с использова­
нием лексемы ? в C# нельзя (типы данных, допускающие значение null, рассматрива­
ются в главе 4).
// Определять неюно типизированные переменные как допускающие значение n u ll
// нельзя, поскольку таким переменным изначально не разрешено присваивать n u ll!
var? nope = new SportsCar();
var? stillNo = 12;
var? noWay = null;
Неявно типизированные данные являются строго типизированными
Следует иметь в виду, что неявная типизация локальных переменных приводит к
получению строго типизированных данных. Таким образом, применение ключевого
слова var в C# отличается от техники, используемой в языках сценариев (таких как
JavaScript или Perl), а также от применения типа данных Variant в СОМ, где перемен­
ная на протяжении своего существования в программе может хранить значения разных
типов (это часто называется динамической типизацией).
На заметку! В .NET 4.0 появилась возможность динамической типизации в C# с использованием
нового ключевого слова dynamic. Более подробно об этом аспекте языка будет рассказы­
ваться в главе 18.
Выведение типа позволяет языку C# оставаться строго типизированным и оказы­
вает влияние только на объявление переменных во время компиляции. После этого
данные трактуются как объявленные с выведенным типом; присваивание такой пе­
ременной значения другого типа будет приводить к возникновению ошибок на этапе
компиляции.
static void ImplicitTypinglsStrongTyping()
{
// Компилятор знает, что s имеет тип System.String.
var s = "This variable can only hold string data!" ;
s = "This is fine...";
// Можно вызывать любой член л е ж а щ е г о в основе типа.
string upper = s.ToUpperO;
// Ошибка! Присваивание числовых данных строке невозможно!
s = 44;
144
Часть II. Главные конструкции программирования на C#
Польза от неявно типизированных локальных переменных
Теперь, когда был показан синтаксис, используемый для объявления неявно типи­
зируемых локальных переменных, наверняка возник вопрос, в каких ситуациях его
полезно применять? Самое важное, что необходимо знать — использование v a r для
объявления локальных переменных просто так особой пользы не приносит, а в действи­
тельности может даже вызвать путаницу у тех, кто будет изучать данный код, посколь­
ку лишает возможности быстро определить тип данных и, следовательно, понять, для
чего предназначена переменная. Поэтому если точно известно, что переменная должна
относиться к типу int, лучше сразу объявить ее с указанием этого типа.
В наборе технологий LINQ, как будет показано в начале главы 13, применяются так
называемые выражения запросов (query expression), которые позволяют получать дина­
мически создаваемые результирующие наборы на основе формата запроса. В таких вы­
ражениях неявная типизация чрезвычайно полезна, так как в некоторых случаях явное
указание типа попросту не возможно. Ниже приведен соответствующий пример кода
LINQ, в котором предлагается определить базовый тип данных subset:
static void QueryOverInts ()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
// Запрос LINQ
var subset = from i in numbers where i < 10 select
i;
Console.Write("Values in subset: " );
//Значения в subset
foreach (var i in subset)
{
Console.Write ("{0 } ", l);
}
Console.WnteLine () ;
// К какому типу относится subset?
Console .WnteLine ("subset is a: {0}", subset.GetType().Name);
Console.WriteLine ("subset is defined in: {0}", subset.GetType().Namespace);
}
Если интересно, проверьте свои предположения по поводу типа данных subset, вы­
полнив предыдущий код (subset целочисленным массивом не является). В любом слу­
чае должно быть понятно, что неявная типизация занимает свое место в рамках набора
технологий LINQ. На самом деле, можно даже утверждать, что единственным случаем,
когда применение ключевого слова v a r вполне оправдано, является определение дан­
ных, возвращаемых из запроса LINQ. Нужно всегда помнить о том, что если в точно­
сти известно, что переменная должна представлять собой переменную типа int, лучше
всегда сразу же объявлять ее с указанием этого типа. Излишняя неявная типизация
(с помощью va r) считается плохим стилем в производственном коде.
Исходный код. Проект Im p lic it ly T y p e d L o c a lV a r s доступен в подкаталоге Chapter 3.
Итерационные конструкции в C#
Все языки программирования предлагают способы для повторения блоков кода
до тех пор, пока не будет соблюдено какое-то условие завершения. Какой бы язык не
использовался ранее, операторы, предлагаемые для осуществления итераций в С#, не
должны вызывать особых недоумений и требовать особых объяснений. В частности, че­
тырьмя поддерживаемыми в C# для выполнения итераций конструкциями являются:
Глава 3. Главные конструкции программирования на С#: часть I
145
• цикл for;
• цикл foreach/in;
• цикл while;
• цикл do/while.
Давайте рассмотрим каждую из этих конструкций по очереди, предварительно соз­
дав новый проект типа C o n so le A p p lic a tio n по имени IterationsAndDecisions.
Цикл f o r
Если требуется проходить по блоку кода фиксированное количество раз, приличную
гибкость демонстрирует оператор for. Он позволяет указать, сколько раз должен повто­
ряться блок кода, а также задать конечное условие, при котором его выполнение долж­
но быть завершено. Ниже приведен пример применения этого оператора:
// База для цикла.
static void ForAndForEachLoop ()
{
// Переменная i доступна только в контексте этого цикла fo r.
for(int i = 0; 1 < 4; i++ )
{
Console.WriteLine("Number is: {0} ", 1 ) ;
}
// Здесь переменная i уже не доступна.
}
Все приемы, освоенные в языках С, C++ и Java, применимы также при создании опе­
раторов for в С#. Как и в других языках, в C# можно создавать сложные конечные усло­
вия, определять бесконечные циклы и использовать ключевые слова goto, continue и
break. Предполагается, что читатель уже знаком с данной итерационной конструкцией.
Дополнительные сведения об использовании ключевого слова for в C# можно найти в
документации .NET Framework 4.0 SDK.
Цикл f o r e a c h
Ключевое слово foreach в C# позволяет проходить в цикле по всем элементам мас­
сива (или коллекции, как будет показано в главе 10) без проверки его верхнего предела.
Ниже приведено два примера использования цикла foreach, в одном из которых произ­
водится проход по массиву строк, а в другом — проход по массиву целых чисел.
// Проход по элементам массива с помощью foreach.
static void ForAndForEachLoop ()
string[] carTypes = {"Ford", "BMW", "Yugo", "Honda" };
foreach (string c in carTypes)
Console.WriteLine(c);
int[] mylnts = { 10, 20, 30, 40 };
foreach (int i in mylnts)
Console.WriteLine(l);
}
Помимо прохода по простым массивам, fo r e a c h также позволяет осуществлять
итерации по системным и определяемым пользователем коллекциям. Рассмотрение со­
ответствующих деталей откладывается до главы 9, поскольку этот способ применения
fo rea ch требует понимания особенностей программирования с использованием интер­
фейсов, таких как IEnumerator и IEnumerable.
146
Часть II. Главные конструкции программирования на C#
Использование v a r в конструкциях f o r e a c h
В итерационных конструкциях fо reach также можно применять неявную типиза­
цию. Нетрудно догадаться, что компилятор в таких случаях будет корректно выводить
соответствующий “тип типа”. Рассмотрим приведенный ниже метод, в котором произ­
водится проход по локальному массиву целых чисел:
static void Var IriForeachLoop ()
{
intL] mylnts = { 10, 20, 30, 40 };
// Использование var в стандартном цикле foreach.
foreach (var it pm in mylnts)
{
Console.WiiteLine("Item value: (0}", item); // вывод значения элемента
Следует отметить, что в данном примере веской причины для использования ключе­
вого слова var в цикле foreach нет, поскольку четко видно, что итерация осуществляет­
ся по массиву целых чисел. Но, опять-таки, в модели программирования LINQ использо­
вание var в цикле foreach будет очень полезно, а иногда и вообще обязательно.
Конструкции w h i l e и d o / w h i l e
Конструкцию while удобно применять, когда требуется, чтобы блок операто­
ров выполнялся до тех пор, пока не будет удовлетворено какое-то конечное условие.
Естественно, нужно позаботиться о том, чтобы это условие когда-нибудь действитель­
но достигалось, иначе получится бесконечный цикл. Ниже приведен пример, в котором
на экран будет выводиться сообщение In while loop (В цикле while) до тех пор, пока
пользователь не завершит цикл вводом в командной строке слова yes:
static void ExecuteWhileLoop()
{
string userlsDone = "" ;
// Проверка на соответствие строке в нижнем регистре.
w h ile (userlsDone.ToLower () '= "yes")
{
,
Console.Write("Are you done? [yes] [no]: "); // запрос окончания
userlsDone = Console.ReadLine ();
Console.WriteLine ("In while loop");
}
}
Оператор do/while тесно связан с циклом while. Как и обычный while, цикл do/while
применяется тогда, когда какое-то действие должно выполняться неопределенное количест­
во раз. Разница между этими двумя циклами состоит в том, что цикл do/while гарантирует
выполнение соответствующего блока кода хотя бы один раз (в то время как цикл while
может его вообще не выполнить, если условие с самого начала оказывается ложным).
static void ExecuteDoWhileLoop ()
{
string userlsDone = " ";
do
Console.WriteLine ("In do/while loop");
Console.Write("Are you done? [yes] [no]: ");
userlsDone = Console.ReadLine ();
} w h ile (userlsDone.ToLower() != "yes"); // Обратите внимание на точку с запятой!
Глава 3. Главные конструкции программирования на С#: часть I
147
Конструкции принятия решений
и операции сравнения
Теперь, когда было показано, как обеспечить выполнение блока операторов в цикле,
давайте рассмотрим следующую связанную концепцию, а именно — управление выпол­
нением программы. Для изменения хода выполнения программы в C# предлагаются две
следующих конструкции:
• оператор i f / e l s e ;
• оператор sw itch .
Оператор i f / e l s e
Сначала рассмотрим хорошо знакомый оператор i f / e l s e . В отличие от языков С
и C++, в C# этот оператор может работать только с булевскими выражениями, но не с
произвольными значениями вроде -1 и 0. С учетом этого, для получения литеральных
булевских значений в операторах i f / e l s e обычно применяются операции, перечислен­
ные в табл. 3.7.
Таблица 3.7. Операции сравнения в C#
Операция
сравнения*У
Пример использования
Описание
==
i f (age == 30)
Возвращает tru e, только если выражения одинаковы
1=
i f ( "Foo"
!= myStr)
Возвращает tru e, только если выражения разные
<
>
<=
>=
if(b o n u s
if(b o n u s
if(b o n u s
if(b o n u s
< 2000)
> 2000)
<= 2000)
>= 2000)
Возвращает tru e, только если выражение слева
(bonus) меньше, больше, меньше или равно либо
больше или равно выражению справа (2000)
Программисты, ранее работавшие с С и C++, должны иметь в виду, что старые прие­
мы проверки неравенства значения нулю в C# работать не будут. Например, предполо­
жим, что требуется проверить, состоит ли текущая строка из более чем нуля символов.
У программистов на С и C++ может возникнуть соблазн написать код следующего вида:
static yoid ExecutelfElse ()
{
// Такой код недопустим, поскольку Length возвращает in t , а не bool.
string stnngData = "Му textual data";
if (stnngData.Length)
{
Console .WnteLine ("string is greater than 0 characters") ;
}
}
Если необходимо использовать свойство S t r i n g . Length для определения истинно­
сти или ложности, вычисляемое в условии выражение потребуется изменить так, чтобы
оно давало в результате булевское значение:
// Такой код является допустимым, поскольку
// условие будет возвращать true или fa ls e .
if(stringData.Length > 0)
{
Console.WriteLine("string is greater than 0 characters");
}
148
Часть II. Главные конструкции программирования на C#
В операторе i f могут применяться сложные выражения, и он может содержать
операторы else, обеспечивая выполнение более сложных проверок. Синтаксис похож
на применяемый в аналогичных ситуациях в языках С (C++) и Java. При построении
сложных выражений в C# используется вполне ожидаемый набор условных операций,
описанный в табл. 3.8.
Таблица 3.8. Условные операции в C#
Операция
Пример
Описание
&&
if (аде == 30 && name == "Fred")
Условная операция AND (И).
Возвращает true, если все выраже­
ния истинны
11
if (age ==30 | | name == "Fred")
Условная операция OR (ИЛИ).
Возвращает true, если истинно хотя
бы одно из выражений
1
i f (ImyBool)
Условная операция NOT (НЕ).
Возвращает true, если выражение
ложно, или false, если истинно
На заметку! Операции && и | | поддерживают сокращенный путь выполнения, если это необходи­
мо. То есть сразу же после определения, что некоторое сложное выражение является ложным,
оставшиеся подвыражения вычисляться не будут.
Оператор s w i t c h
Еще одной простой конструкцией, предназначенной в C# для реализации выбора,
является оператор switch. Как и в остальных С-подобных языках, в C# этот оператор
позволяет организовать выполнение программы на основе заранее определенного на­
бора вариантов. Например, в приведенном ниже методе Main () для каждого из двух
возможных вариантов выводится свое сообщение (блок default обрабатывает невер­
ный выбор).
// Переход на основе выбранного числового значения.
static void ExecuteSwitch ()
{
Console.WriteLine ("1 [C#], 2 [VB]");
Console.Write("Please pick your language preference: ") ;
string langChoice = Console.ReadLine ();
*
int n = int.Parse (langChoice);
switch (n)
{
case 1: Console.WriteLine("Good choice, C# is a fine language.");
break;
case 2: Console.WriteLine("VB: OOP, multithreading, and more1");
break;
default: Console.WriteLine("Well... good luck with that!");
break;
}
}
На заметку! В языке C# каждый блок case, в котором содержатся выполняемые операторы (блок
default в том числе), должен завершаться оператором break или goto, во избежание
сквозного прохода.
Глава 3. Главные конструкции программирования на С#: часть I
149
Одна из замечательных особенностей оператора switchBC# заключается в том, что
помимо числовых данных он также позволяет производить вычисления и со строковыми
данными. Ниже для примера приведена модифицированная версия предыдущего опе­
ратора switch (обратите внимание, что при этом подвергать пользовательские данные
синтаксическому разбору и преобразованию их в числовые значения не требуется).
static void ExecuteSwitchOnStnng ()
{
Console .WnteLine ("C# or VB");
Console.Write("Please pick your language preference: ");
string langChoice = Console.ReadLine ();
switch (langChoice)
case "C#" : Console .WnteLine ("Good choice, C# is a fine language.");
break;
case "VB": Console.WnteLine ("VB : OOP, multithreading and more' ") ;
break;
default: Console .WnteLine ("Well... good luck with that!" );\
break;
Исходный код. Проект IterationsAndDecisions доступен в подкаталоге Chapter 3.
На этом рассмотрение поддерживаемых в C# ключевых слов для организации цик­
лов и принятия решений, а также общих операций, которые можно использовать при
написании сложных операторов, завершено. Здесь предполагалось, что у читателя име­
ется опыт работы с аналогичными ключевыми словами (if, for, switch и т.д.) в других
языках программирования. Дополнительные сведения по данной теме можно найти в
документации .NET Framework 4.0 SDK.
Резюме
Задачей настоящей главы было описание многочисленных ключевых аспектов языка
программирования С#. Сначала рассматривались типичные конструкции, используе­
мые в любом приложении. Затем была описана роль объекта приложения и рассказано
о том, что в каждой исполняемой программе на C# должен обязательно присутствовать
тип, определяющий метод Main () , который служит входной точкой в приложении.
Внутри метода Main () обычно создается набор объектов, которые, работая вместе, при­
водят приложение в действие.
Далее были рассмотрены базовые типы данных в C# и разъяснено, что используе­
мые для их представления ключевые слова (вроде int) на самом деле являются сокра­
щенными обозначениями полноценных типов из пространства имен System (в данном
случае System. Int32). Благодаря этому, каждый тип данных в C# имеет набор встроен­
ных членов. Также была описана роль операций расширения и сужения типов и таких
ключевых слов, как checked и unchecked.
Кроме того, рассматривались особенности неявной типизации с помощью ключевого
слова var. Как было отмечено, неявная типизация наиболее полезна в модели програм­
мирования LINQ. И, наконец, в главе кратко рассматривались различные конструкции
С#, предназначенные для создания циклов и принятия решений. Теперь, когда базовые
характеристики языка C# известны, можно переходить к изучению его ключевой функ­
циональности, а также объектно-ориентированных возможностей.
ГЛАВА
4
Главные конструкции
программирования
на С#: часть II
этой главе будет завершен обзор ключевых аспектов языка программирования
С#. Сначала рассматриваются различные детали, касающиеся построения мето­
дов в С#, в частности, ключевые слова out, r e f и params. Кроме того, будут описаны
две новых функциональных возможности С#, которые появились в .NET 4.0 — необяза­
тельные и именованные параметры.
Затем будет рассматриваться перегрузка методов, манипулирование массивами с
использованием синтаксиса C# и функциональность связанного с массивами класса
В
System.Array.
Вдобавок в главе показано, как создавать перечисления и структуры в С#, и подроб­
но описаны отличия между типами значений и ссылочными типами. И, наконец, рас­
сматривается роль нулевых (nullable) типов данных и операций ? и ??. После изучения
материалов настоящей главы можно переходить к ознакомлению с объектно-ориенти­
рованными возможностями языка С#.
Методы и модификаторы параметров
Для начала давайте изучим детали, касающиеся определения методов в С#. Как и ме­
тод Main () (см. главу 3), специальные методы могут как принимать, так и не принимать
параметров, а также возвращать или не возвращать значения вызывающей стороне.
Как будет показано в следующих нескольких главах, методы могут быть реализованы в
контексте классов или структур (а также прототипированы внутри типов интерфейсов)
и снабжаться различными ключевыми словами (in te r n a l, v i r t u a l , p u b lic , new и т.д.)
для уточнения их поведения. До настоящего момента в этой книге формат каждого из
демонстрировавшихся методов в целом выглядел так:
// Статические методы могут вызываться
// напрямую без создания экземпляра класса,
class Program
• {
// static возвращаемыйТип ИмяМетода (параметры) {...}
static int Add(±nt х, int у) { return х + у; }
}
Глава 4. Главные конструкции программирования на С#: часть II
151
Х отя определение метода в C# выглядит довольно понятно, существует несколько
ключевых слов, с помощью которых можно управлять способом передачи аргументов
интересующему методу. Все эти ключевые слова описаны в табл. 4.1.
Таблица 4.1. Модификаторы параметров в C#
Модификатор параметра
Описание
(отсутствует)
Если параметр не сопровождается модификатором, предполага­
ется, что он должен передаваться по значению, т.е. вызываемый
метод должен получать копию исходных данных
out
Выходные параметры должны присваиваться вызываемым мето­
дом (и, следовательно, передаваться по ссылке). Если парамет­
рам out в вызываемом методе значения не присвоены, компиля­
тор сообщит об ошибке
ref
Это значение первоначально присваивается вызывающим кодом
и при желании может повторно присваиваться в вызываемом
методе (поскольку данные также передаются по ссылке). Если
параметрам ref в вызываемом методе значения не присвоены,
компилятор никакой ошибки генерировать не будет
params
Этот модификатор позволяет передавать в виде одного логическо­
го параметра переменное количество аргументов. В каждом мето­
де может присутствовать только один модификатор params и он
должен обязательно указываться последним в списке параметров.
В реальности необходимость в использовании модификатора
params возникает не особо часто, однако он применяется во мно­
гих методах внутри библиотек базовых классов
Чтобы посмотреть, как эти ключевые слова используются на практике, давайте соз­
дадим новый проект типа Console Application по имени FunWithMethods и с его по­
мощью изучим роль каждого из этих ключевых слов.
Стандартное поведение при передаче параметров
По умолчанию параметр передается по значению. Попросту говоря, если аргумент
не снабжается каким-то конкретным модификатором параметра, функции передается
копия данных. Как будет объясняться в конце настоящей главы, то, как именно выгля­
дит эта копия, зависит от того, к какому типу относится параметр — типу значения или
ссылочному типу. Пока что давайте создадим внутри класса Program следующий метод,
который оперирует двумя числовыми типами данных, передаваемыми по значению:
//По умолчанию аргументы передаются по значению.
public static int Add(int x, int y)
{
int ans = x + y;
// Вызывающий метод не увидит эти изменения, поскольку
// изменяться в таком случае будет лишь копия исходных данных.
X = 10000;
у = 88888;
return ans;
}
Числовые данные подпадают под категорию типов значения. Поэтому в случае измене­
ния значений параметров внутри члена вызывающий метод будет оставаться в полном не­
ведении об этом, поскольку значения будут изменяться л и т ь в копии исходных данных.
152
Часть II. Главные конструкции программирования на C#
static void Main(string [] args)
{
Console .WnteLine (" *****Fun with Methods *****\n");
// Передача двух переменных no значению.
int x = 9, у = 10;
Console .WnteLine ("Before call: X: {0}, Y: {1}", x, y) ;
Console .WnteLine ("Answer is: {0}", Add(x, y) ) ;
Console.WriteLine("After call: X: {0}, Y: {1}", x, y) ;
Console.ReadLine();
// до вызова
// ответ
// после вызова
}
Как и следовало ожидать, значения х и у до и после вызова Add ( ) будут выглядеть
совершенно идентично:
***** Fun wlth Methods *****
Before call: X: 9 , Y: 10
Answer is: 19
After call: X: 9 , Y: 10
Модификатор o u t
Теперь посмотрим, как используются выходные параметры. Методы, которым при
определении (с помощью ключевого слова out) указано принимать выходные парамет­
ры, должны перед выходом обязательно присваивать им соответствующие значения
(в противном случае компилятор сообщит об ошибке).
В целях иллюстрации ниже приведена альтернативная версия метода Add () , кото­
рая предусматривает возврат суммы двух целых чисел с использованием модификато­
ра out (обратите внимание, что физическим возвращаемым значением метода теперь
является vo id ).
// Выходные параметры должны предоставляться вызываемым методом.
public static void Add (int x, int y, out int ans)
{
ans = x + y;
}
В вызове метода с выходными параметрами тоже должен использоваться модифи­
катор out. Локальным переменным, передаваемым в качестве выходных параметров,
присваивать начальные значения не требуется (после вызова эти значения все равно
будут утрачены). Причина, по которой компилятор позволяет передавать на первый
взгляд неинициализированные данные, связана с тем, что в вызываемом методе опера­
ция присваивания должна выполняться обязательно. Ниже приведен пример.
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Methods *****");
// Присваивать первоначальное значение локальным
// переменным, используемым в качестве выходных
// параметров, не требуется, при условии, что в первый раз
// они используются в качестве выходных аргументов.
int ans;
A d d (90, 90, out ans);
Console.WriteLine("90 + 90 = {0}", ans);
Console.ReadLine();
}
Этот пример был представлен исключительно для иллюстрации; на самом деле нет
совершенно никаких оснований возвращать значение операции суммирования в выход­
Глава 4. Главные конструкции программирования на С#: часть II
153
ном параметре. Но сам модификатор out в C# действительно является очень полезным:
он позволяет вызывающему коду получать в результате одного вызова метода сразу не­
сколько значений.
// Возврат множества выходных параметров.
public static void FillTheseVals (out int a, out string b, out bool c)
I
a = 9;
b = "Enjoy your string.";
c = true;
}
В вызывающем коде в таком случае может находиться обращ ение к методу
FillTheseValues (), как показано ниже. Обратите внимание, что модификатор out
должен использоваться как при вызове, так и при реализации данного метода.
static void Main(string [] args)
{
Console.WnteLine (''***** Fun with Methods *****");
int i; string str; bool b;
FillTheseValues (out i , out str, out b);
Console.WnteLine ("Int is: {0}", 1 );
Console.WriteLine ("String is: {0}", str);
Console.WriteLine("Boolean is: {0}", b);
// целое число
// строка
// булевское значение
Console.ReadLine();
}
И, наконец, не забывайте, что в любом методе, в котором определяются выходные
параметры, перед выходом им обязательно должны быть присвоены действительные
значения. Следовательно, для следующего кода компилятор будет выдавать ошибку, по­
скольку выходному параметру в области действия метода никакого значения присвоено
не было:
static void ThisWontCompile (out int a)
{
Console. WriteLine("Error 1 Forgot to assign output arg!");
// Ошибка! Забыли присвоить значение выходному аргументу!
Модификатор r e f
Теперь посмотрим, как в C# используется модификатор r e f (от “reference” - ссылка).
Параметры, сопровождаемые таким модификатором, называются ссылочными и приме­
няются, когда нужно позволить методу выполнять операции и обычно также изменять
значения различных элементов данных, объявляемых в вызывающем коде (например,
в процедуре сортировки или обмена). Обратите внимание на следующие отличия между
ссылочными и выходными параметрами.
• Выходные параметры не нужно инициализировать перед передачей методу.
Причина в том, что метод сам должен присваивать значения выходным парамет­
рам перед выходом.
• Ссылочные параметры нужно обязательно инициализировать перед передачей
методу. Причина в том, что они подразумевают передачу ссылки на уже суще­
ствующую переменную. Если первоначальное значение ей не присвоено, это бу­
дет равнозначно выполнению операции над неинициализированной локальной
переменной.
154
Часть II. Главные конструкции программирования на C#
Давайте рассмотрим применение ключевого слова r e f на примере метода, меняю­
щего две строки местами:
// Ссылочные параметры.
public static void SwapStnngs (ref string si, ref string s2)
{
string tempStr = si;
si = s2;
s2 = tempStr;
}
Этот метод может быть вызван следующим образом:
static void Main(string [] args)
{
Console.WriteLine (''***** Fun with Methods *****");
string si = "Flip";
string s2 = "Flop";
Console.WriteLine ("Before: {0}, {1} ", si, s2); // до
SwapStrings (r e f s i, r e f s2 );
Console.WriteLine("After: {0}, [1} ", si, s2); // после
Console.ReadLine ();
}
Здесь в вызывающем коде производится присваивание первоначальных значений
локальным строкам (s i и s2). Благодаря этому после выполнения вызова SwapStrings ()
в строке s i будет содержаться значение "F lo p ", а в строке s2 — значение " F l i p " :
Before: Flip, Flop
After: Flop, Flip
На заметку! Поддерживаемое в C# ключевое слово ref рассматривается в разделе "Типы значе­
ния и ссылочные типы” далее в главе. Как будет показано, поведение этого ключевого слова
немного меняется в зависимости от того, является аргумент типом значения (структурой) или
ссылочный типом (классом).
Модификатор p a ra m s
В C# поддерживается использование массивов параметров за счет применения клю­
чевого слова params. Для овладения этой функциональностью требуются хорошие зна­
ния массивов С#, основные сведения о которых приведены в разделе “Массивы в С#”
далее в главе.
Ключевое слово params позволяет передавать методу переменное количество аргу­
ментов одного типа в виде единственного логического параметра. Аргументы, поме­
ченные ключевым словом params, могут обрабатываться, если вызывающий код на их
месте передает строго типизированный массив или разделенный запятыми список эле­
ментов. Конечно, это может вызывать путаницу. Чтобы стало понятнее, предположим,
что требуется создать функцию, которая бы позволила вызывающему коду передавать
любое количество аргументов и возвращала бы их среднее значение.
Если прототипировать соответствующий метод так, чтобы он принимал массив зна­
чений типа double, вызывающий код должен будет сначала определить массив, затем
заполнить его значениями и только потом, наконец, передать. Однако если определить
метод CalculateAverage () так, чтобы он принимал массив параметров (params) типа
double, тогда вызывающий код может просто передать разделенный запятыми список
значений double, а исполняющая среда .NET автоматически упакует этот список в мас­
сив типа double.
Глава 4. Главные конструкции программирования на С#: часть II
155
// Возвращение среднего из некоторого количества значений double.
static double CalculateAverage(params double[] values)
{
// Вывод количества значений
Console.WriteLine("You sent me {0} doubles.", values.Length);
double sum = 0;
if(values.Length == 0)
return sum;
for (int i = 0; l < values.Length; i++)
sum += values [l] ;
return (sum / values.Length);
}
Метод определен таким образом, чтобы принимать массив параметров со значения­
ми типа double. По сути, этот метод ожидает произвольное количество (включая ноль)
значений double и вычисляет по ним среднее значение. Благодаря этому, он может
вызываться любым из показанных ниже способов:
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Methods *****");
// Передача разделенного запятыми списка значений d o u b le ...
double average;
average = CalculateAverage(4.0, 3.2, 5.7, 64.22, 87.2);
Console.WriteLine("Average of data is: {0}", average);
// Среднее значение получается таким:
// . . .или массива значений double.
double [] data = { 4.0, 3.2, 5.7 };
average = CalculateAverage(data);
Console.WriteLine("Average of data is: 10}", average);
// Среднее из 0 равно О!
Console.WriteLine("Average of data is: (0}", CalculateAverage());
Console.ReadLine();
}
Если бы в определении CalculateAverage () не было модификатора params, первый спо­
соб вызова этого метода приводил бы к ошибке на этапе компиляции, поскольку тогда компи­
лятор искал бы версию CalculateAverage (), принимающую пять аргументов double.
На заметку! Во избежание какой бы то ни было неоднозначности, в C# требуется, чтобы в любом
методе поддерживался только один аргумент params, который должен быть последним в спи­
ске параметров.
Как не трудно догадаться, такой подход является просто более удобным для вызы­
вающего кода, поскольку в случае его применения необходимый массив создается са­
мой CLR-средой. К моменту, когда этот массив попадает в область действия вызывае­
мого метода, он будет трактоваться как полноценный массив .NET, обладающий всеми
функциональными возможностями базового типа System.Array. Ниже показано, как
мог бы выглядеть вывод приведенного выше метода:
You sent me 5 doubles.
Average of data is: 32.864
You sent me 3 doubles.
Average of data is: 4.3
You sent me 0 doubles.
Average of data is: 0
156
Часть II. Главные конструкции программирования на C#
Определение необязательных параметров
С выходом версии .NET 4.0 у разработчиков приложений на C# теперь появилась
возможность создавать методы, способные принимать так называемые необязательные
аргументы (optional arguments). Это позволяет вызывать единственный метод, опуская
необязательные аргументы, при условии, что подходят их значения, установленные по
умолчанию.
На заметку! Как будет показано в главе 18, главным стимулом для добавления необязательных
аргументов послужила необходимость в упрощении взаимодействия с объектами СОМ. В не­
скольких объектных моделях Microsoft (например, Microsoft Office) функциональность предос­
тавляется через объекты СОМ, многие из которых были написаны давно и рассчитаны на ис­
пользование необязательных параметров.
Чтобы посмотреть, как работать с необязательными аргументами, давайте создадим
метод по имени EnterLogData () с одним необязательным параметром:
static void EnterLogData(string message, string owner = "Programmer")
{
Console.Beep ();
Console .WnteLine ("Error : {0}", message);
Console .WnteLine ("Owner of Error: {0}", owner) ;
}
Последнему аргументу string был присвоено используемое по умолчанию значение
" Programmer" с применением операции присваивания внутри определения парамет­
ров. В результате метод EnterLogData () можно вызывать в Main () двумя способами:
static void Main(string[] args)
{
Console.WnteLine (''***** Fun with Methods *****");
EnterLogData ("Oh no! Grid can't find data");
EnterLogData ("Oh no! I can't find the payroll data", "CFO");
Console.ReadLine ();
}
Поскольку в первом вызове EnterLogData () не было указано значение для второго
аргумента string, будет использоваться его значение по умолчанию — "Programmer".
Еще один важный момент, о котором следует помнить, состоит в том, что значение,
присваиваемое необязательному параметру, должно быть известно во время компиля­
ции и не может вычисляться во время выполнения (в этом случае на этапе компиляции
сообщается об ошибке). Для иллюстрации предположим, что понадобилось модифици­
ровать метод EnterLogData ( ) , добавив в него еще один необязательный параметр:
// Ошибка1 Значение, используемое по умолчанию для необязательного
// аргумента, должно быть известно во время компиляции!
static void EnterLogData(string message,
string owner = "Programmer", DateTime timestamp = DateTime.Now)
{
Console.Beep ();
Console.WriteLine ("Error: {0}", message);
Console .WnteLine ("Owner of Error: {0}", owner) ;
Console.WriteLine("Time of Error: {0}", timestamp);
}
Этот код скомпилировать не получится, потому что значение свойства Now класса
DateTime вычисляется во время выполнения, а не во время компиляции.
Глава 4. Главные конструкции программирования на С#: часть II
157
На заметку! Во избежание возникновения любой неоднозначности, необязательные параметры
должны всегда размещаться в конце сигнатуры метода. Если необязательный параметр ока­
жется перед обязательными, компилятор сообщит об ошибке.
Вызов методов с использованием именованных параметров
Еще одной функциональной возможностью, которая добавилась в C# с выходом вер­
сии .NET 4.0, является поддержка так называемых именованных аргументов (named
arguments). По правде говоря, на первый взгляд может показаться, что такая языковая
конструкция способна лишь запутать код. Это действительно может оказаться имен­
но так! Во многом подобно необязательным аргументам, стимулом для включения под­
держки именованных параметров главным образом послужило желание упростить ра­
боту с уровнем взаимодействия с СОМ (см. главу 18).
Именованные аргументы позволяют вызывать метод с указанием значений пара­
метров в любом желаемом порядке. Следовательно, вместо того, чтобы передавать па­
раметры исключительно в соответствии с позициями, в которых они определены (как
приходится поступать в большинстве случаев), можно указывать имя каждого аргумен­
та, двоеточие и конкретное значение. Чтобы продемонстрировать применение имено­
ванных аргументов, добавим в класс Program следующий метод:
static void DisplayFancyMessage(ConsoleColor textColor,
ConsoleColor backgroundColor, string message)
{
// Сохранение старых цветов для обеспечения возможности
//их восстановления сразу после вывода сообщения.
ConsoleColor oldTextColor = Console.ForegroundColor;
ConsoleColor oldbackgroundColor = Console.BackgroundColor;
// Установка новых цветов и вывод сообщения.
Console.ForegroundColor = textColor;
Console.BackgroundColor = backgroundColor;
Console.WriteLine(message);
// Восстановление предыдущих цветов.
Console.ForegroundColor = oldTextColor;
Console.BackgroundColor = oldbackgroundColor;
}
Возможно, ожидается, что при вызове методу DisplayFancyMessage () должны пе­
редаваться две переменных типа ConsoleColor со следующим за ним значением типа
string. Однако за счет применения именованных аргументов DisplayFancyMessage ()
вполне можно вызвать и так, как показано ниже:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Methods *****");
DisplayFancyMessage(message: "Wow! Very Fancy indeed!",
textColor: ConsoleColor.DarkRed,
backgroundColor: ConsoleColor.White);
DisplayFancyMessage(backgroundColor: ConsoleColor.Green,
message: "Testing...",
textColor: ConsoleColor.DarkBlue);
Console.ReadLine();
}
158
Часть II. Главные конструкции программирования на C#
Одной малоприятной особенностью применения именованных аргументов является
то, что в вызове метода позиционные параметры должны быть перечислены перед лю ­
быми именованными параметрами. Другими словами, именованные аргументы должны
всегда размещаться в конце вызова метода. Ниже показан пример, иллюстрирующий
сказанное.
// Здесь все в порядке, поскольку позиционные
// аргументы идут перед именованными.
DisplayFancyMessage(ConsoleColor.Blue,
message: "Testing...",
backgroundColor: ConsoleColor.White);
// Здесь присутствует ошибка, поскольку позиционные
// аргументы идут после именованных.
DisplayFancyMessage(message: "Testing...",
backgroundColor: ConsoleColor.White,
ConsoleColor.Blue);
Помимо этого ограничения может возникать вопрос, а когда вообще понадобится эта
языковая конструкция? Зачем нужно менять позиции трех аргументов метода?
Как оказывается, если в методе необходимо определять необязательные аргументы,
то эта конструкция может оказаться очень полезной. Для примера перепишем метод
DisplayFancyMessage () так, чтобы он поддерживал необязательные аргументы и пре­
дусматривал для них подходящие значения по умолчанию:
static void DisplayFancyMessage(ConsoleColor textColor = ConsoleColor.Blue,
ConsoleColor backgroundColor = ConsoleColor.White,
string message = "Test Message")
{
}
Из-за того, что каждый аргумент теперь имеет значение по умолчанию, в вызываю­
щем коде с помощью именованных аргументов можно указывать только тот параметр
или параметры, для которых не должны применяться значения по умолчанию. То есть,
если нужно, чтобы значение " H e l l o ! " появлялось в виде текста голубого цвета на бе­
лом фоне, в вызывающем коде можно использовать следующую строку:
DisplayFancyMessage(message: "Hello!")
Если же необходимо, чтобы строка "Test Message" выводилась синим цветом на
зеленом фоне, то должен применяться такой код:
DisplayFancyMessage(backgroundColor: ConsoleColor.Green);
Как не трудно заметить, необязательные аргументы и именованные параметры
действительно часто работают бок о бок. Чтобы завершить рассмотрение деталей по­
строения методов в С#, необходимо обязательно ознакомиться с понятием перегрузки
методов.
Исходный код. Приложение FunWithEnums доступно в подкаталоге Chapter 4.
Перегрузка методов
Как и в других современных объектно-ориентированных языках программирования,
в C# можно перегружать (overload) методы. Перегруженными называются методы с на­
бором одинаково именованных параметров, отличающиеся друг от друга количеством
(или типом) параметров.
Глава 4. Главные конструкции программирования на С#: часть II
159
Чтобы оценить полезность перегрузки методов, давайте представим себя на месте
разработчика, использующего Visual Basic 6.0, и предположим, что требуется создать
набор методов, возвращающих сумму значений различных типов (Integer, Double
и т.д.). Из-за того, что в VB6 перегрузка методов не поддерживается, для решения этой
задачи придется определить уникальный набор методов, каждый из которых, по сути,
будет делать одно и тоже (возвращать сумму аргументов):
' Примеры кода на VB6.
Public Function A d d ln ts(ByVal x As Integer, ByVal у As Integer) As Integer
Addlnts = x + у
End Function
Public Function AddDoubles (ByVal x As Double, ByVal у As Double) As Double
AddDoubles = x + у
End Function
Public Function AddLongs (ByVal x As Long, ByVal у As Long) As Long
AddLongs = x + у
End Function
Такой код не только труден в сопровождении, но и заставляет помнить имя каждо­
го метода. С применением перегрузки, однако, можно дать возможность вызывающему
коду вызывать только единственный метод по имени Add () . При этом важно обеспе­
чить, чтобы каждая версия метода имела отличающийся набор аргументов (различий в
одном только возвращаемом типе не достаточно).
На заметку! Как будет показано в главе 10, в C# возможно создавать обобщенные методы, которые
переводят концепцию перегрузки на новый уровень. С использованием обобщений для реали­
заций методов можно определять так называемые “заполнители", которые будут заполняться
во время вызова этих методов.
Чтобы попрактиковаться с перегруженными методами, создадим новый проект
Console Application (Консольное приложение) по имени Methodoverloading и добавим
в него следующее определение класса на С#:
// Код на С#.
class Program
{
static void Main(string[] args) { }
// Перегруженный метод Add() .
static int Add(int x, int y)
{ return x + y; }
static double Add(double x, double y)
{ return x + y; }
static long Add(long x, long y)
{ return x + y; }
}
Теперь можно вызывать просто метод Add () с требуемыми аргументами, а компиля­
тор будет самостоятельно находить правильную реализацию, подлежащую вызову, на
основе предоставляемых ему аргументов:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Method Overloading ** * **\n ");
// Вызов in t -версии Add()
Console.WriteLine(Add(10, 10));
'
// Вызов long-версии Add()
Console.WriteLine(Add(900000000000, 900000000000));
160
Часть II. Главные конструкции программирования на C#
// Вызов double-версии Add()
Console .WnteLine (Add (4.3, 4.4));
Console.ReadLine();
}
ШЕ-среда Visual Studio 2010 обеспечивает помощь при вызове перегруженных ме­
тодов. При вводе имени перегруженного метода (как, например, хорошо знакомого
Console .WnteLine ( ) ) в списке IntelliSense предлагаются все его доступные версии.
Обратите внимание, что по списку можно легко перемещаться, щелкая на кнопках со
стрелками вниз и вверх (рис. 4.1).
Рис. 4.1. Окно IntelliSense, отображаемое в Visual Studio 2010 при работе
с перегруженными методами
На заметку! Приложение MethodOverloading доступно в подкаталоге Chapter 4.
На этом обзор основных деталей создания методов с использованием синтаксиса C#
завершен. Теперь давайте посмотрим, как в C# создавать и манипулировать массивами,
перечислениями и структурами, и завершим главу изучением того, что собой представ­
ляют “нулевые типы данных” и такие поддерживаемые в C# операции, как ? и ??.
Массивы в C#
Как, скорее всего, уже известно, массивом (array) называется набор элементов дан­
ных, доступ к которым получается по числовому индексу. Если говорить более конкрет­
но, то любой массив, по сути, представляет собой ряд связанных между собой элемен­
тов данных одинакового типа (например, массив int, массив string, массив SportCar
и т.п.). Объявление массива в C# осуществляется довольно понятным образом. Чтобы
посмотреть, как именно, давайте создадим новый проект типа Console Application (по
имени FunWithArrays) и добавим в него вспомогательный метод SimpleArrays ( ) , вы­
зываемый из М а ш ().
class Program
{
static void Main(string[] args)
{
Console .WnteLine ("**** * Fun with Arrays *****");
SimpleArrays();
Console.ReadLine();
Глава 4. Главные конструкции программирования на С#: часть II
161
static void SimpleArrays ()
{
Console.WriteLine ("=> Simple Array Creation.");
// Создание массива in t с тремя элементами {0, 1, 2 }.
int[] mylnts = new int[3];
// Инициализация массива с 100 элементами s t r in g ,
// проиндексированными от 0 до 99.
string[] booksOnDotNet = new string[100];
Console.WriteLine ();
Внимательно почитайте комментарии в коде. При объявлении массива с помощью
такого синтаксиса указываемое в объявлении число обозначает общее количество эле­
ментов, а не верхнюю границу. Кроме того, нижняя граница в массиве всегда начинает­
ся с 0. Следовательно, в результате объявления i n t [ ] m ylnts = new i n t [3] получается
массив, содержащий три элемента, проиндексированных по позициям 0, 1,2.
После определения переменной массива можно переходить к его заполнению элемен­
тами от индекса к индексу, как показано ниже на примере метода S im pleA rrays () :
static void SimpleArrays ()
{
Console.WriteLine ("=> Simple Array Creation.");
// Создание и заполнение массива тремя
// целочисленными значениями.
int[] mylnts = new int[3];
mylnts[0] = 100;
mylnts[1] = 200;
mylnts[2] = 300;
// Отображение значений.
foreach (int i in mylnts)
Console.WriteLine (1 );
Console.WriteLine ();
}
На заметку! Следует иметь в виду, что если массив только объявляется, но явно не инициализиру­
ется, каждый его элемент будет установлен в значение, принятое по умолчанию для соответст­
вующего типа данных (например, элементы массива типа bool будут устанавливаться в fa ls e ,
а элементы массива типа in t — в 0).
Синтаксис инициализации массивов в C#
Помимо заполнения массива элемент за элементом, можно также заполнять его с
использованием специального синтаксиса инициализации массивов. Для этого необхо­
димо перечислить включаемые в массив элемент в фигурных скобках ({ }). Такой син­
таксис удобен при создании массива известного размера, когда нужно быстро задать
его начальные значения. Ниже показаны альтернативные версии объявления массива.
static void Arraylnitialization ()
{
Console.WriteLine ("=> Array Initialization.");
// Синтаксис инициализации массива с помощью
// ключевого слова new.
string[] stringArray = new string[]
{ "one", "two", "three" };
Console.WriteLine("stringArray has {0} elements", stringArray.Length);
162
Часть II. Главные конструкции программирования на C#
// Синтаксис инициализации массива без применения
// ключевого слова new.
bool[] boolArray = { false, false, true };
Console.WriteLine("boolArray has {0} elements", boolArray.Length);
// Синтаксис инициализации массива с указанием ключевого
// слова new и желаемого размера.
int[] intArray = new int[4] { 20, 22, 23, 0 };
Console.WriteLine("intArray has {0} elements", intArray.Length);
Console.WriteLine ();
}
Обратите внимание, что в случае применения синтаксиса с фигурными скобками
размер массива указывать не требуется (как видно на примере создания переменной
stringArray), поскольку этот размер автоматически вычисляется на основе количест­
ва элементов внутри фигурных скобок. Кроме того, применять ключевое слово new не
обязательно (как при создании массива boolArray).
В объявлении intArray указанное числовое значение обозначает количество эле­
ментов в массиве, а не верхнюю границу. Если между объявленным размером и коли­
чеством инициализаторов имеется несоответствие, на этапе компиляции будет выдано
сообщение об ошибке. Ниже приведен соответствующий пример:
// Размер не соответствует количеству элементов!
int[] intArray = new int[2] { 20, 22, 23, 0 };
Неявно типизированные локальные массивы
В предыдущей главе рассматривалась тема неявно типизированных локальных пе­
ременных. Вспомните, что ключевое слово var позволяет определить переменную так,
чтобы лежащий в ее основе тип выводился компилятором. Аналогичным образом мож­
но также определять неявно типизированные локальные м ассивы С использованием
такого подхода можно определить новую переменную массива без указания типа эле­
ментов, содержащихся в массиве.
static void DeclarelmplicitArrays ()
{
Console.WriteLine ("=> Implicit Array Initialization.");
// В действительности a - массив типа i n t [ ] .
var a = new[] { 1, 10, 100, 1000 };
Console .WriteLine ("a is a: {0}", a .ToStnng () ) ;
// В действительности b - массив типа doublet] .
var b = new [] { 1, 1.5, 2, 2.5 };
Console.WriteLine("b is a: {0}", b .ToString());
// В действительности с - массив типа s t r in g [ ] .
var с = new[] { "hello", null, "world" };
Console.WriteLine("c is a: {0}", c .ToString());
Console.WriteLine();
}
Разумеется, как и при создании массива с использованием явного синтаксиса С#,
элементы, указываемые в списке инициализации массива, должны обязательно иметь
один и тот же базовый тип (т.е. должны все быть intr string или SportsCar). В отли­
чие от возможных ожиданий, неявно типизированный локальный массив не получает
по умолчанию тип SystemObject, поэтому приведенный ниже код вызывает ошибку на
этапе компиляции:
// Ошибка! Используются смешанные типы!
var d = new[J { 1, "one", 2, "two", false };
Глава 4. Главные конструкции программирования на С#: часть II
163
Определение массива объектов
В большинстве случаев при определении массива тип элемента, содержащегося в
массиве, указывается явно. Хотя на первый взгляд это выглядит довольно понятно, су­
ществует одна важная особенность. Как будет объясняться в главе 6, в основе каждого
типа в системе типов .NET (в том числе фундаментальных типов данных) в конечном
итоге лежит базовый класс S ystem .O b ject. В результате получается, что в случае оп­
ределения массива объектов находящиеся внутри него элементы могут представлять
собой что угодно. Например, рассмотрим показанный ниже метод A rr a y O fO b je c ts ()
(который можно вызвать в Main () для целей тестирования).
static void ArrayOfObjects ()
{
Console.WriteLine ("=> Array of Objects.");
// В массиве объектов могут содержаться элементы любого типа.
object [] myObjects = new object [4];
myObjects[0] = 10 ;
myObjects[l] = false;
myObjects[2] = new DateTime(1969, 3, 24);
myObjects[3] = "Form & Void";
foreach (object obj in myObjects)
{
// Вывод имени типа и значения каждого элемента массива.
Console.WriteLine("Type: {0}, Value: {1}", obj.GetType(), obj);
}
Console.WriteLine();
}
В коде сначала осуществляется проход циклом по содержимому массива m yObjects,
а затем с использованием метода GetType () из System . Ob j e c t выводится имя базового
типа и значения всех элементов. Вдаваться в детали метода System. Ob j e c t . GetType ()
на этом этапе пока не требуется; сейчас главное уяснить только то, что с помощью этого
метода можно получить полностью уточенное имя элемента (получение информации о
типах и службы рефлексии подробно рассматриваются в главе 15). Ниже показано, как
будет выглядеть вывод после вызова A rra y O fO b je c ts () .
=> Array of Objects.
Type: System.Int32, Value: 10
Type: System.Boolean, Value: False
Type: System.DateTime, Value: 3/24/1969 12:00:00 AM
Type: System.String, Value: Form & Void
Работа с многомерными массивами
В дополнение к одномерным массивам, которые демонстрировались до сих пор, в
C# также поддерживаются две разновидности многомерных массивов. Многомерные
массивы первого вида называются прямоугольными массивами и содержат несколько
измерений, где все строки имеют одинаковую длину. Объявляются и заполняются такие
массивы следующим образом:
static void RectMultidimensionalArray()
{
Console.WriteLine("=> Rectangular multidimensional array.");
// Прямоугольный многомерный массив.
int[,] myMatrix;
myMatrix = new int[6,6];
164
Часть II. Главные конструкции программирования на C#
// Заполнение массива (6 * 6) .
for(int i = 0; i < 6; i++)
for(int j = 0; j < 6; j++)
myMatrix[i, j] = i * j;
// Вывод массива (6 * 6) .
for(int i = 0; l < 6; i++)
{
for(int j = 0; j < 6; Ц++)
Console.Write(myMatrix[i , j] + "\t");
Console .WnteLine () ;
}
Console .WnteLine () ;
}
Многомерные массивы второго вида называются зубчатыми (jagged) массивами и
содержат некоторое количество внутренних массивов, каждый из которых может иметь
собственный уникальный верхний предел, например:
static void JaggedMultidimensionalArray()
{
Console.WriteLine("=> Jagged multidimensional array.");
// Зубчатый многомерный массив (т .е . массив массивов) .
// Здесь он будет состоять из 5 других массивов.
int [] [] myJagArray = new int [5 ] [] ;
// Создание зубчатого массива.
for (int i = 0 ; i < myJagArray.Length; i++)
myJagArray [i ] = new int[i + 7];
// Вывод всех строк (не забывайте, что по умолчанию
// каждый элемент устанавливается в 0) .
for (int i = 0; i < 5; i++)
{
for(int j = 0; j < myJagArray[l].Length; j++)
Console.Write(myJagArray[l][j] + " ");
Console.WriteLine ();
}
Console.WriteLine ();
}
На рис. 4.2 показано, как будет выглядеть вывод после вызова в Main () методов
R e c tM u ltid im e n s io n a lA r r a y () и J a g g e d M u ltid im e n s io n a lA rra y ( ).
Рис. 4.2. Прямоугольные и зубчатые многомерные массивы
Глава 4. Главные конструкции программирования на С#: часть II
165
Использование массивов в качестве аргументов
и возвращаемых значений
После создания массив можно передавать как аргумент или получать в виде возвра­
щаемого значения члена. Например, ниже приведен код метода PrintArray ( ), который
принимает в качестве входного параметра массив значений int и выводит каждое из
них в окне консоли, а также метод GetString () , который заполняет массив значения­
ми string и возвращает его вызывающему коду.
static void PrintArray (int [] mylnts)
{
for(int i = 0; i < mylnts.Length; i++)
Console.WriteLine("Item {0} is {1}", i, mylnts[1 ]);
}
static s t r in g [] GetStringArray()
{
string[] theStrings = {"Hello", "from", "GetStringArray"};
return theStrings;
}
Вызываются эти методы так, как показано ниже:
static void PassAndReceiveArrays ()
{
Console.WriteLine("=>Arrays as params and return values.");
// Передача массива в качестве параметра.
int [] ages = {20, 22, 23, 0} ;
P rin tA rra y (a g e s );
// Получение массива в качестве возвращаемого значения.
string[] strs = GetStringArray();
foreach(string s in strs)
Console.WriteLine(s);
Console.WriteLine() ;
}
К этому моменту должно уже быть понятно, как определять, заполнять и изучать со­
держимое массивов в С#. В завершение картины рассмотрим роль класса System. Array.
Базовый класс S y s t e m .A r r a y
Каждый создаваемый массив получает большую часть функциональности от класса
System. Array. Общие члены этого класса позволяют работать с массивом с использо­
ванием полноценной объектной модели. В табл. 4.2 приведено краткое описание не­
которых наиболее интересных членов класса System. Array (полное описание этих и
других членов данного класса можно найти в документации .NET Framework 4.0 SDK).
Таблица 4.2. Некоторые члены класса System. A rray
Член класса
System. Array
Описание
Clear ()
Статический метод, который позволяет устанавливать для всего ряда элементов
в массиве пустые значения (0 — для чисел, null — для объектных ссылок и
false — для булевских выражений)
СоруТо ()
Метод, который позволяет копировать элементы из исходного массива в целевой
Length
Свойство, которое возвращает информацию о количестве элементов в массиве
166
Часть II. Главные конструкции программирования на C#
Окончание табл. 4.2
Член класса
System. Array
Описание
Rank
Свойство, которое возвращает информацию о количестве измерений в массиве
Reverse ()
Статическое свойство, которое представляет содержимое одномерного массива
в обратном порядке
Sort ()
Статический метод, который позволяет сортировать одномерный массив
внутренних типов. В случае реализации элементами в массиве интерфейса
IComparer также позволяет сортировать и специальные типы (см. главу 9)
Давайте теперь посмотрим, как некоторые из этих членов выглядят в действии.
Н и ж е приведен вспомогательный метод, в котором с помощью статических методов
Reverse () и Clear () в окне консоли выводится информация о массиве типов string:
static void SystemArrayFunctionality()
{
Console.WriteLine("=> Working with System.Array.");
// Инициализация элементов при запуске.
string[] gothicBands = {"Tones on Tail", "Bauhaus", "Sisters of Mercy"};
// Вывод элементов в порядке их объявления.
Console.WriteLine("-> Here is the array:");
for (int i = 0; i < gothicBands.Length; i++)
,
{
// Вывод элемента.
Console.Write(gothicBands[i] + ", " );
}
Console.WriteLine("\n");
// Изменение порядка следования элементов на обратный...
Array.Reverse(gothicBands);
Console.WriteLine("-> The reversed array");
// . . . и их вывод.
for (int i = 0 ; i < gothicBands.Length; i++)
{
// Вывод элемента.
Console.Write(gothicBands[i] + ", ");
}
Console.WriteLine("\n");
// Удаление всех элементов, кроме одного.
Console.WriteLine("-> Cleared out all but one...");
Array.Clear(gothicBands, 1, 2);
for (int i = 0; l < gothicBands.Length; i++)
{
// Вывод элемента.
Console.Write(gothicBands[i] + ", " );
}
Console.WriteLine();
}
В результате вызова этого метода в Main () можно получить следующий вывод:
=> Working with System.Array.
-> Here is the array:
Tones on Tail, Bauhaus, Sisters of Mercy,
-> The reversed array
Sisters of Mercy, Bauhaus, Tones on Tail,
-> Cleared out all but one. ..
Sisters of Mercy, , ,
Глава 4. Главные конструкции программирования на С#: часть II
167
Обратите внимание, что многие из членов класса System.Array определены как
статические и потому, следовательно, могут вызываться только на уровне класса (на­
пример, методы Array.Sort () и Array.Reverse ()). Таким методам передается массив,
подлежащий обработке. Остальные члены System. Array (вроде свойства Length) дей­
ствуют на уровне объекта и потому могут вызываться прямо на массиве.
Исходный код. Приложение FunWithArrays доступно в подкаталоге Chapter 4.
Тип enum
Как рассказывалось в главе 1, в состав системы типов .NET входят классы, структу­
ры, перечисления, интерфейсы и делегаты. Для начала рассмотрим роль перечислений
(enum), создав новый проект типа Console Application по имени FunWithEnums.
При построении той или иной системы зачастую удобно создавать набор симво­
лических имен, которые отображаются на известные числовые значения. Например,
в случае создания системы начисления заработной платы может понадобиться ссы­
латься на сотрудников определенного типа (ЕтрТуре) с помощью таких констант, как
VicePresident (вице-президент), Manager (менеджер), Contractor (подрядчик) и Grunt
(рядовой сотрудник). Для этой цели в C# поддерживается понятие специальных пере­
числений. Например, ниже показано специальное перечисление по имени ЕтрТуре:
// Специальное перечисление,
enum ЕтрТуре
{
Manager,
// = 0
Grunt,
// = 1
Contractor,
// = 2
VicePresident
// = 3
}
В перечислении ЕтрТуре определены четыре именованных константы, которые со­
ответствуют дискретным числовым значениям. По умолчанию первому элементу при­
сваивается значение 0, а всем остальным элементам значения присваиваются соглас­
но схеме п+1. При желании исходное значение можно изменять как угодно. Например,
если в данном примере нумерация членов ЕтрТуре должна идти с 102 до 105, необхо­
димо поступить следующим образом:
// Начинаем нумерацию со
enum ЕтрТуре
{
Manager = 102,
Grunt,
//
Contractor,
//
VicePresident
//
значения 102.
= 103
= 104
= 105
Нумерация в перечислениях вовсе не обязательно должна быть последовательной и
содержать только уникальные значения. Например, вполне допустимо (по той или иной
причине) сконфигурировать перечисление ЕтрТуре показанным ниже образом:
// Значения элементов в перечислении вовсе не обязательно
// должны идти в последовательном порядке.
enum ЕтрТуре
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 9
168
Часть II. Главные конструкции программирования на C#
Управление базовым типом, используемым
для хранения значений перечисления
По умолчанию для хранения значений перечисления используется тип System.
Int32 (который в C# называется просто int); однако при желании его легко заменить.
Перечисления в C# можно определять аналогичным образом для любых других ключе­
вых системных типов (byte, short, int или long). Например, чтобы сделать для пере­
числения ЕтрТуре базовым тип byte, а не int, напишите следующий код:
//На этот раз ЕшрТуре отображается на тип byte.
enum ЕшрТуре : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 9
}
Изменять базовый тип перечислений удобно в случае создания таких приложений
.NET, которые будут развертываться на устройствах с небольшим объемом памяти (та­
ких как поддерживающие .NET сотовые телефоны или устройства PDA), чтобы эконо­
мить память везде, где только возможно. Естественно, если для перечисления в качест­
ве базового типа указан byte, каждое значение в этом перечислении ни в коем случае
не должно выходить за рамки диапазона его допустимых значений. Например, приве­
денная ниже версия ЕшрТуре вызовет ошибку на этапе компиляции, поскольку значе­
ние 999 не вписывается в диапазон допустимых значений типа byte:
// Компилятор сообщит об ошибке! Значение 999
// является слишком большим для типа b y te 1
enum ЕшрТуре : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 999
}
Объявление переменных типа перечислений
После указания диапазона и базового типа перечисление можно использовать вме­
сто так называемых “магических чисел”. Поскольку перечисления представляют собой
не более чем просто определяемый пользователем тип данных, их можно применять в
качестве возвращаемых функциями значений, параметров методов, локальных пере­
менных и т.д. Для примера давайте создадим метод по имени AskForBonus ( ), прини­
мающий в качестве единственного параметра переменную ЕшрТуре. На основе значе­
ния этого входного параметра в окне консоли будет выводиться соответствующий ответ
на запрос о надбавке к зарплате.
class Program
{
static void Main(string [] args)
{
Console.WnteLine ("**** Fun with Enums *****");
// Создание типа Contractor.
ЕшрТуре emp = EmpType.Contractor;
AskForBonus(emp);
Console.ReadLine ();
Глава 4. Главные конструкции программирования на С#: часть II
169
// Использование перечислений в качестве параметра.
static void AskForBonus(EmpType e)
{
switch (e)
{
case EmpType.Manager:
Console .WnteLine ("How about stock options instead?");
//H e желаете ли взамен фондовые опционы?
break;
case EmpType.Grunt:
Console.WriteLine("You have got to be kidding...");
// Шутить изволите. ..
break;
case EmpType.Contractor:
Console.WriteLine("You already get enough cash...");
// У вас уже достаточно наличности. ..
break;
case EmpType.VicePresident:
Console.WriteLine("VERY GOOD, Sir1"); // Очень хорошо, сэр!
break;
}
}
}
Обратите внимание, что при присваивании значения переменной перечисления пе­
ред значением (Grunt) должно обязательно указываться имя перечисления (EmpType).
Из-за того, что перечисления представляют собой фиксированные наборы пар “имя/
значение”, устанавливать для переменной перечисления значение, которое не опреде­
лено напрямую в перечисляемом типе, не допускается:
static void ThisMethodWillNotCompile ()
{
// Ошибка! Значения SalesManager в перечислении ЕшрТуре нет!
EmpType emp = EmpType.SalesManager;
// Ошибка! Забыли указать имя перечисления ЕшрТуре перед значением Grunt!
emp = Grunt;
Тип S y s t e m .E n u m
Интересный аспект перечислений в .NET связан с тем, что функциональность они
получают от типа класса System.Enum. В этом классе предлагается набор методов, ко­
торые позволяют опрашивать и преобразовывать заданное перечисление. Одним из
наиболее полезных среди них является метод Enum.GetUnderlyingType (), который
возвращает тип данных, используемый для хранения значений перечислимого типа
(в рассматриваемом объявлении EmpType это System.Byte):
static void Main(string [] args)
{
Console.WriteLine("**** Fun with Enums *****");
// Создание типа Contractor.
EmpType emp = EmpType.Contractor;
AskForBonus(emp);
// Отображение информации о типе, который используется
// для хранения значений перечисления.
Console.WriteLine("EmpType uses a {0} for storage",
Enum.GetUnderlyingType(emp.GetType()));
Console.ReadLine() ;
}
170
Часть II. Главные конструкции программирования на C#
Заглянув в окно O bject Brow ser (Браузер объектов) в Visual Studio 2010, можно удо­
стовериться, что метод Enum. GetUnderlyingType () требует передачи в качестве пер­
вого параметра System.Туре. Как будет подробно разъясняться в главе 16, Туре пред­
ставляет описание метаданных для заданной сущности .NET.
Один из возможных способов получения метаданных (как показывалось ранее) пре­
дусматривает применение метода Get Туре ( ), который является общим для всех типов
в библиотеках базовых классов .NET. Другой способ состоит в использовании операции
typeof, поддерживаемой в С#. Одно из преимуществ этого способа связано с тем, что
он не требует объявления переменной сущности, описание метаданных которой требу­
ется получить:
//На этот раз для извлечения информации
/ / о типе применяется операция typeof.
Console .WnteLine ("EmpType uses a {0} for storage",
Enum.GetUnderlyingType(typeof(EmpType)));
Динамическое обнаружение пар “имя/значение” перечисления
Помимо метода Enum.GetUnderlyingType () , все перечисления C# также поддержи­
вают метод по имени ToString ( ) , который возвращает имя текущего значения пере­
числения в виде строки. Ниже приведен соответствующий пример кода:
static void Main(string[] args)
{
Console.WriteLine ("**** Fun with Enums *****");
EmpType emp = EmpType.Contractor;
// Выводит строку "emp is a C on tractor".
Console.WriteLine("emp is a {0}.", emp.ToString());
Console.ReadLine() ;
1
Чтобы выяснить не имя, а значение определенной переменной перечисления, можно
просто привести ее к лежащему в основе типу хранения. Ниже показан пример, как это
делается:
static void Main(string[] args)
{
Console.WriteLine("**** Fun with Enums *****");
EmpType emp = EmpType.Contractor;
// Выводит строку "Contractor = 100".
Console.WriteLine("{0} = {1}", emp.ToString() , (byte)emp);
Console.ReadLine();
На заметку! Статический метод Enum. Format () позволяет производить более точное форматиро­
вание за счет указания флага, представляющего желаемый формат. Более подробную инфор­
мацию о нем можно найти в документации .NET Framework 4.0 SDK.
В System. Enum также предлагается еще один статический метод по имени
GetValues ( ). Этот метод возвращает экземпляр System. Array. Каждый элемент в мас­
сиве соответствует какому-то члену в указанном перечислении. Для примера рассмот­
рим показанный ниже метод, который выводит в окне консоли пары “имя/значение”,
имеющиеся в передаваемом в качестве параметра перечислении:
// Этот метод отображает детали любого перечисления.
static void EvaluateEnum(System.Enum e)
{
Глава 4. Главные конструкции программирования на С#: часть II
171
Console.WriteLine("=> Information about {0}",
e .GetType().Name);
Console.WriteLine("Underlying storage type: {0}",
Enum.GetUnderlyingType(e.GetType()));
// Получение всех nap "имя/значение"
//
для входн ого п ар ам етр а.
Array enumData = Enum.GetValues(е.GetType());
Console.WriteLine("This enum has {0} members.", enumData.Length);
// Количество членов:
/ Л Вы вод с т р о к о в о г о и м ен и и а с с о ц и и р у е м о г о з н а ч е н и я
/ / с
и сп о л ь зо в а н и е м ф л ага D д л я ф орм ати рован и я
for(int i = 0 ;
(см .
гл а в у 3) .
i < enumData.Length; i++)
{
Console.WriteLine ("Name: {0}, Value: {0:D}",
enumData.GetValue (i));
}
Console.WriteLine();
Чтобы протестировать этот новый метод, давайте обновим Main () так, чтобы в нем
создавались переменные нескольких объявленных в пространстве имен System типов
перечислений (вместе с перечислением ЕшрТуре):
static void Main(string[] args)
{
Console.WriteLine("**** Fun with Enums *****");
EmpType e2 = EmpType.Contractor;
// Эти типы представляют собой перечисления
//и з пространства имен System.
DayOfWeek day = DayOfWeek.Monday;
ConsoleColor cc = ConsoleColor.Gray;
EvaluateEnum(e2);
EvaluateEnum(day);
EvaluateEnum(cc);
Console.ReadLine();
}
На рис. 4.3 показано, как будет выглядеть вы­
вод в этом случае.
Как можно будет убедиться по ходу настоящей
книги, перечисления очень широко применяют­
ся во всех библиотеках базовых классов .NET.
Например, в ADO.NET множество перечислений
используется для обозначения состояния соеди­
нения с базой данных (например, открыто оно
или закрыто) и состояния строки в D a ta T a b le
(например, является она измененной, новой или
отсоединенной). Поэтому в случае применения
любых перечислений следует всегда помнить о
наличии возможности взаимодействовать с па­
рами “имя/значение” в них с помощью членов
System.Enum.
Исходный код. Проект FunWithEnums доступен в подкаталоге Chapter 4.________ »_____________________
рис 4 3 Динамическое получеНие пар
“имя/значение" типов перечислений
172
Часть II. Главные конструкции программирования на C#
Типы структур
Теперь, когда роль типов перечислений должна быть ясна, давайте рассмотрим ис­
пользование типов структур (s t r u c t ) в .NET. Типы структур прекрасно подходят для
моделирования математических, геометрических и прочих “атомарных” сущностей в
приложении. Они (как и перечисления) представляют собой определяемые пользовате­
лем типы, однако (в отличие от перечислений) просто коллекциями пар “имя/значение”
не являются. Вместо этого они скорее представляют собой типы, способные содержать
любое количество полей данных и членов, выполняющих над ними какие-то операции.
На заметку! Если вы ранее занимались объектно-ориентированным программированием, можете
считать структуры "облегченными классами” , поскольку они тоже предоставляют возможность
определять тип, поддерживающий инкапсуляцию, но не могут применяться для построения се­
мейства взаимосвязанных типов. Когда есть потребность в создании семейства взаимосвязан­
ных типов через наследование, нужно применять типы классов.
На первый взгляд процесс определения и использования структур выглядит очень
просто, но, как известно, сложности обычно скрываются в деталях. Чтобы приступить
к изучению основных аспектов структур, давайте создадим новый проект по имени
F u n W ith S tru ctu res. В C# структуры создаются с помощью ключевого слова s tr u c t.
Поэтому далее определим в проекте с использованием этого ключевого слова структуру
P o in t и добавим в нее две переменных экземпляра типа i n t и несколько методов для
взаимодействия с ними.
struct Point
{
// Поля структуры.
public int X;
public int Y;
// Добавление 1 к позиции (X, Y) .
public void Increment ()
{
X++; Y++;
}
// Вычитание 1 из позиции (X, Y) .
public void Decrement ()
{
X--; Y— ;
}
// Отображение текущей позиции.
public void Display ()
{
Console.WriteLine("X = {0}, Y = {1}", X, Y) ;
}
}
Здесь определены два целочисленных поля (X и Y) с использованием ключевого слова
p u b lic , которое представляет собой один из модификаторов управления доступом (см.
главу 5). Объявление данных с использованием ключевого слова p u b lic гарантирует
наличие у вызывающего кода возможности напрямую получать к ним доступ из данной
переменной P o in t (через операцию точки).
На заметку! Обычно определение общедоступных (public) данных внутри класса или структуры
считается плохим стилем. Вместо этого рекомендуется определять приватные (private) дан­
ные и получать к ним доступ и изменять их с помощью общ едоступны х свойств. Более под­
робно об этом будет рассказываться в главе 5.
Глава 4. Главные конструкции программирования на С#: часть II
173
Ниже приведен код метода Main ( ), с помощью которого можно протестировать тип
Point.
static void Main(string [] args)
{
Console.WriteLine ("***** A First Look at Structures *****");
// Создание Point с первоначальными значениями X и Y.
Point myPoint;
myPoint.X = 349;
myPoint.Y = 76;
myPoint.Display ();
// Настройка значений X и Y.
myPoint.Increment ();
myPoint.Display ();
Console.ReadLine ();
}
Вывод, как и следовало ожидать, выглядит следующим образом:
***** A First Look at Structures *****
X = 34 9, Y = 76
X = 350, Y = 77
Создание переменных типа структур
Для создания переменной типа структуры на выбор доступно несколько вариантов.
Ниже просто создается переменная P o in t с присваиванием значений каждому из ее
общедоступных элементов данных типа полей перед вызовом ее членов. Если значения
общедоступным элементам структуры (в данном случае X и Y) не присвоены перед ее
использованием, компилятор сообщит об ошибке.
// Ошибка! Полю Y не было присвоено значение.
Point pi;
pl.X = 10;
pi .Display();
// Все в порядке1 Обоим полям были присвоены
// значения перед использованием.
Point р2;
р2 .X = 10;
р2 .Y = 10;
' р 2 .Display();
В качестве альтернативного варианта переменные типа структур можно создавать с
применением ключевого слова new, поддерживаемого в С#, что предусматривает вызов
для структуры конструктора по умолчанию. По определению используемый по умолча­
нию конструктор не принимает никаких аргументов. Преимущество подхода с вызовом
для структуры конструктора по умолчанию состоит в том, что в таком случае каждому
элементу данных полей автоматически присваивается соответствующее значение по
умолчанию:
// Установка для всех полей значений по умолчанию
// за счет применения конструктора по умолчанию.
Point pi = new Point () ;
// Выводит Х=0, Y=0
pi.Display();
Создавать структуру можно также с помощью специального конструктора что по­
зволяет указывать значения для полей данных при создании переменной, а не уста­
174
Часть II. Главные конструкции программирования на C#
навливать их для каждого из них по отдельности. В главе 5 специальные конструкторы
будут рассматриваться более подробно; здесь же, чтобы в общем посмотреть, как их
использовать, изменим структуру Point и добавим в нее следующий код:
struct Point
{
// Поля структуры.
public int X;
public int Y;
// Специальный конструктор.
public Point (int XPos, int YPos)
{
X = XPos;
Y = YPos;
После этого переменные типа Point можно создавать так, как показано ниже:
// Вызов специального конструктора.
Point р2 = new Point (50, 60);
// Выводит Х=50, Y=60
р 2 .Display ();
Как упоминалось ранее, на первый взгляд процесс работы со структурами выглядит
довольно понятно. Однако чтобы лучше разобраться в нем, необходимо знать, в чем
состоит разница между типами значения и ссылочными типами в .NET.
Исходный код. Проект FunWithStructures доступен в подкаталоге Chapter 4.
Типы значения и ссылочные типы
На заметку! В приведенном далее обсуждении типов значения и ссылочных типов предполагается
наличие базовых знаний объектно-ориентированного программирования. Дополнительные све­
дения по этому поводу даны в главах 5 и 6.
В отличие от массивов, строк и перечислений, структуры в C# не имеют эквивалент­
ного представления с похожим названием в библиотеке .NET (т.е. класса вроде System.
Structure не существует), но зато они все неявно унаследованы от класса System.
ValueType. Попросту говоря, роль класса System. ValueType заключается в гарантиро­
вании размещения производного типа (например, любой структуры) в стеке, а не в куче
с автоматически производимой сборкой мусора. Данные, размещаемые в стеке, могут
создаваться и уничтожаться очень быстро, поскольку срок их жизни зависит только от
контекста, в котором они определены. За данными, размещаемыми в куче, наблюдает
сборщик мусора .NETT, и время их существования зависит от целого ряда различных
факторов, которые более подробно рассматриваются в главе 8.
С функциональной точки зрения единственной задачей System.ValueType являет­
ся переопределение виртуальных методов, объявленных в System.Object, так, чтобы
в них использовалась семантика, основанная на значениях, а не на ссылках. Как, воз­
можно, уже известно, под переопределением понимается изменение реализации вирту­
ального (или, что тоже возможно, абстрактного) метода, определенного внутри базового
класса. Базовым классом для ValueType является System.Object. В действительности
Глава 4. Главные конструкции программирования на С#: часть II
175
методы экземпляра, определенные в System. ValueType, идентичны тем, что определе­
ны в System.Object:
// Структуры и перечисления неявным образом
// расширяют возможности System.ValueType.
public abstract class ValueType : object
{
public
public
public
public
virtual bool Equals(object obj ) ;
virtual int GetHashCode();
Type GetTypeO ;
virtual string ToStringO;
}
Из-за того, что в типах значения используется семантика, основанная на значени­
ях, время жизни структуры (которая включает все числовые типы данных, наподобие
int, float и т.д., а также любое перечисление или специальную структуру) получается
очень предсказуемым. При выходе переменной типа структуры за пределы контекста,
в котором она определялась, она сразу же удаляется из памяти.
// Локальные структуры извлекаются из стека
// после завершения метода.
static void LocalValueTypes()
{
//В действительности in t представляет
// собой структуру System.Int32.
int i = 0;
//В действительности Point представляет
// собой тип структуры.
Point р = new Point () ;
} // Здесь i и р изымаются из стека.
Типы значения, ссылочные типы и операция присваивания
Когда один тип значения присваивается другому, получается почленная копия
данных полей. В случае простого типа данных вроде System. Int32 единственным ко­
пируемым членом является числовое значение. Однако в ситуации с типом Point ко­
пироваться в новую переменную структуры будут два значения: X и Y. Чтобы удосто­
вериться в этом, давайте создадим новый проект типа Console Application по имени
ValueAndReferenceTypes, скопируем в новое пространство имен предыдущее опреде­
ление Point и добавим в Program следующий метод:
// Присваивание двух внутренних типов значения друг другу
// приводит к созданию двух независимых переменных в стеке.
static void ValueTypeAssignment()
{
Console.WriteLine("Assigning value types\n");
Point pi = new Point (10, 10);
Point p2 = pi;
// Вывод обеих переменных Point,
pi .Display();
p2 .Display();
// Изменение значение pl.X и повторный вывод.
// Значение р2.Х не изменяется.
pl.X = 100;
Console.WriteLine("\n=> Changed pl.X\n");
pi .Display();
p2.Display();
}
176
Часть II. Главные конструкции программирования на C#
В коде сначала создается переменная типа P o in t (pi), которая затем присваивается
другой переменной типа P o in t (р2). Из-за того, что P o in t представляет собой тип зна­
чения, в стеке размещаются две копии MyPoint, каждая из которых может подвергать­
ся отдельным манипуляциями. Поэтому при изменении значения p i .X значение р2 .X
остается неизменным. Ниже показано, как будет выглядеть вывод в случае выполнения
этого кода:
Assigning value types
X = 10, Y = 10
X = 10, Y = 10
=> Changed p i .X
X = 100, Y = 10
X = 10, Y = 10
В отличие от присваивания одного типа значения другому, в случае применения
операции присваивания в отношении ссылочных типов (т.е. всех экземпляров класса)
происходит переадресация на то, на что ссылочная переменная указывает в памяти.
Чтобы увидеть это на примере, давайте создадим новый тип класса по имени P o in tR e f
с точно такими же членами, как у структуры P o in t, только переименуем конструктор в
соответствие с именем этого класса:
// Классы всегда представляют собой ссылочные типы.
class PointRef
{
// Те же члены, что и в структуре Point. . .
// Здесь нужно не забыть изменить имя конструктора на PointRef.
public PointRef(int XPos, int YPos)
{
X = XPos;
Y = YPos;
}
}
Теперь воспользуемся этим типом P o in t R e f в показанном ниже новом методе.
Обратите внимание, что за исключением применения класса P o in tR e f, а не структу­
ры P o in t, код в целом выглядит точно так же, как и код приведенного ранее метода
V a lu eT yp eA ssign m en t( ) .
static void ReferenceTypeAssignment ()
{
Console.WriteLine("Assigning reference types\n");
PointRef pi = new PointRef (10, 10);
PointRef p2 = pi;
// Вывод обеих переменных PointRef.
p i .Display();
p 2 .Display();
// Изменение значения pl.X и повторный вывод.
pl.X = 100;
Console.WriteLine("\n=> Changed pl.X\n");
p i .Display();
p 2 .Display();
}
В данном случае создаются две ссылки, указывающие на один и тот же объект в
управляемой куче. Поэтому при изменении значения X по ссылке р2 значение p l.X ос­
тается прежним. Ниже показано, как будет выглядеть вывод в случае вызова этого но­
вого метода из Main ().
Глава 4. Главные конструкции программирования на С#: часть II
177
Assigning reference types
X = 10, Y = 10
X = 10, Y = 10
=> Changed p i .X
X = 100, Y = ‘10
X = 100, Y = 10
Типы значения, содержащие ссылочные типы
Теперь, когда стало более понятно, в чем состоят основные отличия между типами
значения и ссылочными типами, давайте рассмотрим более сложный пример. Сначала
предположим, что в распоряжении имеется следующий ссылочный тип (класс) с инфор­
мационной строкой (in fo S t r in g ), которая может устанавливаться с помощью специ­
ального конструктора:
class Shapelnfo
{
public string infoString;
public Shapelnfo(string info)
{ infoString = info; }
1
Пусть необходимо, чтобы переменная этого типа класса содержалась внутри типа
значения по имени R ecta n gle, и чтобы вызывающий код мог устанавливать значение
внутренней переменной экземпляра S h apeln fo, для чего также можно предусмотреть
специальный конструктор. Ниже показано полное определение типа R ec ta n gle.
struct Rectangle
{
// Структура Rectangle содержит член ссылочного типа,
public Shapelnfo rectlnfo;
public int rectTop, rectLeft, rectBottom, rectRight;
public Rectangle(string info, int top, int left, int bottom, int right)
{
rectlnfo = new Shapelnfo(info);
rectTop = top; rectBottom = bottom;
rectLeft = left; rectRight = right;
}
public void Display()
{
Console .WnteLine ("String = {0}, Top = {1}, Bottom = {2}," +
"Left = {3}, Right = {4}",
rectlnfo.inf oStnng, rectTop, rectBottom, rectLeft, rectRight);
}
}
Ссылочный тип содержится внутри типа значения, и отсюда, естественно, вытекает
важный вопрос о том, что же произойдет в результате присваивания одной переменной
типа R e c ta n g le другой? Исходя из того, что известно о типах значения, можно верно
предположить, что целочисленные данные (которые в действительности образуют струк­
туру) должны являться независимой сущностью для каждой переменной R ec ta n g le. Но
что будет с внутренним ссылочным типом? Будут ли копироваться все данные о состоя­
нии этого объекта, или же только ссылка на него? Чтобы получить ответ на этот вопрос,
определим показанный ниже метод и вызовем его в Main ().
178
Часть II. Главные конструкции программирования на C#
static void ValueTypeContainingRefType ()
{
// Создание первой переменной Rectangle.
Console .WnteLine ("-> Creating rl");
Rectangle rl = new Rectangle ("First Rect", 10, 10, 50, 50);
// Присваивание второй переменной Rectangle ссылки на rl.
Console .WnteLine ("-> Assigning r2 to rl");
Rectangle r2 = rl;
// Изменение некоторых значений в г2.
Console.WnteLine ("-> Changing values of r2");
r2.rectInfо .infoString = "This is new info1";
r2.rectBottom = 4444;
// Вывод обеих переменных.
rl.Display();
r2.Display();
}
Ниже показан вывод, полученный в результате вызова этого метода:
-> Creating rl
-> Assigning r2 to rl
-> Changing values of r2
String = This is new info!, Top = 10, Bottom = 50, Left = 10, Right = 50
String = This is new info!, Top = 10, Bottom = 4444, Left = 10, Right = 50
Нетрудно заметить, что при изменении значения информационной строки с исполь­
зованием ссылки г2, значение ссылки r l остается прежним. По умолчанию, когда внут­
ри типа значения содержатся другие ссылочные типы, операция присваивания при­
водит к копированию ссылок. В результате получаются две независимых структуры, в
каждой из которых содержится ссылка, указывающая на один и тот же объект в памяти
(т.е. поверхностная копия). При желании получить детальную копию, при которой в но­
вый объект копируются полностью все данные состояния внутренних ссылок, в качест­
ве одного из способов можно реализовать интерфейс ICloneable (см. главу 9).
Исходный код. Проект ValueAndReferenceTypes доступен В подкаталоге Chapter 4.
Передача ссылочных типов по значению
Очевидно, что ссылочные типы и типы значения могут передаваться членам в виде
параметров. Способ передачи ссылочного типа (например, класса) по ссылке, однако
довольно сильно отличается от способа его передачи по значению. Чтобы посмотреть,
в чем разница, давайте создадим новый проект типа C o n so le Application по имени
RefTypeValTypeParams и определим в нем следующий простой класс Person:
class Person
{
public string personName;
public int personAge;
// Конструкторы.
public Person(string name, int age)
{
personName = name;
personAge = age;
}
public Person () {}
Глава 4. Главные конструкции программирования на С#: часть II
179
public void Display ()
{
Console.WriteLine ("Name: {0}, Age: {1}", personName, personAge);
}
}
Теперь создадим метод, позволяющий вызывающему коду передавать тип Person по
значению (обратите внимание, что никакие модификаторы для параметров, подобные
out или ref, здесь не используются):
static void SendAPersonByValue(Person p)
{
// Изменение значения возраста в р.
р.personAge = 99;
// Увидит ли вызывающий код это изменение?
р = new Person("Nikki", 99);
}
Важно обратить внимание, что метод SendAPersonByValue () пытается присвоить
входному аргументу Person ссылку на новый объект Person, а также изменить некото­
рые данные состояния. Испробуем этот метод, вызвав его в Main ( ):
static void Main(string [] args)
{
// Передача ссылочных типов no значению.
Console.WriteLine ("***** Passing Person object by value *****");
Person fred = new Person ("Fred", 12);
Console.WriteLine("\nBefore by value call, Person is:");
// перед вызовом
fred.Display();
SendAPersonByValue(fred);
Console.WriteLine("\nAfter by value call, Person is:");
fred. Display();
Console.ReadLine();
// после вызова
Ниже показан результирующий вывод:
***** Passing Person object by value *****
Before by value call, Person is:
Name: Fred, Age: 12
After by value call, Person is:
Name: Fred, Age: 99
Как видите, значение PersoneAge изменилось. Кажется, что такое поведение про­
тиворечит смыслу передачи параметра “по значению”. Из-за удавшейся попытки из­
менить состояние входного объекта Person возникает вопрос о том, что же тогда
было скопировано? А вот что: ссылка на объект вызывающего кода. Поскольку метод
SendAPersonByValue () теперь указывает на тот же самый объект, что и вызывающий
код, получается, что данные состояния объекта можно изменять. Что нельзя делать, так
это изменять объект, на который указывает ссылка.
Передача ссылочных типов по ссылке
Теперь создадим метод SendAPersonByReference ( ) , в котором ссылочный тип пе­
редается по ссылке (обратите внимание, что здесь для параметра используется моди­
фикатор ref):
public static void SendAPersonByReference (ref Person p)
{
// Изменение некоторых данных в р.
р.personAge = 555;
180
Часть II. Главные конструкции программирования на C#
// Теперь р указывает на новый объект в куче.
р = new Person("Nikki", 222);
}
Нетрудно догадаться, что такой подход предоставляет вызывающему коду полную
свободу в плане манипулирования входным параметром. Вызывающий код может не
только изменять состояние объекта, но и переопределять ссылку так, чтобы она указыва­
ла на новый тип Person. Давайте испробуем новый метод SendAPersonByReference ( ) ,
вызвав его в методе Main (), как показано ниже.
static void Main(string [] args)
{
// Передача ссылочных типов по ссылке.
Console. WriteLine ("***** Passing Person object by reference *****");
Person mel = new Person ("Mel", 23);
Console.WriteLine("Before by ref call, Person is:");
mel.Display();
SendAPersonByReference(ref mel);
Console.WriteLine("After by ref call, Person is:");
mel.Display();
Console.ReadLine();
}
Ниже показано, как в этом случае выглядит вывод:
***** Passing Person object by reference *****
Before by ref call, Person is:
Name: Mel, Age: 23
After by ref call, Person is:
Name: Nikki, Age: 999
Как не трудно заметить, после вызова объект по имени Mel возвращается как объект
по имени Nikki, поскольку методу удалось изменить тип, на который указывала входная
ссылка в памяти. Ниже перечислены главные моменты, которые можно вынести из все­
го этого и о которых следует всегда помнить при передаче ссылочных типов.
• В случае передачи ссылочного типа по ссылке вызывающий код может изменять
значения данных состояния объекта, а также сам объект, на который указывает
входная ссылка.
• В случае передачи ссылочного типа по значению вызывающий код может изме­
нять только значения данных состояния объекта, но не сам объект, на который
указывает входная ссылка.
Исходный код. Проект RefTypeValTypeParams доступен В подкаталоге Chapter 4.
Заключительные детали относительно типов
значения и ссылочных типов
В завершение темы ссылочных типов и типов значения ознакомьтесь с табл. 4.3, где
приведено краткое описание всех основных отличий между типами значения и ссылоч­
ными типами.
Несмотря на все эти отличия, не следует забывать о том, что типы значения и
ссылочные типы обладают способностью реализовать интерфейсы и могут поддержи­
вать любое количество полей, методов, перегруженных операций, констант, свойств и
событий.
Глава 4. Главные конструкции программирования на С#: часть II
181
Таблица 4.3. Отличия между типами значения и ссылочными типами
Интересующий вопрос
Тип значения
Ссылочный тип
Где размещается этот тип?
В стеке
В управляемой куче
Как представляется переменная?
В виде локальной копии
В виде ссылки, указываю­
щей на занимаемое соот­
ветствующим экземпляром
место в памяти
Какой тип является базовым?
Должен обязательно на­
следоваться от System.
ValueType
Может наследоваться от
любого другого типа (кроме
System.ValueType), глав­
ное чтобы тот не был запеча­
танным (см. главу 6)
Может ли этот тип выступать в
роли базового для других типов?
Нет. Типы значения всегда
являются запечатанными,
поэтому наследовать от них
нельзя
Да. Если тип не является
запечатанным, он может вы­
ступать в роли базового типа
для других типов
Каково по умолчанию поведение
при передаче параметров?
Переменные этого типа пе­
редаются по значению (т.е.
вызываемой функции пере­
дается копия переменной)
В случае типов значения
объект копируется по значе­
нию, а в случае ссылочных
типов ссылка копируется по
значению
Может ли в этом типе переоп­
ределяться метод System.
Нет. Типы значения, никогда
не размещаются в куче и
потому в финализации не
нуждаются
Да, неявным образом
(см. главу 8)
Можно ли для этого типа опреде­
лять конструкторы?
Да, но при этом следует
помнить, что имеется заре­
зервированный конструктор
по умолчанию (это значит,
что все специальные конст­
рукторы должны обязатель­
но принимать аргументы)
Безусловно!
Когда переменные этого типа пре­
кращают свое существование?
Когда выходят за рамки
того контекста, в котором
определялись
Когда объект подвергается
сборке мусора
Object.Finalize()?
Нулевые типы в C#
В заключение настоящей главы давайте рассмотрим роль нулевых типов дан­
ных (nullable data types) на примере создания консольного приложения по имени
NullableTypes. Как уже известно, типы данных CLR обладают фиксированным диа­
пазоном значений и имеют соответствующий представляющий их тип в пространст­
ве имен System. Например, типу данных System.Boolean могут присваиваться только
значения из набора {true, false). Вспомните, что все числовые типы данных (а также
тип Boolean) представляют собой типы значения. Таким типам значение null никогда
не присваивается, поскольку оно служит для установки пустой ссылки на объект:
static void Main(string[] args)
{
// Компилятор сообщит об ошибке!
// Типам значения не может присваиваться значение n u ll!
182
Часть II. Главные конструкции программирования на C#
bool myBool = null;
int mylnt = null;
// Здесь все в порядке, потому что строки представляют собой ссылочные типы.
string my S t n n g = null;
}
Возможность создавать нулевые типы появилась еще в версии .NET 2.0. Попросту
говоря, нулевой тип может принимать все значения лежащего в его основе типа плюс
значение null. Если объявить нулевым, например, тип bool, его допустимыми значе­
ниями будут true, false и null. Это может оказаться чрезвычайно удобным при ра­
боте с реляционными базами данных, поскольку в их таблицах довольно часто встре­
чаются столбцы с неопределенными значениями. Помимо нулевого типа данных в C#
больше не существует никакого удобного способа для представления элементов данных,
не имеющих значения.
Чтобы определить переменную нулевого типа, необходимо присоединить к имени
лежащего в основе типа данных знак вопроса (?). Обратите внимание, что примене­
ние такого синтаксиса является допустимым лишь в отношении типов значения. При
попытке создать нулевой ссылочный тип (в том числе нулевой тип string) компиля­
тор будет сообщать об ошибке. Как и ненулевым переменным, нулевым локальным пе­
ременным должно быть присвоено начальное значение, прежде чем их можно будет
использовать.
static void LocalNullableVariables ()
{
// Определение нескольких локальных переменных с нулевыми типами.
int? nullablelnt = 10;
double? nullableDouble = 3.14;
bool? nullableBool = null;
char? nullableChar = 'a';
int?[] arrayOfNullablelnts = new int? [10];
// Ошибка! Строки относятся к ссылочным типам!
// string? s = "oops";
}
Синтаксис с использованием знака ? в качестве суффикса в C# представля­
ет собой сокращенный вариант создания экземпляра обобщенного типа структуры
System.Nullable<T>. Хотя об обобщениях будет подробно рассказываться лишь в гла­
ве 10, уже сейчас важно понять, что тип System.Nullable<T> имеет набор членов, ко­
торые могут применяться во всех нулевых типах.
Например, выяснить программным образом, было ли нулевой переменной действи­
тельно присвоено значение null, можно с помощью свойства Has Value или операции
! =, а получить присвоенное нулевому типу значение — либо напрямую, либо через свой­
ство Value. На самом деле, из-за того, что суффикс ? является всего лишь сокращен­
ным вариантом использования типа Nullable<T>, метод LocalNullableVariables ()
вполне можно было бы реализовать и следующим образом:
static void LocalNullableVariablesUsingNullable ()
{
// Определение нескольких нулевых типов за счет использования Nullable<T>.
Nullable<int> nullablelnt = 10;
Nullable<double> nullableDouble = 3.14;
Nullable<bool> nullableBool = null;
Nullable<char> nullableChar = 'a';
Nullable<int> [] arrayOfNullablelnts = new int?[10];
}
Глава 4. Главные конструкции программирования на С#: часть II
183
Работа с нулевыми типами
Как упоминалось ранее, нулевые типы данных особенно полезны при взаимодейст­
вии с базами данных, поскольку некоторые столбцы внутри таблиц данных в них могут
преднамеренно делаться пустыми (с неопределенными значениями). Для примера рас­
смотрим приведенный ниже класс, в котором имитируется процесс получения досту­
па к базе данных с таблицей, два столбца в которой могут принимать значения null.
Обратите внимание, что в методе GetlntFromDatabase () значение нулевой целочис­
ленной переменной экземпляра не присваивается, а в методе GetBoolFromDatabase ()
значение члену bool? присваивается:
class DatabaseReader
{
// Нулевые поля данных.
public int? numencValue = null;
public bool? boolValue = true;
// Обратите внимание на использование
// нулевого возвращаемого типа.
public int? GetlntFromDatabase ()
{ return numencValue; }
// Здесь тоже обратите внимание на использование
// нулевого возвращаемого типа.
public bool? GetBoolFromDatabase ()
{ return boolValue; }
}
Теперь давайте создадим следующий метод Main (), в котором будет вызываться ка­
ждый из членов класса DatabaseReader и выясняться, какие значения были им при­
своены, с помощью членов HasValue и Value, а также поддерживаемой в C# операции
равенства (точнее — операции “не равно”):
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Nullable Data *****\n");
DatabaseReader dr = new DatabaseReader ();
// Получение значения in t из "базы данных".
int? i = dr.GetlntFromDatabase();
if (i.HasValue)
// вывод значения l
Console.WriteLine("Value of 'i' is: {0}", i.Value);
else
// значение i не определено
Console.WriteLine("Value of 'i ' is undefined.");
// Получение значения bool из "базы данных".
bool? b = dr.GetBoolFromDatabase();
if (b != null)
// вывод значения b
Console.WriteLine("Value of 'b' is: {0}", b.Value);
else
// значение b не определено
Console.WriteLine("Value of 'b' is undefined.");
Console.ReadLine ();
}
184
Часть II. Главные конструкции программирования на C#
Операция ‘ ? ? 5
Последним аспектом нулевых типов, о котором следует знать, является использова­
ние с ними операции ??, поддерживаемой в С#. Эта операция позволяет присваивать
значение нулевому типу, если извлеченное значение равно n u ll. Для примера предпо­
ложим, что в случае возврата методом GetlntFrom D atabase () значения n u ll (разуме­
ется, этот метод запрограммирован всегда возвращать n u ll, но тут главное уловить об­
щую идею) локальной целочисленной переменной нулевого типа должно присваиваться
значение 100:
static void Main(string [] args)
{
Console .WnteLine ("***** Fun with Nullable Data *****\n");
DatabaseReader dr = new DatabaseReader();
// В случае возврата GetlntFromDatabase()
// значения n u ll локальной переменной должно
// присваиваться значение 100.
int myData = d r .GetlntFromDatabase() ?? 100;
Console .WnteLine ("Value of myData: {0}", myData);
Console.ReadLine();
}
Преимущество подхода с применением операции ? ? в том, что он обеспечивает бо­
лее компактную версию кода, чем применение традиционной условной конструкции
i f / e ls e . При желании можно написать следующий функционально эквивалентный
код, который в случае n u ll устанавливает значение переменной равным 100:
// Более длинная версия применения синтаксиса ? : ??.
int? moreData = dr .GetlntFromDatabase();
if (!moreData.HasValue)
moreData = 100;
Console.WriteLine("Value of moreData: {0}", moreData);
Исходный код. Приложение NullableType доступно в подкаталоге Chapter 4.
Резюме
В настоящей главе сначала рассматривался ряд ключевых слов С#, которые позво­
ляют создавать специальные методы. Вспомните, что по умолчанию параметры переда­
ются по значению, однако их можно передавать и по ссылке за счет добавления к ним
модификатора r e f или out. Также было рассказано о роли необязательных и именован­
ных параметров и о том, как определять и вызывать методы, принимающие массивы
параметров.
В главе рассматривалась перегрузка методов, определение массивов, перечислений
и структур в C# и их представления в библиотеках базовых классов .NET. Были описаны
некоторые детали, касающиеся т ипов значения и ссылочных типов, в том числе то,
как они ведут себя при передаче в качестве параметров методам, и то, каким образом
взаимодействовать с нулевыми типами данных с помощью операций ? и ? ?.
На этом первоначальное знакомство с языком программирования C# заверше­
но. В следующей главе начинается изучение деталей объектно-ориентированной
разработки.
ГЛАВА
5
Определение
инкапсулированных
типов классов
предыдущих двух главах мы исследовали ряд основных синтаксических конст­
рукций, присущих любому приложению .NET, которое вам придется разрабаты­
вать. Здесь мы приступим к изучению объектно-ориентированных возможностей С#.
Первое, что предстоит узнать — это процесс построения четко определенных типов
классов, которые поддерживают любое количество конструкторов. После описания ос­
нов определения классов и размещения объектов в остальной части главы рассматри­
вается тема инкапсуляции. По ходу изложения вы узнаете, как определяются свойства
класса, а также какова роль статических полей, синтаксиса инициализации объектов,
полей, доступных только для чтения, константных данных и частичных классов.
В
Знакомство с типом класса C#
Что касается платформы .NET, то наиболее фундаментальной программной конст­
рукцией является тип класса. Формально класс — это определяемый пользователем
тип, который состоит из данных полей (часто именуемых переменными-членами) и чле­
нов, оперирующих этими данными (конструкторов, свойств, методов, событий и т.п.).
Все вместе поля данных класса представляют “состояние” экземпляра класса (иначе
называемого объектом). Мощь объектных языков, подобных С#, состоит в их способно­
сти группировать данные и связанную с ними функциональность в определении класса,
что позволяет моделировать программное обеспечение на основе сущностей реального
мира.
Для начала создадим новое консольное приложение C# по имени SimpleClassExample.
Затем добавим в проект новый файл класса (по имени Car.cs), используя пункт меню
Project^Add Class (ПроектаДобавить класс), выберем пиктограмму Class (Класс) в ре­
зультирующем диалоговом окне, как показано на рис. 5.1, и щелкнем на кнопке Add
(Добавить).
Класс определятся в C# с помощью ключевого слова c la s s . Вот как выглядит про­
стейшее из возможных объявление класса:
class Саг
{
}
186
Часть II. Главные конструкции программирования на C#
Рис. 5.1. Добавление нового типа класса C#
После определения типа класса нужно определить набор переменных-членов, ко­
торые будут использоваться для представления его состояния. Например, вы можете
решить, что объекты Саг (автомобили) должны иметь поле данных типа in t, представ­
ляющее текущую скорость, и поле данных типа s t r in g для представления дружествен­
ного названия автомобиля. С учетом этих начальных положений дизайна класс Саг
будет выглядеть следующим образом:
class Саг
// 'Состояние' объекта Саг.
public string petName;
public int currSpeed;
}
Обратите внимание, что эти переменные-члены объявлены с использованием моди­
фикатора доступа p u b lic . Общедоступные (p u b lic ) члены класса доступны непосред­
ственно, как только создается объект данного типа. Как вам, возможно, уже известно,
термин “объект” служит для представления экземпляра данного типа класса, созданно­
го с помощью ключевого слова new.
На заметку! Поля данных класса редко (если вообще когда-нибудь) должны определяться с моди­
фикатором p u b lic . Чтобы обеспечить целостность данных состояния, намного лучше объявлять
данные приватными (p r iv a te ) или, возможно, защищенными (p r o te c te d ) и открывать контро­
лируемый доступ к данным через свойства типа (как будет показано далее в этой главе). Однако
чтобы сделать первый пример насколько возможно простым, оставим данные общедоступными.
После определения набора переменных-членов, представляющих состояние класса,
следующим шагом в проектировании будет создание членов, которые моделируют его
поведение. Для данного примера класс Саг определяет один метод по имени Speedup () и
еще один — по имени P r in t S t a t e (). Модифицируйте код класса следующим образом:
class Саг
{
// 'Состояние' объекта Саг.
public string petName;
Глава 5. Определение инкапсулированных типов классов
187
public int currSpeed;
// Функциональность Car.
public void Pnn t S t a t e O
{
Console.WriteLine ("{0 } is going {1} MPH.", petName, currSpeed);
}
public void SpeedUp(int delta)
{
currSpeed += delta;
Метод P r in t S ta t e () — это более или менее диагностическая функция, которая про­
сто выводит текущее состояние объекта Саг в окно командной строки. Метод SpeedUp ()
повышает скорость Саг, увеличивая ее на величину, переданную во входящем парамет­
ре типа in t. Теперь обновите код метода M ain(), как показано ниже:
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Class Types *****\n");
// Разместить в памяти и сконфигурировать объект Саг.
Car myCar = new Car();
myCar.petName = "Henry";
myCar.currSpeed = 10;
// Повысить скорость автомобиля в несколько раз
/ / и вывести новое состояние.
for (int i = 0; i <= 10; i++)
{
myCar.SpeedUp (5) ;
myCar.PrintState ();
}
Console.ReadLine ();
Запустив программу, вы увидите, что переменная Car (myCar) поддерживает свое те­
кущее состояние на протяжении жизни всего приложения, как показано в следующем
выводе:
★**** Fun with Class Types
Henry
Henry
Henry
Henry
Henry
Henry
Henry
Henry
Henry
Henry
Henry
is
is
is
is
is
is
is
is
is
is
is
going
going
going
going
going
going
going
going
going
going
going
15
20
25
30
35
40
45
50
55
60
65
MPH.
MPH.
MPH.
MPH.
MPH.
MPH.
MPH.
MPH.
MPH.
MPH.
MPH.
Размещение объектов с помощью ключевого слова new
Как было показано в предыдущем примере кода, объекты должны быть размещены
в памяти с использованием ключевого слова new. Если ключевое слово new не указать
и попытаться воспользоваться переменной класса в следующем операторе кода, будет
получена ошибка компиляции. Например, следующий метод M ain () компилироваться
не будет:
188
Часть II. Главные конструкции программирования на C#
static void Main(string [] args)
{
Console.WnteLine ("***** Fun with Class Types *****\n");
// Ошибка1 Забыли использовать new для создания объекта!
Car myCar;
myCar.petName = "Fred";
}
Чтобы корректно создать объект с использованием ключевого слова new, можно оп­
ределить и разместить в памяти объект Саг в одной строке кода:
static void Main(string [] args)
{
Console .WnteLine ("***** Fun with Class Types *****\n");
Car myCar = new Car () ;
myCar.petName = "Fred";
}
В качестве альтернативы, определение и размещение в памяти экземпляра класса
может осуществляться в разных строках кода:
static void Main(string[] args)
{
Console .WnteLine ("***** Fun with Class Types *****\n") ;
Car myCar;
myCar = new Car() ;
myCar.petName = "Fred";
}
Здесь первый оператор кода просто объявляет ссылку на еще не созданный объект
типа Саг. Только после явного присваивания ссылка будет указывать на действитель­
ный объект в памяти.
В любом случае, к этому моменту мы получили простейший тип класса, который
определяет несколько элементов данных и некоторые базовые методы. Чтобы рас­
ширить функциональность текущего класса Саг, необходимо разобраться с ролью
конструкторов.
Понятие конструктора
Учитывая, что объект имеет состояние (представленное значениями его перемен­
ных-членов), программист обычно желает присвоить осмысленные значения полям
объекта перед тем, как работать с ним. В настоящий момент тип Саг требует присваи­
вания значений полям perName и cu г гSpeed. Для текущего примера это не слишком
проблематично, поскольку общедоступных элементов данных всего два. Однако нередко
классы состоят из нескольких десятков полей. Ясно, что было бы нежелательно писать
20 операторов инициализации для всех 20 элементов данных такого класса.
К счастью, в C# поддерживается механизм конструкторов, которые позволяют ус­
танавливать состояние объекта в момент его создания. Конструктор (constructor) — это
специальный метод класса, который вызывается неявно при создании объекта с исполь­
зованием ключевого слова new. Однако в отличие от “нормального” метода, конструктор
никогда не имеет возвращаемого значения (даже void) и всегда именуется идентично
имени класса, который он конструирует.
Роль конструктора по умолчанию
Каждый класс C# снабжается конструктором по умолчанию, который при необходи­
мости может быть переопределен. По определению такой конструктор никогда не при­
Глава 5. Определение инкапсулированных типов классов
189
нимает аргументов. После размещения нового объекта в памяти конструктор по ум ол­
чанию гарантирует установку всех полей в соответствующие стандартные значения
(значениях по умолчанию для типов данных C# описаны в главе 3).
Если вы не удовлетворены такими приеваиваниями по умолчанию, можете переоп­
ределить конструктор по умолчанию в соответствии со своими нуждами. Для иллю ст­
рации модифицируем класс С#, как показано ниже:
class Саг
{
// 'Состояние' объекта Саг.
public string petName;
public int currSpeed;
// Специальный конструктор no умолчанию.
public Ca r ()
{
petName = "Chuck";
currSpeed = 10;
В данном случае мы заставляем объекты Саг начинать свою жизнь под именем
Chuck и скоростью 10 миль в час. При этом создавать объекты со значениями по ум ол­
чанию можно следующим образом:
я
static void Main(string [] args)
{
Console .WnteLine ("***** Fun with Class Types *****\n");
// Вызов конструктора no умолчанию.
Car chuck = new Car() ;
// Печатает "Chuck is going 10 MPH."
chuck.PrintState();
}
Определение специальных конструкторов
Обычно помимо конструкторов по умолчанию в классах определяются дополнитель­
ные конструкторы. При этом пользователь объекта обеспечивается простым и согла­
сованным способом инициализации состояния объекта непосредственно в момент его
создания. Взгляните на следующее изменение класса Саг, который теперь поддержива­
ет целых три конструктора:
class Саг
{
// 'Состояние' объекта Саг.
public string petName;
public int currSpeed;
// Специальный конструктор no умолчанию.
public Car()
{
petName = "Chuck";
currSpeed = 10;
}
// Здесь currSpeed получает значение
//no умолчанию типа in t (О) .
public Car(string pn)
{
petName = pn;
}
190
Часть II. Главные конструкции программирования на C#
// Позволяет вызывающему коду установить полное состояние Саг.
public Car(string рп, int cs)
{
petName = рп;
currSpeed = cs;
}
Имейте в виду, что один конструктор отличается от другого (с точки зрения компи­
лятора С#) количеством и типом аргументов. В главе 4 было показано, что определение
методов с одним и тем же именем, но разным количеством и типами аргументов, назы­
вается перегрузкой. Таким образом, класс Саг имеет перегруженный конструктор, что­
бы предоставить несколько способов создания объекта во время объявления. В любом
случае, теперь можно создавать объекты Саг, используя любой из его общедоступных,
конструкторов. Например:
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Class Types *****\n");
// Создать объект Car no имени Chuck со скоростью 10 миль в час.
Car chuck = new Car() ;
chuck.PnntState () ;
// Создать объект Car no имени Mary со скоростью 0 миль в час.
Саг шагу = new Car ("Магу" ) ;
шагу.PrintState();
// Создать объект Саг по имени Daisy со скоростью 75 миль в час.
Car daisy = new Car ("Daisy", 75);
daisy.PrintState();
Еще раз о конструкторе по умолчанию
Как вы только что узнали, все классы “бесплатно” снабжаются конструктором по
умолчанию. Таким образом, если добавить в текущий проект новый класс по имени
M o to rcycle , определенный следующим образом:
class Motorcycle
{
public void PopAWheelyO
{
Console.WriteLine("Yeeeeeee Haaaaaeewww1");
}
то сразу можно будет создавать экземпляры M o to rc y c le с помощью конструктора по
умолчанию:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
Motorcycle me = new Motorcycle();
m e .PopAWheely();
Однако, как только определен специальный конструктор, конструктор по умолчанию
молча удаляется из класса и становится недоступным! Воспринимайте это так: если вы
не определили специального конструктора, компилятор C# снабжает класс конструктором
по умолчанию, чтобы позволить пользователю объекта размещать его в памяти с набором
Глава 5. Определение инкапсулированных типов классов
191
данных, имеющих значения по умолчанию. В случае же, когда определяется уникальный
конструктор, компилятор предполагает, что вы решили взять власть в свои руки.
Таким образом, чтобы позволить пользователю объекта создавать экземпляр типа
посредством конструктора по умолчанию, а также специального конструктора, пона­
добится явно переопределить конструктор по умолчанию. И, наконец, в подавляющем
большинстве случаев реализация конструктора класса по умолчанию намеренно оста­
ется пустой, поскольку все, что требуется — это создание объекта со значениями всех
полей по умолчанию. Внесем в класс Motorcycle следующие изменения:
class Motorcycle
{
public int driverlntensity;
public void PopAWheelyO
{
for (int 1 = 0 ; l <= driverlntensity; i++)
{
Console .WnteLine ("Yeeeeeee Haaaaaeewww!" ) ;
}
// Вернуть конструктор no умолчанию, который будет устанавливать
// для всех членов данных значения по умолчанию.
public Motorcycle () {}
// Специальный конструктор.
public Motorcycle(int intensity)
{ driverlntensity = intensity; }
}
Роль ключевого слова t h i s
В языке C# имеется ключевое слово this, которое обеспечивает доступ к текуще­
му экземпляру класса. Одно из возможных применений ключевого слова this состоит
в том, чтобы разрешать неоднозначность контекста, которая может возникнуть, когда
входящий параметр назван так же, как поле данных данного типа. Разумеется, в идеа­
ле необходимо просто придерживаться соглашения об именовании, которое не может
привести к такой неоднозначности; однако чтобы проиллюстрировать такое исполь­
зование ключевого слова this, добавим в класс Motorcycle новое поле типа string
(по имени name), представляющее имя водителя. После этого добавим метод по имени
SetDriverNameO, реализованный следующим образом:
class Motorcycle
{
public int driverlntensity;
// Новые члены, представляющие имя водителя.
public string name;
public void SetDriverName(string name)
{ name = name; }
}
Хотя этот код нормально компилируется, Visual Studio 2010 отобразит предупреж­
дающее сообщение о том, что переменная присваивается сама себе! Чтобы проиллюст­
рировать это, добавим в Main() вызов SetDriverNameO и выведем значение поля name.
Обнаружится, что значением поля name осталась пустая строка!
// Усадим на Motorcycle байкера по имени Tiny?
Motorcycle с = new Motorcycle (5);
с .SetDriverName("Tiny");
с .PopAWheely ();
Console.WnteLine ("Rider name is {0}", c.name) ; // Выводит пустое значение name!
192
Часть II. Главные конструкции программирования на C#
Проблема в том, что реализация SetDriverName () выполняет присваивание входя­
щему параметру его же значения, поскольку компилятор предполагает, что name здесь
ссылается на переменную, существующую в контексте метода, а не на поле паше в
контексте класса. Для информирования компилятора, что необходимо установить зна­
чение поля данных текущего объекта, просто используйте t h i s для разрешения этой
неоднозначности:
public void SetDriverName(string name)
{ this.name == name; }
Имейте в виду, что если неоднозначности нет, то вы не обязаны использовать клю­
чевое слово this, когда классу нужно обращаться к собственным данным или членам.
Например, если переименовать член данных паше типа string в driverName (что так­
же потребует обновления метода Main()), то применение this станет не обязательным,
поскольку исчезает неоднозначность контекста:
class Motorcycle
{
public int dnverlntensity;
public string driverName;
public void SetDriverName(string name)
{
// Эти два оператора функционально эквивалентны.
driverName == name;
this .driverName == name;
}
}
Помимо небольшого выигрыша от использования this в неоднозначных ситуаци­
ях, это ключевое слово может быть полезно при реализации членов, поскольку такие
IDE-среды, как SharpDevelop и Visual Studio 2010, включают средство IntelliSense, когда
вводится this. Это может здорово помочь, когда вы забыли название члена класса и
хотите быстро вспомнить его определение. Взгляните на рис. 5.2.
Рис. 5.2 . Активизация средства IntelliSense для this
Глава 5. Определение инкапсулированных типов классов ■
193
На заметку! Применение ключевого слова this внутри реализации статического члена приво­
дит к ошибке компиляции. Как будет показано, статические члены оперируют на уровне класса
(а не объекта), а на этом уровне нет текущего объекта, потому и не существует this!
Построение цепочки вызовов конструкторов
с использованием t h i s
Другое применение ключевого слова this состоит в проектировании класса, исполь­
зующего технику под названием сцепление конструкторов или цепочка конструкто­
ров (constructor chaining). Этот шаблон проектирования полезен, когда имеется класс,
определяющий несколько конструкторов. Учитывая тот факт, что конструкторы часто
проверяют входящие аргументы на соблюдение различных бизнес-правил, возникает
необходимость в избыточной логике проверки достоверности внутри множества конст­
рукторов. Рассмотрим следующее измененное объявление класса Motorcycle:
class Motorcycle
{
public int dnverlntensity;
public string dnverName;
public Motorcycle () { }
// Избыточная логика конструктора!
public Motorcycle (int intensity)
{
if (intensity > 10)
{
intensity = 10;
}
dnverlntensity = intensity;
}
public Motorcycle(int intensity, string name)
{
if (intensity > 10)
{
intensity = 10;
}
dnverlntensity = intensity;
dnverName = name;
Здесь (возможно, стараясь обеспечить безопасность гонщика) в каждом конструк­
торе предпринимается проверка, что уровень мощности не превышает 10. Хотя все это
правильно и хорошо, в двух конструкторах появляется избыточный код. Это далеко от
идеала, поскольку придется менять код в нескольких местах в случае изменения правил
(например, если предельное значение мощности будет установлено равным 5).
Один из способов исправить создавшуюся ситуацию состоит в определении в классе
Motorcycle метода, который выполнит проверку входных аргументов. Если поступить
так, то каждый конструктор должен будет вызывать этот метод перед присваиванием
значений полям. Хотя такой подход позволяет изолировать код, который придется об­
новлять при изменении бизнес-правил, теперь появилась другая избыточность:
class Motorcycle
{
public int dnverlntensity;
public string dnverName;
194
Часть II. Главные конструкции программирования на C#
// Конструкторы.
public Motorcycle () { }
public Motorcycle(int intensity)
{
Setlntensity(intensity);
}
public Motorcycle(int intensity, string name)
{
Setlntensity(intensity);
dnverName = name;
}
public void Setlntensity(int intensity)
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
Более ясный подход предусматривает назначение конструктора, который принима­
ет максимальное количество аргументов, в качестве “ведущего конструктора”, с реа­
лизацией внутри него необходимой логики проверки достоверности. Остальные конст­
рукторы смогут использовать ключевое слово t h is , чтобы передать входные аргументы
ведущему конструктору и при необходимости предоставить любые дополнительные па­
раметры. В результате беспокоиться придется только о поддержке единственного конст­
руктора для всего класса, в то время как остальные конструкторы остаются в основном
пустыми.
Ниже приведена финальная реализация класса M o to rc y c le (с дополнительным кон­
структором в целях иллюстрации). При связывании конструкторов в цепочку обратите
внимание, что ключевое слово t h i s располагается вне тела конструктора (и отделяется
от его имени двоеточием):
class Motorcycle
{
public int driverlntensity;
public string dnverName;
// Связывание конструкторов в цепочку.
public Motorcycle () {}
public Motorcycle (int intensity)
: this(intensity, "") {}
public Motorcycle(string name)
: this(0, name) {}
// Это 'ведущий конструктор' , выполняющий всю реальную работу.
public Motorcycle(int intensity, string name)
{
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
driverName = name;
Глава 5. Определение инкапсулированных типов классов
195
Имейте в виду, что применение ключевого слова this для связывания вызовов
конструкторов в цепочку вовсе не обязательно. Однако использование такой техники
позволяет получить лучше сопровождаемое и более краткое определение кода. С помо­
щью этой техники также можно упростить решение программистских задач, поскольку
реальная работа'делегируется единственному конструктору (обычно имеющему мак­
симальное количество параметров), в то время как остальные просто передают ему
ответственность.
Обзор потока конструктора
Напоследок отметим, что как только конструктор передал аргументы выделенному
ведущему конструктору (и этот конструктор обработал данные), вызывающий конструк­
тор продолжает выполнение всех остальных операторов. Чтобы прояснить мысль, моди­
фицируем конструкторы класса Motorcycle, добавив вызов Console.WriteLine():
class Motorcycle
{
public int dnverlntensity;
public string driverName;
// Связывание конструкторов в цепочку.
public Motorcycle()
{
Console .WnteLine ("In default ctor") ;
}
public Motorcycle (int intensity) : this(intensity, "")
{
Console .WnteLine (" In ctor taking an int");
}
public Motorcycle(string name) : this(0, name)
{
Console .WnteLine ("In ctor taking a string");
}
// Это 'ведущий конструктор' , выполняющий всю реальную работу.
public Motorcycle(int intensity, string name)
{
Console.WriteLine("In master ctor ");
if (intensity > 10)
{
intensity = 10;
}
driverlntensity = intensity;
driverName = name;
Теперь изменим метод M ain(), чтобы он обрабатывал объект M o torcycle , как пока­
зано ниже:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Class Types *****\n");
// Создание Motorcycle.
Motorcycle c = new Motorcycle(5);
c .SetDriverName("Tiny");
c .PopAWheely();
Console.WriteLine("Rider name is {0}", c .driverName); // вывод имени гонщика
Console.ReadLine();
196
Часть II. Главные конструкции программирования на C#
Вывод, полученный в результате выполнения предыдущего метода M ain(), выглядит
следующим образом:
***** Fun with Class Types *****
In master ctor
In ctor taking an int
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww!
Rider name is Tiny
Поток логики конструкторов описан ниже.
• Создается объект за счет вызова конструктора, который принимает один аргу­
мент типа in t.
• Конструктор передает полученные данные ведущему конструктору, добавляя не­
обходимые дополнительные начальные аргументы, не указанные вызывающим
кодом.
• Ведущий конструктор присваивает входные данные полям объекта.
• Управление возвращается первоначально вызванному конструктору, который вы­
полняет остальные операторы кода.
В построении цепочки конструкторов замечательно то, что этот шаблон программи­
рования работает с любой версией языка C# и платформой .NET. Однако если в случае
если целевой платформой является .NET 4.0 и выше, можно еще более упростить задачу
программирования за счет использования необязательных аргументов в качестве аль­
тернативы традиционным цепочкам конструкторов.
Еще раз об необязательных аргументах
Вы главе 4 вы узнали об необязательных и именованных аргументах. Вспомните, что
необязательные аргументы позволяют определять значения по умолчанию для входных
аргументов. Если вызывающий код удовлетворяют эти значения по умолчанию, указы­
вать уникальные значения не обязательно, но это можно делать, когда объект требуется
снабдить специальными данными. Рассмотрим следующую версию класса M otorcycle,
который теперь предоставляет несколько возможностей конструирования объектов, ис­
пользуя единственное определение конструктора.
class Motorcycle
{
// Единственный конструктор, использующий необязательные аргументы.
public Motorcycle(int intensity = 0, string name = "")
{
if (intensity > 10)
{
intensity = 10;
}
dnverlntensity = intensity;
dnverName = name;
Глава 5. Определение инкапсулированных типов классов
197
С этим единственным конструктором можно создать объект Motorcycle, используя
ноль, один или два аргумента. Вспомните, что синтаксис именованных аргументов по­
зволяет, по сути, пропускать приемлемые установки по умолчанию (см. главу 4).
static void MakeSomeBikes()
{
// driverName = "" , d riv e rln te n sity = 0
Motorcycle ml = new Motorcycle();
Console .WnteLine ("Name= {0}, Intensity= {1}",
m l .driverName, m l .driverlntensity);
// driverName = "Tiny" , d riv e rln te n sity = 0
Motorcycle m2 = new Motorcycle(name:"Tiny");
Console .WnteLine ("Name= {0}, Intensity= {1}",
m2.driverName, m2.driverlntensity);
// driverName = "" , d riv e rln te n sity = 7
Motorcycle m3 = new Motorcycle(7);
Console .WnteLine ("Name= {0}, Intensity= {1}",
m 3 .driverName, m 3 .driverlntensity);
}
Хотя применение необязательных/именованных аргументов — очень удобный путь
упрощения определения набора конструкторов, используемых определенным классом,
следует всегда помнить, что этот синтаксис привязывает компиляцию приложений к
C# 2010, а их выполнение — к .NET 4.0. Если требуется построить классы, которые
должны выполняться на платформе .NET любой версии, лучше придерживаться клас­
сической технологии цепочек конструкторов.
В любом случае, теперь можно определить класс с данными полей (переменнымичленами) и различными членами, которые могут быть созданы с помощью любого коли­
чества конструкторов. Теперь давайте формализуем роль ключевого слова s t a t i c .
Исходный код. Проект SimpleClassExample доступен в подкаталоге Chapter 5.
Понятие ключевого слова s t a t i c
Класс C# может определять любое количество статических членов с использовани­
ем ключевого слова static. При этом соответствующий член должен вызываться не­
посредственно на уровне класса, а не на объектной ссылке. Чтобы проиллюстрировать
разницу, обратимся к System.Console. Как уже было показано, метод W n t e L i n e ()
не вызывается на уровне объекта:
// Ошибка! W riteLin e() не является методом уровня объекта1
Console с = new Console ();
с .WriteLine("I can't be printed...");
Вместо этого статический член WriteLine () предваряется именем класса:
// Правильно! W riteLin e() - статический метод.
Console .WnteLine ("Thanks ...");
Проще говоря, статические члены — это элементы, задуманные (проектировщиком
класса) как общие, так что нет нужды создавать экземпляр типа при их вызове. Когда
в любом классе определены только статические члены, такой класс можно считать “об­
служивающим классом”. Например, если воспользоваться браузером объектов Visual
Studio 2 0 1 0 (выбрав пункт меню V ie w ^ O b je c t B ro w se r (Вид1
^Браузер объектов)) для про­
смотра пространства имен System сборки mscorlib.dll, можно увидеть, что все члены
классов Console, Math, Environment и GC представляют свою функциональность через
статические члены.
198
Часть II. Главные конструкции программирования на C#
Определение статических методов
Предположим, что имеется новый проект консольного приложения по имени
StaticMethods, и в нем — класс по имени Teenager, определяющий статический метод
Complain (). Этот метод возвращает случайную строку, полученную вызовом статиче­
ской вспомогательной функции по имени GetRandomNumber ():
class Teenager
{
public static Random r = new Random();
public static int GetRandomNumber(short upperLimit)
{
return r .Next(upperLimit);
}
public static string Complain ()
{
string[] messages = {"Do I have to?", "He started it!",
"I'm too tired...", "I hate school1",
"You are sooooooo wrong!"};
return messages[GetRandomNumber(5)];
}
}
Обратите внимание, что переменная-член System.Random и вспомогательная
функция GetRandomNumber () также были объявлены статическими членами класса
Teenager, учитывая правило, что статические члены, такие как метод Complain(), мо­
гут оперировать только другими статическими членами.
На заметку! Здесь следует повторить: статические члены могут оперировать только статическими
данными и вызывать статические методы определяющего их класса. Попытка использования
нестатических данных класса или вызова нестатического метода класса внутри реализации
статического члена приводит к ошибке времени компиляции.
Подобно любому статическому члену, для вызова Complain () нужен префикс — имя
определяющего его класса:
static void Main(string [] args)
{
Console.WnteLine ("***** Fun with Class Types *****\n");
for(int l =0; l < 5; i++)
Console.WnteLine (Teenager .Complain () ) ;
Console.ReadLine();
Исходный код. Проект StaticMethods доступен в подкаталоге Chapter 5.
Определение статических полей данных
В дополнение к статическим методам, в классе (или структуре) также могут быть
определены статические поля, такие как переменная-член Random, представленная в
предыдущем классе Teenager. Знайте, что когда класс определяет нестатические дан­
ные (правильно называемые данными экземпляра), то каждый объект этого типа под­
держивает независимую копию поля. Например, представим класс, который моделиру­
ет депозитный счет, определенный в новом проекте консольного приложения по имени
StaticData:
Глава 5. Определение инкапсулированных типов классов
199
/ / П р остой к л а с с д е п о з и т н о г о с ч е т а .
class SavingsAccount
{
public double currBalance;
public SavingsAccount(double balance)
{
currBalance = balance;
1
}
}
При создании объектов SavingsAccount память под поле currBalance выделяется
для каждого объекта. Статические данные, с другой стороны, распределяются однажды
и разделяются всеми объектами того же класса. Чтобы проиллюстрировать удобство
статических данных, добавьте в класс SavingsAccount статический элемент данных по
имени currlnterestRate, принимающий значение по умолчанию 0.04:
/ / П р остой к л а с с д е п о з и т н о г о с ч е т а .
class SavingsAccount
{ 1
public double currBalance;
//
С тати ч еск и й эл ем ен т данны х.
public static double currlnterestRate = 0.04;
public SavingsAccount(double balance)
{
currBalance = balance;
Если создать три экземпляра SavingsAccount, как показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Static Data *****\n ");
SavingsAccount si = new SavingsAccount (50);
SavingsAccount s2 = new SavingsAccount(100);
SavingsAccount s3 = new SavingsAccount(10000.75);
Console.ReadLine();
}
то размещение данных в памяти будет выглядеть примерно так, как показано на рис. 5.3.
Рис. 5.3 . Статические данные размещаются один раз и разделяются
между всеми экземплярами класса
Теперь давайте изменим класс SavingsAccount, добавив к нему два статических ме­
тода для получения и установки значения процентной ставки:
200
Часть II. Главные конструкции программирования на C#
// Простой класс депозитного счета.
class SavingsAccount
{
public double currBalance;
// Статический элемент данных.
public static double currInterestRate = 0.04;
public SavingsAccount(double balance)
{
currBalance = balance;
}
// Статические члены для установки/получения процентной ставки.
public static void SetInterestRate(double newRate )
{ currlnterestRate = newRate; }
public static double GetInterestRate ()
{ return currlnterestRate; }
}
Рассмотрим следующее применение класса:
static void Main(string [] args)
{
Console.WnteLine (''***** Fun with Static Data *****\n");
SavingsAccount si = new SavingsAccount(50);
SavingsAccount s2 = new SavingsAccount(100);
// Вывести текущую процентную ставку.
Console .WnteLine ("Interest Rate is: {0}", SavingsAccount.Get InterestRate ()) ;
// Создать новый объект; это не 'сбросит' процентную ставку.
SavingsAccount s3 = new SavingsAccount(10000.75);
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate()) ;
Console.ReadLine();
}
Вывод предыдущего метода Main() показан ниже:
***** Fun W1th Static Data *****
In static ctor1
Interest Rate is: 0.04
Interest Rate is: 0.04
Как видите, при создании новых экземпляров класса SavingsAccount значение
статических данных не сбрасывается, поскольку CLR выделяет для них место в памяти
только один раз. После этого все объекты типа SavingsAccount оперируют одним и тем
же значением.
При проектировании любого класса C# одна из задач связана с выяснением того,
какие части данных должны быть определены как статические члены, а какие — нет.
Хотя на этот счет не существует строгих правил, помните, что поле статических данных
разделяется между всеми объектами данного класса. Поэтому, если необходимо, чтобы
часть данных совместно использовалась всеми объектами, статические члены будут са­
мым подходящим вариантом.
Предположим, что переменная currlnterestRate не определена с ключевым сло­
вом static. Это означает, что каждый объект SavingAccount имеет собственную
копию currlnterestRate. Пусть создано 100 объектов SavingAccount, и требует­
ся изменить значение процентной ставки. Это потребует стократного вызова методв
SetInterestRate()l Ясно, что такой способ моделирования общих для объектов класса
данных нельзя считать удобным. Еще раз: статические данные идеальны, когда имеет­
ся значение, которое должно быть общим для всех объектов данной категории.
Глава 5. Определение инкапсулированных типов классов
201
Определение статических конструкторов
»
Вспомните, что конструкторы служат для установки значений поля данных объекта
во время его создания. Таким образом, если вы хотите присвоить значение статическо­
му члену внутри конструктора уровня экземпляра, то удивитесь, обнаружив, что это
значение будет сбрасываться каждый раз, когда создается новый объект! Например,
предположим, что класс SavingsA ccou nt изменен следующим образом:
class SavingsAccount
{
public double currBalance;
public static double currlnterestRate;
public SavingsAccount(double balance)
{
currlnterestR ate = 0.04;
currBalance = balance;
}
При выполнении предыдущего метода M a in () обнаруживается, что перемен­
ная c u r r l n t e r e s t R a t e будет сбрасываться при каждом создании нового объекта
S avingsAccount, всегда возвращаясь к значению 0.04. Ясно, что установка значений
статических данных в нормальном конструкторе экземпляра сводит на нет весь их
смысл. Всякий раз, когда создается новый объект, данные уровня класса сбрасывают­
ся! Один из способов правильной установки статического поля состоит в использовании
синтаксиса инициализации члена, как это делалось изначально:
class SavingsAccount
{
public double currBalance;
// Статические данные.
public static double currlnterestRate = 0.04;
Этот подход гарантирует, что статическое поле будет установлено только однажды,
независимо от того, сколько объектов будет создано. Однако что, если значение стати­
ческих данных нужно получить во время выполнения? Например, в типичном банков­
ском приложении значение переменной — процентной ставки должно быть прочитано
из базы данных или внешнего файла. Решение подобных задач требует контекста мето­
да (такого как конструктор), чтобы можно было выполнить операторы кода.
Именно по этой причине в C# предусмотрена возможность определения статическо­
го конструктора. Взгляните на следующее изменение в коде:
class SavingsAccount
{
public double currBalance;
public static double currlnterestRate;
public SavingsAccount(double balance)
{
currBalance = balance;
}
// Статический конструктор.
static SavingsAccount ()
{
Console.WriteLine("In static ctor'");
currlnterestRate = 0.04;
202
Часть II. Главные конструкции программирования на C#
Упрощенно, статический конструктор — это специальный конструктор, который яв­
ляется идеальным местом для инициализации значений статических данных, когда их
значение не известно на момент компиляции (например, когда его нужно прочитать из
внешнего файла или сгенерировать случайное число). Ниже приведено несколько инте­
ресных моментов, касающихся статических конструкторов.
• В отдельном классе может быть определен только один статический конструктор.
Другими словами, статический конструктор нельзя перегружать.
• Статический конструктор не имеет модификатора доступа и не может принимать
параметров.
• Статический конструктор выполняется только один раз, независимо от того,
сколько объектов отдельного класса создается.
• Исполняющая система вызывает статический конструктор, когда создает экземп­
ляр класса или перед первым обращением к статическому члену этого класса.
• Статический конструктор выполняется перед любым конструктором уровня
экземпляра.
Учитывая сказанное, при создании новых объектов S avings Ac count значения стати­
ческих данных сохраняются, поскольку статический член устанавливается только один
раз внутри статического конструктора, независимо от количества созданных объектов.
Определение статических классов
Ключевое слово s t a t i c возможно также применять прямо на уровне класса. Когда
класс определен как статический, его нельзя создать с использованием ключевого слова
new, и он может включать в себя только статические члены или поля. Если это правило
нарушить, возникнет ошибка компиляции.
На заметку! Классы или структуры, предоставляющие только статическую функциональность, час­
то называют служебными (utility). При проектировании такого класса рекомендуется применять
ключевое слово s t a t i c к определению класса.
На первый взгляд это может показаться довольно бесполезным средством, учитывая
невозможность создания экземпляров класса. Однако следует учесть, что класс, не со­
держащий ничего кроме статических членов и/или константных данных, прежде всего,
не нуждается в выделении памяти. Рассмотрим следующий новый статический тип:
// Статические классы могут содержать только статические члены!
static class TimeUtilClass
{
public static void PrintTime()
{ Console.WriteLine(DateTime.Now.ToShortTimeString()); }
public static void PrintDate ()
{ Console.WriteLine (DateTime.Today.ToShortDateString()); }
}
Учитывая, что этот класс определен с ключевым словом s t a t ic , создавать экземп­
ляры T im e U tilC la s s с помощью ключевого слова new нельзя. Напротив, вся функцио­
нальность доступна на уровне класса:
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Static Data *****\n ");
// Это работает нормально.
TimeUtilClass.PrintDate();
TimeUtilClass.PrintTime();
Глава 5. Определение инкапсулированных типов классов
203
// Ошибка компиляции! Создавать экземпляр статического класса нельзя!
TimeUtilClass u = new TimeUtilClass ();
До появлении статических классов в .NET 2.0 единственный способ предотвратить
создание экземпляров класса, предлагающего только статическую функциональность,
состоял либо в переопределении конструктора по умолчанию с модификатором p r iv a te ,
либо в объявлении класса как абстрактного с использованием ключевого слова a b s t r a c t
(подробности об абстрактных типах ищите в главе 6). Рассмотрим следующие подходы:
class TimeUtilClass2
{
// Переопределение конструктора по умолчанию как
// приватного для предотвращения создания экземпляров.
private TimeUtilClass2 (){}
public static void PrintTimeO
{ Console.WriteLine (DateTime.Now.ToShortTimeString()); }
public static void PrintDateO
{ Console.WriteLine(DateTime.Today.ToShortDateString()); }
}
// Определение типа как абстрактного для предотвращения создания экземпляров.
abstract class TimeUtilClass3
{
public static void PrintTimeO
{ Console.WriteLine(DateTime.Now.ToShortTimeString()); }
public static void PrintDateO
{ Console.WriteLine(DateTime.Today.ToShortDateString()); }
}
Хотя эти конструкции допустимы и сейчас, использование статических классов
представляет собой более ясное и более безопасное в отношении типов решение, учи ­
тывая тот факт, что предыдущие два приема допускают объявление нестатических ч ле­
нов внутри класса без ошибок. В результате возникнет большая проблема! При наличии
класса, который больше не допускает создания экземпляров, в распоряжении окажется
фрагмент функциональности (т.е. все нестатические члены), который не может быть
использован. К этому моменту должно быть ясно, как определять простые классы,
включающие конструкторы, поля и различные статические (и нестатические) члены.
Обладая такими базовыми знаниями, можем приступать к ознакомлению с тремя ос­
новными принципами объектно-ориентированного программирования.
Исходный код. Проект S ta tic D a ta доступен в подкаталоге C hapter 5.
Основы объектно-ориентированного
программирования
Все основанные на объектах языки (С#, Java, C++, Smalltalk, Visual Basic и in .)
должны отвечать трем основным принципам объектно-ориентированного программи­
рования (ООП), которые перечислены ниже.
• Инкапсуляция. Как данный язык скрывает детали внутренней реализации объек­
тов и предохраняет целостность данных?
• Наследование. Как данный язык стимулирует многократное использование кода?
• Полиморфизм. Как данный язык позволяет трактовать связанные объекты сход­
ным образом?
204
Часть II. Главные конструкции программирования на C#
Прежде чем погрузиться в синтаксические детали реализации каждого принципа,
важно понять базовую роль каждого из них. Ниже предлагается обзор каждого принци­
па, а в остальной части этой и последующих главах рассматриваются детали.
Роль инкапсуляции
Первый основной принцип ООП называется инкапсуляцией. Этот принцип касает­
ся способности языка скрывать излишние детали реализации от пользователя объекта.
Например, предположим, что используется класс по имени DatabaseReader, который
имеет два главных метода: Ореп() и close().
// Этот класс инкапсулирует детали открытия и закрытия базы данных.
DatabaseReader dbReader = new DatabaseReader ();
dbReader.Open(0"C:\AutoLot.mdf");
// Сделать что-то с файлом данных и закрыть файл.
dbReader.Close() ;
Фиктивный класс DatabaseReader инкапсулирует внутренние детали нахождения,
загрузки, манипуляций и закрытия файла данных. Программистам нравится инкапсу­
ляция, поскольку этот принцип ООП упрощает кодирование. Нет необходимости бес­
покоиться о многочисленных строках кода, которые работают “за кулисами”, чтобы
реализовать функционирование класса DatabaseReader. Все, что потребуется — это
создать экземпляр и отправлять ему соответствующие сообщения (например, “открыть
файл по имени AutoLot.mdf, расположенный на диске С:”).
С идеей инкапсуляции программной логики тесно связана идея защиты данных.
В идеале данные состояния объекта должны быть специфицированы с использовани­
ем ключевого слова private (или, возможно, protected). Таким образом, внешний мир
должен вежливо попросить, если захочет изменить или получить лежащее в основе зна­
чение. Это хороший принцип, поскольку общедоступные элементы данных можно лег­
ко повредить (даже нечаянно, а не преднамеренно). Чуть позже будет дано формальное
определение этого аспекта инкапсуляции.
Роль наследования
Следующий принцип ООП — наследование — касается способности языка позволять
строить новые определения классов на основе определений существующих классов. По
сути, наследование позволяет расширять поведение базового (или родительского) клас­
са, наследуя основную функциональность в производном подклассе (также именуемом
дочерним классом). На рис. 5.4 показан простой пример.
Прочесть диаграмму на рис. 5.4 можно так: “шестиугольник является фигурой, кото­
рая является объектом”. При наличии классов, связанных этой формой наследования,
между типами устанавливается отношение “является” (“is-a”). Такое отношение называ­
ют классическим наследованием.
Здесь можно предположить, что Shape определяет некоторое количество членов, об­
щих для всех наследников (скажем, значение для представления цвета фигуры и другие
значения, задающие высоту и ширину). Учитывая, что класс Hexagon расширяет Shape,
он наследует основную функциональность, определенную классами Shape и Object, а
также определяет дополнительные собственные детали, касающиеся шестиугольников
(какими бы они ни были).
На заметку! На платформе .NET класс System.Object всегда находится на вершине любой иерар­
хии классов и определяет базовую функциональность, которая подробно описана в главе 6.
Глава 5. Определение инкапсулированных типов классов
205
Рис. 5.4 . Отношение “является" (“is-a")
Есть и другая форма повторного использования кода в мире ООП: модель включе­
ния/делегации, также известная под названием о т н о ш е н и е “и м е е т " ( “h a s - а " ) или а г р е ­
гация. Эта форма повторного использования н е применяется для установки отношений
“родительский-дочерний”.Вместо этого такое отношение позволяет одному классу оп­
ределять переменную-член другого класса и опосредованно представлять его функцио­
нальность (при необходимости) пользователю объекта.
Например, предположим, что снова моделируется автомобиль. Может понадобиться
выразить идею, что автомобиль “имеет” радиоприемник. Было бы нелогично пытаться
наследовать класс Саг от Radio или наоборот (ведь Саг не “является” Radio). Взамен
имеются два независимых класса, работающих совместно, причем класс Саг создает и
представляет функциональность Radio:
class Radio
{
public void Power(bool turnOn)
{
Console .WnteLine ("Radio on: {0}", turnOn);
}
}
class Car
{
// Car 'имеет' Radio.
private Radio myRadio = new Radio();
public void TurnOnRadio(bool onOff)
{
// Делегированный вызов внутреннего объекта.
myRadio.Power(onOff);
}
}
Обратите внимание, что пользователь объекта не имеет понятия, что класс Саг ис­
пользует внутренний объект Radio.
static void Main(string[] args)
{
// Вызов передается Radio внутренне.
Car viper = new Car () ;
viper.TurnOnRadio(false);
}
206
Часть II. Главные конструкции программирования на C#
Роль полиморфизма
Последний принцип ООП — полиморфизм. Он обозначает способность языка трак­
товать связанные объекты в сходной манере. В частности, этот принцип ООП позво­
ляет базовому классу определять набор членов (формально называемый полиморфным
интерфейсом), которые доступны всем наследникам. Полиморфный интерфейс класса
конструируется с использованием любого количества виртуальных или абстрактных
членов (подробности ищите в главе 6).
По сути, виртуальный член— это член базового класса, определяющий реализацию
по умолчанию, которая может быть изменена (или, говоря более формально, переоп­
ределена) в производном классе. В отличие от него, абстрактный метод — это член
базового класса, который не предусматривает реализации по умолчанию, а предла­
гает только сигнатуру. Когда класс наследуется от базового класса, определяющего аб­
страктный метод, этот метод обязательно должен быть переопределен в производном
классе. В любом случае, когда производные классы переопределяют члены, определен­
ные в базовом классе, они по существу переопределяют свою реакцию на один и тот же
запрос.
Чтобы увидеть полиморфизм в действии, давайте представим некоторые детали
иерархии фигур, показанной на рис. 5.4. Предположим, что в классе Shape определен
виртуальный метод Draw(), не принимающий параметров. Учитывая тот факт, что ка­
ждая фигура должна визуализировать себя уникальным образом, подклассы (такие как
Hexagon и Circle) вольны переопределить этот метод по своему усмотрению (рис. 5.5).
1
©
Object
T
Shape
Class
Вызов Draw () на объекте
Circle приводит к рисованию
I C irc le
Class
J—
* Shape
двумерного круга.
3
= M e th o d s
♦
D raw
!
Hexagon
Class
Shape
~® ]
Вызов Draw () на объекте
Hexagon приводит к рисованию
двумерного шестиугольника.
Рис. 5.5 . Классический полиморфизм
Когда полиморфный интерфейс спроектирован, можно сделать ряд предположений в
коде. Например, учитывая, что классы Hexagon и Circle унаследованы от общего роди­
теля (Shape), массив типов Shape может содержать всех наследников базового класса.
Более того, учитывая, что Shape определяет полиморфный интерфейс для всех произ­
водных типов (в данном примере — метод Draw О), можно предположить, что каждый
член массива обладает этой функциональностью.
Рассмотрим следующий метод Main(), который заставляет массив типов-наследников Shape визуализировать себя с использованием метода Draw():
class Program
{
static void Main(string[] args)
{
Shape [] myShapes = new Shape [3];
myShapes[0] = new Hexagon ();
myShapes[1] = new Circle ();
myShapes [2] = new Hexagon ();
Глава 5. Определение инкапсулированных типов классов
207
foreach (Shape s in myShapes)
{
// Используется полиморфный интерфейс!
s .Draw();
}
Console.ReadLine ();
}
}
На этом краткое знакомство с основными принципами ООП завершено. Оставшаяся
часть главы посвящена дальнейшим подробностям инкапсуляции в С#. Детали наследо­
вания и полиморфизма рассматриваются в главе 6.
Модификаторы доступа C#
При работе с инкапсуляцией всегда следует принимать во внимание то, какие ас­
пекты типа видимы различным частям приложения. В частности, типы (классы, ин­
терфейсы, структуры, перечисления и делегаты), а также их члены (свойства, методы,
конструкторы и поля) определяются с использованием определенного ключевого слова,
управляющего “видимостью” элемента другим частям приложения. Хотя в C# опреде­
лены многочисленные ключевые слова для управления доступом, их значение может
отличаться в зависимости от места применения (к типу или члену). В табл. 5.1 описаны
роли и применение модификаторов доступа.
Таблица 5.1. Модификаторы доступа C#
Модификатор
доступа
К чему может быть применен
Назначение
public
Типы или члены типов
Общедоступные (public) элемен­
ты не имеют ограничений доступа.
Общедоступный член может быть доступен
как из объекта, так и из любого производ­
ного класса. Общедоступный тип может
быть доступен из других внешних сборок
private
Члены типов или вложенные типы
Приватные (private) элементы могут
быть доступны только в классе (или струк­
туре), в котором они определены
protected
Члены типов или вложенные типы
Защищенные (protected) элементы могут
использоваться классом, который определил
их, и любым дочерним классом. Однако за­
щищенные элементы не доступны внешнему
миру через операцию точки (.)
internal
Типы или члены типов
Внутренние (internal) элементы дос­
тупны только в пределах текущей сборки.
Таким образом, если в библиотеке клас­
сов .NET определен набор внутренних
типов, то другие сборки не смогут ими
пользоваться
protected
internal
Члены типов или вложенные типы
Когда ключевые слова protected и
internal комбинируются в объявлении
элемента, такой элемент доступен внутри
определяющей его сборки, определяюще­
го класса и всех его наследников
208
Часть II. Главные конструкции программирования на C#
В этой главе рассматриваются только ключевые слова public и private. В после­
дующих главах будет рассказываться о роли модификаторов internal и protected
internal (удобных при построении библиотек кода .NETT) и модификатора protected
(удобного при создании иерархий классов).
Модификаторы доступа по умолчанию
По умолчанию члены типов являются неявно приватными (private) и неявно внут­
ренними (internal). Таким образом, следующее определение класса автоматически ус­
тановлено как internal, в то время как конструктор по умолчанию этого типа автома­
тически является private:
// Внутренний класс с приватным конструктором по умолчанию.
class Radio
{
Radio() {}
}
Чтобы позволить другим частям программы обращаться к членам объекта, эти чле­
ны потребуется пометить как общедоступные (public). К тому же, если необходимо от­
крыть Radio внешним сборкам (опять-таки, это удобно при построении библиотек кода
.NET; см. главу 14), следует добавить к нему модификатор public.
// Общедоступный класс с приватным конструктором по умолчанию.
public class Radio
{
Radio() {}
Модификаторы доступа и вложенные типы
Как было показано в табл. 5.1, модификаторы доступа private, protected и
protected internal могут применяться к вложенному типу. Вложение типов детально
рассматривается в главе 6. Пока же достаточно знать, что вложенный (nested) тип — это
тип, объявленный непосредственно внутри объявления класса или структуры. Для при­
мера ниже приведено приватное перечисление (по имени Color), вложенное в общедос­
тупный класс (по имени SportsCar):
public class SportsCar
{
// Нормально! Вложенные типы могут быть помечены как private,
private enum CarColor
{
Red, Green, Blue
Здесь допускается применять модификатор доступа private к вложенному типу.
Однако не вложенные типы (вроде SportsCar) могут определяться только с модифика­
торами public или internal. Поэтому следующее определение класса неверно:
// Ошибка1 Не вложенный тип не может быть помечен как private1
private class SportsCar
{}
После ознакомления с модификаторами доступа можно приступать к формальным
исследованиям первого принципа ООП.
Глава 5. Определение инкапсулированных типов классов
209
Первый принцип: службы инкапсуляции C#
Концепция инкапсуляции вращается вокруг принципа, гласящего, что внутренние
данные объекта.не должны быть напрямую доступны через экземпляр объекта. Вместо
этого, если вызывающий код желает изменить состояние объекта, то должен делать это
через методы доступа (accessor, или метод get) и изменения (mutator, или метод set). В C#
инкапсуляция обеспечивается на синтаксическом уровне с использованием ключевых
слов public, private, internal и protected. Чтобы проиллюстрировать необходимость
в службах инкапсуляции, предположим, что создано следующее определение класса:
// Класс с единственным общедоступным полем.
class Book
{
public int numberOfPages;
}
Проблема с общедоступными данными состоит в том, что сами по себе эти данные
не имеют возможности “понять”, является ли присваиваемое значение допустимым в
рамках существующих бизнес-правил системы. Как известно, верхний предел значе­
ний для типа int в C# довольно велик (2 147 483 647), поэтому компилятор разрешит
следующее присваивание:
//Хм Ничего себе — мини-новелла!
static void Main(string [] args)
{
Book miniNovel = new Book();
miniNovel.numberOfPages = 30000000;
}
Хотя границы типа данных int не превышены, ясно, что мини-новелла на 30 мил­
лионов страниц выглядит несколько неправдоподобно. Как видите, общедоступные
поля не дают возможности перехватывать ошибки, связанные с преодолением верхних
(или нижних) логических границ. Если в текущей системе установлено бизнес-прави­
ло, гласящее, что книга должна иметь от 1 до 1000 страниц, его придется обеспечить
программно. По этой причине общедоступным полям обычно нет места в определении
класса производственного уровня.
На заметку! Говоря точнее, члены класса, представляющие состояние объекта, не должны поме­
чаться как public. В то же время, как будет показано далее в главе, вполне допускается иметь
общедоступные константы и поля только для чтения.
Инкапсуляция предоставляет способ предохранения целостности данных о состоя­
нии объекта. Вместо определения общедоступных полей (которые легко приводят к
повреждению данных), необходимо выработать привычку определения приватных дан­
ных, управление которыми осуществляется опосредованно, с применением одной из
двух техник:
• определение пары методов доступа и изменения;
• определение свойства .NET.
Какая бы техника не была выбрана, идея состоит в том, что хорошо инкапсулиро­
ванный класс должен защищать свои данные и скрывать подробности своего устрой­
ства от любопытных глаз из внешнего мира. Это часто называют программированием
черного ящика. Преимущество такого подхода состоит в том, что объект может свобод­
но изменять внутреннюю реализацию любого метода. За счет обеспечения неизменно­
сти сигнатуры метода, работа существующего кода, который использует этот метод, не
нарушается.
210
Часть II. Главные конструкции программирования на C#
Инкапсуляция с использованием традиционных
методов доступа и изменения
В оставшейся части этой главы будет построен довольно полный класс, модели­
рующий обычного сотрудника. Для начала создадим новое консольное приложение по
имени EmployeeApp и добавим в него новый файл класса (под названием Employee.cs),
используя пункт меню P r o je c t^ A d d c la ss (Проект1
^Добавить класс). Дополним класс
Employee следующими полями, методами и конструкторами:
class Employee
{
// Поля данных.
private string empName;
private int empID;
private float currPay;
// Конструкторы.
public Employee () { }.
public Employee(string name, int id, float pay)
{
empName = name;
empID = id;
currPay = pay;
}
// Методы.
public void GiveBonus(float amount)
{
currPay += amount;
}
public void DisplayStats ()
{
Console.WriteLine("Name: {0}", empName);
Console .WnteLine (" ID : {0}", empID);
Console.WriteLine("Pay: {0}", currPay); (
}
}
Обратите внимание, что поля класса Employee определены с ключевым словом
private. С учетом этого, поля empName, empID и currPay напрямую через объектную
переменную не доступны:
static void Main(string [] args)
{
// Ошибка! Невозможно напрямую обращаться к приватным полям объекта!
Employee emp = new Employee ();
emp.empName = "Marv";
}
Если необходимо, чтобы внешний мир мог взаимодействовать с полным именем со­
трудника, по традиции понадобится определить методы доступа (метод get) и изменения
(метод set). Роль метода get состоит в возврате вызывающему коду значения лежащих в ос­
нове статических данных. Метод set позволяет вызывающему коду изменять текущее зна­
чение лежащих в основе статических данных при условии соблюдения бизнес-правил.
Для целей иллюстрации инкапсулируем поле empName. Для этого к существующему
классу Employee следует добавить показанные ниже общедоступные члены. Обратите
внимание, что метод SetNameO выполняет проверку входящих данных, чтобы удосто­
вериться, что строка имеет длину не более 15 символов. Если это не так, на консоль
выводится сообщение об ошибке и происходит возврат без изменения значения поля
empName.
Глава 5. Определение инкапсулированных типов классов
211
На заметку! В классе производственного уровня в логике конструктора следовало бы предусмот­
реть проверку длины строки с именем сотрудника. Пока опустим эту деталь, но улучшим код
позже, при рассмотрении синтаксиса свойств .NET.
class Employee
{
// Поля данных.
private string empName;
// Метод доступа (метод get) .
public string GetName()
{
return empName;
}
// Метод изменения (метод set) .
public void SetName(string name)
{
// Перед присваиванием проверить входное значение,
if (name.Length > 15)
Console.WriteLine ("Error 1 Name must be less than 16 characters!");
else
empName = name;
Эта техника требует наличия двух уникально именованных методов для управления
единственным элементом данных. Для иллюстрации модифицируем метод M ain() сле­
дующим образом:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Encapsulation *****\n");
Employee emp = new Employee("Marvin", 456, 30000);
emp.GiveBonus(1000);
emp.DisplayStats();
// Использовать методы get/set для взаимодействия с именем объекта.
emp.SetName("Marv");
Console.WriteLine("Employee is named: {0}", emp.GetName());
Console.ReadLine();
}
Благодаря коду в методе SetName (), попытка присвоить строку длиннее 15 символов
приводит к выводу на консоль жестко закодированного сообщения об ошибке:
static void Main(string [] args)
{
Console.WriteLine ( " * * * * * Fun with Encapsulation *****\n");
// Длиннее 15 символов! На консоль выводится сообщение об ошибке.
Employee emp2 = new Employee ();
emp2.SetName("Xena the warrior princess");
Console.ReadLine ();
}
Пока все хорошо. Приватное поле empName инкапсулировано с использованием
двух методов GetName() и SetNam e(). Для дальнейшей инкапсуляции данных в клас­
се Employee понадобится добавить ряд дополнительных методов (например, G etlD Q ,
SetID (), G etC u rren tPay(), S etC u rren tP a y()). Каждый метод, изменяющий данные, мо­
жет иметь в себе несколько строк кода для проверки дополнительных бизнес-правил.
212
Часть II. Главные конструкции программирования на C#
Хотя можно поступить именно так, для инкапсуляции данных класса в C# предлагается
удобная альтернативная нотация.
Инкапсуляция с использованием свойств .NET
Вдобавок к возможности инкапсуляции полей данных с использованием традицион­
ной пары методов get/set, в языках .NET имеется более предпочтительный способ ин­
капсуляции данных с помощью свойств. Прежде всего, имейте в виду, что свойства —
это всего лишь упрощенное представление “реальных” методов доступа и изменения.
Это значит, что разработчик класса по-прежнему может реализовать любую внутрен­
нюю логику, которую нужно выполнить перед присваиванием значения (например, пре­
образовать в верхний регистр, очистить от недопустимых символов, проверить границы
числовых значений и т.д.).
Ниже приведен измененный класс Employee, который теперь обеспечивает инкапсу­
ляцию каждого поля с применением синтаксиса свойств вместо традиционных методов
get/set.
class Employee
{
// Поля данных.
private string empName;
private int empID;
private float currPay;
// Свойства.
public string Name
{
get { return empName; }
set
{
if (value.Length > 15)
Console.WriteLine("Error! Name must be less than 16 characters!");
else
empName = value;
}
// Можно было бы добавить дополнительные бизнес-правила для установки
// этих свойств, но в данном примере в этом нет необходимости.
public int ID
{
get { return empID; }
set { empID = value; }
}
public float Pay
{
get { return currPay; }
set { currPay = value; }
Свойство C# состоит из определений контекстов чтения get (метод доступа) и set
(метод изменения), вложенных непосредственно в контекст самого свойства. Обратите
внимание, что свойство указывает тип данных, которые оно инкапсулирует, как тип
возвращаемого значения. Кроме того, в отличие от метода, в определении свойства не
используются скобки (даже пустые). Обратите внимание на комментарий к текущему
свойству ID:
Глава 5. Определение инкапсулированных типов классов
213
// int представляет тип инкапсулируемых свойством данных.
// Тип данных должен быть идентичен связанному полю (empID).
public int ID // Обратите внимание на отсутствие скобок.
{
get { return empID; }
set { empID = value; }
}
В контексте set свойства используется лексема v a lu e, которая представляет вход­
ное значение, присваиваемое свойству вызывающим кодом. Эта лексема не является
настоящим ключевым словом С#, а представляет собой то, что называется контексту­
альным ключевым словом. Когда лексема v a lu e находится внутри контекста set, она
всегда обозначает значение, присваиваемое вызывающим кодом, и всегда имеет тип,
совпадающий с типом самого свойства. Поэтому свойство Name может проверить допус­
тимую длину s t r in g следующим образом:
public string Name
{
get { return empName; }
set
{
// Здесь value имеет тип string,
if (value.Length > 15)
Console .WnteLine ("Error ! Name must be less than 16 characters1");
else
empName = value;
}
}
При наличии этих свойств вызывающему коду кажется, что он имеет дело с обще­
доступным элементом данных; однако “за кулисами” при каждом обращении вызывает­
ся корректный get или set, обеспечивая инкапсуляцию:
static void Main(string [] args)
{
Console.WriteLine("***** Fun with Encapsulation *****\n");
Employee emp = new Employee("Marvin", 456, 30000);
emp.GiveBonus(1000);
emp.DisplayStats();
// Установка и получение свойства Name.
emp.Name = "Marv";
Console.WriteLine("Employee is named: {0}", emp.Name);
Console.ReadLine();
}
Свойства (в противоположность методам доступа и изменения) также облегчают ма­
нипулирование типами, поскольку способны реагировать на внутренние операции С#.
Для иллюстрации представим, что тип класса Employee имеет внутреннюю приватную
переменную-член, хранящую возраст сотрудника. Ниже показаны необходимые изме­
нения (обратите внимание на использование цепочки вызовов конструкторов):
class Employee
{
// Новое поле и свойство.
private int empAge;
public int Age
{
get { return empAge; }
set { empAge = value; }
}
214
Часть II. Главные конструкции программирования на C#
// Обновленные конструкторы.
public Employee() {}
public Employee(string name, int id, float pay)
:this(name, 0, id, pay)(}
public Employee(string name, int age, int id, float pay)
{
empName = name;
empID = id;
empAge = age;
currPay = pay;
// Обновленный метод D isp la y S ta ts() теперь учитывает возраст.
public void DisplayStats()
{
Console .WnteLine ("Name : {0}", empName);
Console.WriteLine ("ID: {0}", empID);
Console .WnteLine ("Age : {0}", empAge);
Console.WriteLine("Pay: {0}", currPay);
Теперь предположим, что создан объект Employee по имени jo e. Необходимо, чтобы
в день рождения сотрудника возраст увеличивался на 1 год. Используя традиционные
методы set/get, пришлось бы написать код вроде следующего:
Employee joe = new Employee();
joe.SetAge(joe.GetAge () + 1);
Однако если empAge инкапсулируется через свойство по имени Аде, можно записать
проще:
Employee joe = new Employee();
joe.Age++;
Использование свойств внутри определения класса
Свойства, а в особенности их часть set — это общепринятое место для размещения
бизнес-правил класса. В настоящее время класс Employee имеет свойство Name, кото­
рое гарантирует длину имени не более 15 символов. Остальные свойства (ID, Рау и Аде)
также могут быть обновлены с добавлением соответствующей логики.
Хотя все это хорошо, но следует принимать во внимание также и то, что обычно
происходит внутри конструктора класса. Конструктор получает входные параметры,
проверяет корректность данных и затем выполняет присваивания внутренним приват­
ным полям. В настоящее время ведущий конструктор не проверяет входные строковые
данные на допустимый диапазон, поэтому можно было бы изменить его следующим
образом:
public Employee (string name, int age, int id, float pay)
{
// Это может оказаться проблемой...
if (name.Length > 15)
Console.WriteLine ("Error 1 Name must be less than 16 characters1");
else
empName = name;
empID = id;
empAge = age;
currPay = pay;
Глава 5. Определение инкапсулированных типов классов
215
Наверняка вы заметили проблему, связанную с этим подходом. Свойство Name и
ведущий конструктор предпринимают одну и ту же проверку ошибок! В результате
получается дублирование кода. Чтобы упростить код и разместить всю проверку оши­
бок в центральном месте, для установки и получения данных внутри класса разумно
всегда использовать свойства. Ниже показан соответствующим образом обновленный
конструктор:
public Employee (string name, int age, int id, float pay)
{
// Уже лучше! Используйте свойства для установки данных класса.
// Это сократит количество дублированных проверок ошибок.
Name = name;
Age = age;
ID = id;
Pay = pay;
}
Помимо обновления конструкторов с целью использования свойств для присваива­
ния значений, имеет смысл сделать это повсюду в реализации класса, чтобы гаранти­
ровать неукоснительное соблюдение бизнес-правил. Во многих случаях единственное
место, где можно напрямую обращаться к приватным данным — это внутри самого
свойства. С учетом сказанного модифицируем класс Employee, как показано ниже:
class Employee
{
// Поля данных.
private string empName;
private int empID;
private float currPay;
private int empAge;
// Конструкторы,
public Employee () { }
public Employee(string name, int id, float pay)
:this(name, 0, id, pay) { }
public Employee(string name, int age, int id, float pay)
{
Name = name;
Age = age;
ID = id;
Pay = pay;
}
// Методы.
public void GiveBonus(float amount)
{ Pay += amount; }
public void DisplayStats ()
{
Console.WriteLine ("Name : {0}", Name);
Console .WnteLine (" ID: {0}", ID);
Console.WriteLine("Age: {0}", Age);
Console.WriteLine("Pay: {0}", Pay);
}
// Свойства остаются прежними...
}
Внутреннее представление свойств
Многие программисты склонны именовать традиционные методы доступа и измене­
ния с применением префиксов g e t _ и s e t _ (например, g e t _ Name() и s e t _ NameO).
216
Часть II. Главные конструкции программирования на C#
В языке C# такое соглашение об именовании само по себе не проблематично. Однако
важно понимать, что “за кулисами” свойства представлены в коде CIL с использованием
тех же самых префиксов.
Например, открыв сборку Em ployeeApp.exe в утилите ild a sm .ex e, можно увидеть,
что каждое свойство отображается на скрытые методы g e t XXX () и s e t XXX ( ) , вызы­
ваемые внутри CLR (рис. 5.6).
Рис. 5.6. Свойство внутреннее представлено методами get/set
Предположим, что тип Employee теперь имеет приватную переменную-член по име­
ни empSSN для представления номера карточки социального страхования (SSN) лица;
этой переменной будет манипулировать свойство по имени SocialSecurityNumber
(также предположим, что специальный конструктор и метод DisplayStats () обновлены
с учетом нового элемента данных).
// Добавим поддержку нового поля, представляющего SSN сотрудника.
class Employee
{
private string empSSN;
public string SocialSecurityNumber
{
get { return empSSN; }
set { empSSN = value; }
}
// Конструкторы.
public Employee() {}
public Employee(string name, int id, float pay)
:this(name, 0, id, pay, ""){}
public Employee (string name, int age, int id, float pay, string ss-n)
{
Name = name;
Age = age;
ID = id;
Pay = pay;
Глава 5. Определение инкапсулированных типов классов
217
SocialSecuntyNumber = ssn;
}
public void DisplayStats ()
{
Console.WriteLine("Name: {0}", Name);
Console .WnteLine (" ID : {0}", ID);
Console.WriteLine ("Age: {0}", Age);
Console.WriteLine("Pay: {0}", Pay);
Console .WriteLine ("SSN: {0}", SocialSecuntyNumber);
}
Если в этом классе также определить два метода с именами get SocialSecurity
Number () и set_SocialSecurityNumber (), возникнет ошибка времени компиляции:
// Помните, что свойство в действительности отображается на пару get_/set_!
class Employee
{
public string get_SocialSecurityNumber()
{
return empSSN;
}
public void set_SocialSecurityNumber(string ssn)
{
empSSN = ssn;
}
На заметку! В библиотеках базовых классов .NET всегда отдается предпочтение использованию
для инкапсуляции свойств, а не традиционных методов доступа и изменения. Поэтому при по­
строении специальных классов, которые интегрируются с платформой .NET, избегайте объяв­
ления традиционных методов get и set.
Управление уровнями видимости операторов get/set свойств
Если не указать иного, видимость логики get и set управляется исключительно мо­
дификаторами доступа из объявления свойства:
// Учитывая объявление свойства, логика get и set является общедоступной.
public string SocialSecuntyNumber
{
get { return .empSSN; }
set { empSSN = value; }
}
В некоторых случаях было бы удобно задавать уникальные уровни доступа для л о ­
гики get и set. Для этого просто добавьте префикс — ключевое слово доступа к соответ­
ствующим ключевым словам get или set (контекст без префикса получает видимость
объявления свойства):
// Пользователи объекта могут только получать значение, однако класс
// Employee и производные типы могут также устанавливать значение.
public string SocialSecuntyNumber
{
get { return empSSN; }
protected set { empSSN = value; }
218
Часть II. Главные конструкции программирования на C#
В этом случае логика set свойства SocialSecurityNumber может быть вызвана
только текущим классом и его производными классами, а потому не может быть вы­
звана из экземпляра объекта. Ключевое слово protected будет детально описываться
в следующей главе, при рассмотрении наследования и полиморфизма.
Свойства, доступные только для чтения и только для записи
При инкапсуляции данных может понадобиться сконфигурировать свойство, дос­
тупное только для чтения. Для этого нужно просто опустить блок set. Аналогично,
если требуется создать свойство, доступное только для записи, следует опустить блок
get. Например (хоть это и не требуется в рассматриваемом примере), вот как сделать
свойство SocialSecurityNumber доступным только для чтения:
public string SocialSecurityNumber
{
get { return empSSN; }
}
После этого единственным способом модификации номера карточки социального
страхования будет передача его в аргументе конструктора. Теперь попытка установить
новое значение SSN для сотрудника внутри ведущего конструктора приведет к ошибке
компиляции:
public Employee (string name, int age, int id, float pay, string ssn)
{
Name = name;
Age = age;
ID = id;
Pay = pay;
// Теперь это невозможно, поскольку свойство
// предназначено только для чтения!
SocialSecurityNumber = ssn;
Если сделать это свойство доступным только для чтения, в логике конструктора ос­
танется лишь пользоваться лежащей в основе переменной-членом ssn.
Статические свойства
В C# также поддерживаются статические свойства. Вспомните из начала этой главы,
что статические члены доступны на уровне класса, а не на уровне экземпляра (объекта)
этого класса. Например, предположим, что в классе Employee определен статический
элемент данных для представления названия организации, нанимающей сотрудников.
Инкапсулировать статическое свойство можно следующим образом:
// Статические свойства должны оперировать статическими данными!
class Employee
private static string companyName;
public static string Company
{
get { return companyName; }
set { companyName = value; }
Глава 5. Определение инкапсулированных типов классов
219
Манипулировать статическими свойствами можно точно так же, как статическими
методами:
// Взаимодействие со статическим свойством.
static void Main(string[] args)
{
Console.WnteLine ("***** Fun with Encapsulation *****\n");
// Установить компанию.
Employee.Company = "My Company";
Console.WriteLine("These folks work at {0}.", Employee.Company);
Employee emp = new Employee("Marvin", 24, 456, 30000, "111-11-1111");
emp.GiveBonus(1000) ;
emp.DisplayStats();
Console.ReadLine();
}
И, наконец, вспомните, что классы могут поддерживать статические конструкторы.
Поэтому если нужно гарантировать, что имя в статическом поле companyName всегда
будет установлено в Му Company, понадобится написать следующий код:
// Статические конструкторы используются для инициализ ации статических данных.
public class Employee
{
private Static companyName As string
static Employee ()
{
companyName = "My Company";
}
}
Используя такой подход, нет необходимости явно вызывать свойство Company для
того, чтобы установить начальное значение:
// Автоматическая установка значения "Му Company" через статический конструктор.
static void Main(string[] args)
{
Console.WriteLine("These folks work at {0}", Employee.Company);
}
В завершение исследований инкапсуляции с использованием свойств C# следует
запомнить, что эти синтаксические сущности служат для тех же целей, что и тради­
ционные методы get/set. Преимущество свойств состоит в том, что пользователи объ­
ектов могут манипулировать внутренними данными, применяя единый именованный
элемент
Исходный код. Проект Em ployeeАрр доступен в подкаталоге C h apter 5.
Понятие автоматических свойств
С выходом платформы .NET 3.5 в языке C# появился еще один способ определения
простых служб инкапсуляции с минимальным кодом, а именно — синтаксис автомати­
ческих свойств. Для целей иллюстрации создадим новый проект консольного приложе­
ния C# по имени AutoProps. Добавим в него новый файл класса C# (Car.cs), в котором
определен показанный ниже класс, инкапсулирующий единственную порцию данных с
использованием классического синтаксиса свойств.
220
Часть II. Главные конструкции программирования на C#
// Тип Саг, использующий стандартный синтаксис свойств,
class Саг
{
private string carName = string.Empty;
public string PetName
{
get { return carName; }
set { carName = value; }
}
Хотя большинство свойств C# содержат в своем контексте бизнес-правила, не так
уж редко бывает, что некоторые свойства не делают буквально ничего, помимо простого
присваивания и возврата значений, как в приведенном выше коде. В таких случаях было
бы слишком громоздко многократно определять приватные поля и некоторые простые
определения свойств. Например, при построении класса, которому нужно 15 приватных
элементов данных, в конечном итоге получаются 15 связанных с ними свойств, кото­
рые, по сути, представляют собой не более чем тонкие оболочки для инкапсуляции.
Чтобы упростить процесс простой инкапсуляции данных полей, можно применять
синтаксис автоматических свойств. Как следует из названия, это средство переклады­
вает работу по определению лежащего в основе приватного поля и связанного свойства
C# на компилятор, используя небольшое усовершенствование синтаксиса. Рассмотрим
переделанный класс Саг, в котором этот синтаксис применяется для быстрого создания
трех свойств:
class Саг
{
// Автоматические свойства1
public string PetName { get; set; }
public int Speed { get; set; }
public string Color { get; set; }
}
При определении автоматических свойств указывается модификатор доступа, лежа­
щий в основе тип данных, имя свойства и пустые контексты get/set. Во время компи­
ляции тип будет оснащен автоматически сгенерированным полем и соответствующей
реализацией логики set/get.
На заметку! Имя автоматически сгенерированного лежащего в основе приватного поля в коде C#
не доступно. Единственный способ увидеть его — воспользоваться таким инструментом, как
ild a sm .ex e.
Однако в отличие от традиционных свойств С#, создавать автоматические свойства,
предназначенные только для чтения или только для записи, нельзя. Хотя может по­
казаться, что для этого достаточно опустить g e t или s e t в объявлении свойства, как
показано ниже:
// Свойство только для чтения? Ошибка!
public int MyReadOnlyProp { get; }
// Свойство только для записи? Ошибка!
public int MyWriteOnlyProp { set; }
Но на самом деле это приведет к ошибке компиляции. Определяемое автоматическое
свойство должно поддерживать функциональность и чтения, и записи. Тем не менее,
можно реализовать автоматическое свойство с более ограничивающими контекстами
get или set:
Глава 5. Определение инкапсулированных типов классов
221
// Контекст get общедоступный, a set — защищенный.
// Контекст get/set можно также объявить приватным,
public int SomeOtherProperty { get; protected set; }
Взаимодействие с автоматическими свойствами
Поскольку компилятор будет определять лежащие в основе приватные поля во время
компиляции, класс с автоматическими свойствами всегда должен использовать синтак­
сис свойств для установки и чтения лежащих в основе значений. Это важно отметить,
потому что многие программисты напрямую используют приватные поля внутри опре­
деления класса, что в данном случае невозможно. Например, если бы класс Саг вклю­
чал метод D is p la y S ta ts (), он должен был бы реализовать этот метод, используя имя
свойства:
class Саг
{
// Автоматические свойства!
public string PetName { get; set; }
public int Speed { get; set; }
public string Color { get; set; }
public void DisplayStats ()
{
Console.WnteLine ("Car Name: {0}", PetName);
Console.WriteLine("Speed: {0}", Speed);
Console.WnteLine ("Color: {0}", Color);
}
При использовании объекта, определенного с автоматическими свойствами, можно
присваивать и получать значения, используя ожидаемый синтаксис свойств:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Automatic Properties *****\n");
Car c = new Car () ;
c.PetName = "Frank";
c .Speed = 55;
c .Color = "Red";
Console.WriteLine("Your car is named {0}? That's odd...",
c .PetName);
c .DisplayStats();
Console.ReadLine();
}
Замечания относительно автоматических
свойств и значений по умолчанию
При использовании автоматических свойств для инкапсуляции числовых и булев­
ских данных можно сразу применять автоматически сгенерированные свойства типа
прямо в своей кодовой базе, поскольку их скрытым полям будут присвоены безопасные
значения по умолчанию, которые могут быть использованы непосредственно. Однако
будьте осторожны, если синтаксис автоматического свойства применяется для упаков­
ки переменной другого класса, потому что скрытое поле ссылочного типа также будет
установлено в значение по умолчанию, т.е. n u ll.
Рассмотрим следующий новый класс по имени Garage, в котором используются два
автоматических свойства:
222
Часть II. Главные конструкции программирования на C#
class Garage
{
// Скрытое поле int установлено в О!
public int NumberOfCars { get; set; }
// Скрытое поле Car установлено в null!
public Car MyAuto { get; set; }
}
Имея установленные C# значения по умолчанию для полей данных, значение
NumberOfCars можно вывести в том виде, как оно есть (поскольку ему автоматически
присвоено значение 0). Однако если напрямую обратиться к MyAuto, то во время выпол­
нения сгенерируется исключение ссылки на n u ll, потому что лежащей в основе пере­
менной-члену типа Саг не был присвоен новый объект:
static void Main(string [] args)
Garage g = new Garage () ;
// Нормально, печатается значение по умолчанию, равное 0.
Console.WriteLine("Number of Cars: {0}", g .NumberOfCars);
// Ошибка времени выполнения! Лежащее в основе поле в данный момент равно null!
Console.WriteLine(g.MyAuto.PetName);
Console.ReadLine();
}
Учитывая, что лежащие в основе приватные поля создаются во время компиляции,
в синтаксисе инициализации полей C# нельзя непосредственно размещать экземпляр
ссылочного типа с помощью new. Это должно делаться конструкторами класса, что
обеспечит создание объекта безопасным образом. Например:
class Garage
{
// Скрытое поле установлено в 0!
public int NumberOfCars { get; set; }
// Скрытое поле установлено в null!
public Car MyAuto { get; set; }
// Для переопределения значений по умолчанию, присвоенных
// скрытым полям, должны использоваться конструкторы,
public Garage()
{
MyAuto = new Car ();
NumberOfCars = 1;
}
public Garage (Car car, int number)
{
MyAuto = car;
NumberOfCars = number;
После этой модификации объект Саг можно поместить в объект Garage, как пока­
зано ниже:
static void Main(string [] args)
{
Console.WriteLine
(" * ** **
// Создать автомобиль.
Car c = new Car () ;
Fun with Automatic Properties *****\n");
Глава-5. Определение инкапсулированных типов классов
223
c.PetName = "Frank";
с .Speed = 55;
с.Color = "Red";
с .Displaystats ();
// Поместить автомобиль в гараж.
Garage g = new Garage () ;
g.MyAuto = c;
// Вывод количества автомобилей в гараже.
Console .WnteLine ("Number of Cars in garage: {0}", g .NumberOfCars);
// Вывод названия автомобиля.
Console.WriteLine("Your car is named: {0}", g .MyAuto.PetName);
Console.ReadLine();
Нельзя не согласиться с тем, что это очень полезное свойство языка программиро­
вания С#, поскольку свойства для класса можно определить с использованием просто­
го синтаксиса. Естественно, если свойство помимо получения и установки лежащего
в основе приватного поля требует дополнительного кода (такого как логика проверки
достоверности, запись в журнал событий, взаимодействие с базой данных), придется
определить его как “нормальное” свойство .NET вручную. Автоматические свойства C#
никогда не делают ничего кроме предоставления простой инкапсуляции для лежащих в
основе приватных данных (сгенерированных компилятором).
Исходный код. Проект A u to P ro p s доступен в подкаталоге C h a p te r 5.
Понятие синтаксиса инициализации объектов
Как можно было видеть на протяжении этой главы, при создании нового объек­
та конструктор позволяет указывать начальные значения. Также было показано, что
свойства позволяют получать и устанавливать лежащие в основе данные в безопасной
манере. При работе с классами, которые написаны другими, включая классы из биб­
лиотеки базовых классов .NET, нередко можно заметить, что в них есть более одного
конструктора, позволяющего устанавливать каждую порцию данных внутреннего со­
стояния. Учитывая это, программист обычно старается выбрать наиболее подходящий
конструктор, после чего присваивает недостающие значения, используя доступные в
классе свойства.
Чтобы облегчить процесс создания и запуска объекта, в C# предлагается синтаксис
инициализатора объекта. С помощью этого механизма можно создать новую объект­
ную переменную и присвоить значения множеству свойств и/или общедоступных по­
лей в нескольких строках кода. Синтаксически инициализатор объекта выглядит как
список значений, разделенных запятыми, помещенный в фигурные скобки ({}). Каждый
элемент в списке инициализации отображается на имя общедоступного поля или свой­
ства инициализируемого объекта.
Рассмотрим пример применения этого синтаксиса. Создадим новое консольное при­
ложение по имени O b ject I n i t i a l i z e r s . Ниже показан класс Poin t, в котором исполь­
зуются автоматические свойства (что вообще-то не обязательно в данном примере, но
помогает сократить код):
class Point
{
public int X { get; set; }
public int Y { get; set; }
224
Часть II, Главные конструкции программирования на C#
public Point (int xVal, int yVal)
{
X = xVal;
Y = yVal;
}
public Point () { }
public void DisplayStats ()
{
Console. WnteLine ("[{ 0 }, {1}]", X, Y) ;
}
Теперь посмотрим, как создавать объекты P oin t:
static void Main(string[] args)
{
Console .WnteLine ("***** Fun with Object Init Syntax *****\n");
// Создать объект Point с установкой каждого свойства вручную.
Point firstPoint = new Point ();
firstPoint.X = 10;
firstPoint.Y = 10;
firstPoint.DisplayStats ();
// Создать объект Point с использованием специального конструктора.
Point anotherPoint = new Point(20, 20);
anotherPoint.DisplayStats();
// Создать объект Point с использованием синтаксиса инициализатора объекта.
Point finalPoint = new Point { X = 30, Y = 30 };
finalPoint.DisplayStats ();
Console.ReadLine();
}
При создании последней переменной P o in t не используется специальный конструк­
тор (как это принято делать традиционно), а вместо этого устанавливаются значения
общедоступных свойств X и Y. “За кулисами” вызывается конструктор типа по умол­
чанию, за которым следует установка значений указанных свойств. В конечном счете,
синтаксис инициализации объектов — это просто сокращенная нотация синтаксиса
создания переменной класса с помощью конструктора по умолчанию, с последующей
установкой свойств данных состояния.
Вызов специальных конструкторов с помощью
синтаксиса инициализации
В предыдущих примерах типы P o in t инициализировались неявным вызовом конст­
руктора по умолчанию этого типа:
// Здесь конструктор по умолчанию вызывается неявно.
Point finalPoint = new Point { X = 30, Y = 30 };
При желании конструктор по умолчанию можно вызывать и явно:
// Здесь конструктор по умолчанию вызывается явно
Point finalPoint = new Point () { X = 30, Y = 30 };
Имейте в виду, что при конструировании типа с использованием нового синтакси­
са инициализации можно вызывать любой конструктор, определенный в классе. В на­
стоящий момент в типе P o in t определен двухаргументный конструктор для установки
позиции (х , у). Таким образом, следующее объявление P o in t в результате приведет к
установке X равным 100 и Y равным 100, независимо от того факта, что в аргументах
конструктора указаны значения 10 и 16:
Глава 5. Определение инкапсулированных типов классов
225
// Вызов специального конструктора.
Point pt = new Point (10, 16) { X = 100, Y = 100 };
Имея текущее определение типа Point, вызов специального конструктора с при­
менением синтаксиса инициализации не особенно полезен (и чересчур многословен).
Однако если тип Point предоставляет новый конструктор, позволяющий вызывающе­
му коду установить цвет (через специальное перечисление PointColor), комбинация
специальных конструкторов и синтаксиса инициализации объекта становится ясной.
Изменим Point следующим образом:
public enum PointColor
{ LightBlue, BloodRed, Gold }
class Point
{
public int X { get; set; }
public int Y { get; set; }
public PointColor Colorf get; set; }
public Point(int xVal, int yVal)
{
X = xVal;
Y = yVal;
Color = PointColor.Gold;
}
public Point(PointColor ptColor)
{
Color = ptColor;
}
public Point ()
: this(PointColor.BloodRed){ }
public void DisplayStats ()
{
Console.WnteLine (" [{0 }, {1}]", X, Y);
Console.WriteLine ("Point is {0}", Color);
}
С помощью этого нового конструктора можно создать золотую точку (в позиции
(90, 20)), как показано ниже:
// Вызов более интересного специального конструктора
/ / с синтаксисом инициализации
Point goldPoint = new Point (PointColor.Gold) { X = 90, Y = 20 };
Console.WriteLine("Value of Point is: {0}", goldPoint.DisplayStats ());
Инициализация вложенных типов
Как было ранее кратко упомянуто в этой главе (и будет подробно рассматриваться
в главе 6), отношение “имеет” (“has-a”) позволяет составлять новые классы, определяя
переменные-члены существующих классов. Например, предположим, что имеется класс
Rectangle, который использует тип Point для представления координат верхнего л е­
вого и нижнего правого углов. Поскольку автоматические свойства устанавливают все
внутренние переменные классов в null, новый класс будет реализован с использовани­
ем “традиционного” синтаксиса свойств.
class Rectangle
{
private Point topLeft = new Point ();
private Point bottomRight = new Point ();
226
Часть II, Главные конструкции программирования на C#
public Point TopLeft
{
get { return topLeft; }
set { topLeft = value; }
}
public Point BottomRight
{
get { return bottomRight; }
set { bottomRight = value; }
}
public void DisplayStats ()
{
Console .WnteLine (" [TopLeft: {0}, {1}, {2} BottomRight: {3}, {4}, {5}]",
topLeft.X, topLeft.Y, topLeft.Color,
bottomRight.X, bottomRight.Y, bottomRight.Color);
С помощью синтаксиса инициализации объекта можно было бы создать новую пере­
менную Rectangle и установить внутренние экземпляры Point следующим образом:
// Создать и инициализировать Rectangle.
Rectangle myRect = new Rectangle
{
TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200}
Преимущество синтаксиса инициализации объектов в том, что он в основном сокра­
щает объем кода (предполагая, что нет подходящего конструктора). Вот как выглядит
традиционный подход для установки того же экземпляра Rectangle:
// Традиционный подход.
Rectangle г = new Rectangle ();
Point pi = new Point ();
pl.X = 10;
pl.Y = 10;
r .TopLeft = pi;
Point p2 = new Point ();
p2 .X = 200;
p2.Y = 200;
r .BottomRight = p2;
Хотя поначалу синтаксис инициализации объекта может показаться не слишком
привычным, как только вы освоитесь с кодом, то будете удивлены, насколько быстро
можно устанавливать состояние нового объекта с минимальными усилиями.
В завершение главы рассмотрим три небольшие темы, которые способствуют луч­
шему пониманию построения хорошо инкапсулированных классов: константные дан­
ные, поля, доступные только на чтения, и определения частичных классов.
Исходный код. Проект Objectlnitialozers доступен в подкаталоге Chapter 5.
Работа с данными константных полей
В C# имеется ключевое слово const для определения константных данных, которые
никогда не могут изменяться после начальной установки. Как и можно было предполо­
жить, это полезно при определении наборов известных значений, логически привязан­
ных к конкретному классу или структуре, для использования в приложениях.
Глава 5. Определение инкапсулированных типов классов
227
Предположим, что создается служебный класс по имени MyMathClass, в котором
нужно определить значение PI (будем считать его равным 3.14). Начнем с создания
нового проекта консольного приложения по имени ConstData. Учитывая, что другие
разработчики не должны иметь возможность изменять значение PI в коде, его можно
смоделировать с помощью следующей константы:
namespace ConstData
{
class MyMathClass
{
public const double PI = 3.14;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Const *****\n");
Console .WnteLine ("The value of PI is: {0}", MyMathClass .PI) ;
// Ошибка! Нельзя изменять константу!
MyMathClass.PI = 3.1444;
Console.ReadLine();
}
}
Обратите внимание, что обращение к константным данным, определенным в клас­
се MyMathClass, осуществляется с использованием префикса в виде имени класса (т.е.
MyMathClass.PI). Это связано с тем, что константные поля класса являются неявно
статическими. Однако допустимо определять и обращаться к локальным константным
переменным внутри члена типа, например:
static void LocalConstStnngVanable ()
{
// Локальные константные данные доступны непосредственно.
const string fixedStr = "Fixed string Data";
Console.WriteLine(fixedStr);
// Ошибка!
fixedStr = "This will not work!";
}
Независимо от того, где определяется константный элемент данных, следует всегда
помнить, что начальное значение константы всегда должно быть указано в момент ее
определения. Таким образом, если модифицировать класс MyMathClass так, что зна­
чение PI будет присваиваться в конструкторе класса, то возникнет ошибка времени
компиляции:
class MyMathClass
{
// Попытка установить PI в конструкторе?
public const double PI;
public MyMathClass ()
{
// Ошибка1
PI = 3.14;
}
Причина этого ограничения в том, что значение константных данных должно быть
известно во время компиляции. Конструкторы же, как известно, вызываются во время
выполнения.
228
Часть II. Главные конструкции программирования на C#
Понятие полей только для чтения
Близко к понятию константных данных лежит понятие данных полей, доступных
только для чтения (которые не следует путать со свойствами только для чтения).
Подобно константам, поля только для чтения не могут быть изменены после началь­
ного присваивания. Однако, в отличие от констант, значение, присваиваемое такому
полю, может быть определено во время выполнения, и потому может быть на законном
основании присвоено в контексте конструктора, но нигде более. Это может быть очень
полезно, когда значение поля неизвестно вплоть до момента выполнения (возможно,
потому, что для получения значения необходимо прочитать внешний файл), но нужно
гарантировать, что оно не будет изменено после первоначального присваивания. Для
иллюстрации рассмотрим следующее изменение в классе MyMathClass:
class MyMathClass
{
// Поля только для чтения могут присваиваться
/ / в конструкторах, но нигде более.
public readonly double PI;
public MyMathClass ()
{
PI = 3.14;
}
}
Любая попытка выполнить присваивание полю, помеченному как readonly, вне кон­
текста конструктора приведет к ошибке компиляции:
class MyMathClass
{
public readonly double PI;
public MyMathClass ()
{
PI = 3.14;
}
// Ошибка!
public void ChangePIO
{ PI = 3.14444; }
}
Статические поля только для чтения
В отличие от константных полей, поля только для чтения не являются неявно ста­
тическими. Поэтому если необходимо представить PI на уровне класса, то для этого по­
надобится явно использовать ключевое слово static. Если значение статического поля
только для чтения известно во время компиляции, то начальное присваивание выгля­
дит очень похожим на константу:
class MyMathClass
{
public static readonly double PI = 3.14;
}
class Program
{
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Const *****");
Console.WriteLine("The value of PI is: {0}", MyMathClass.PI);
Console.ReadLine ();
Глава 5. Определение инкапсулированных типов классов
229
Однако если значение статического поля только для чтения не известно до момента
выполнения, можно прибегнуть к использованию статического конструктора, как было
описано ранее в этой главе:
class MyMathClass
{
public static readonly double PI;
static MyMathClass ()
{ PI = 3.14; }
}
Исходный код. Проект ConstData доступен в подкаталоге Chapter 5.
Понятие частичных типов
В этой главе осталось еще разобраться с ролью ключевого слова p a r t i a l . Класс
производственного уровня легко может содержать многие сотни строк кода. К тому же,
учитывая, что типичный класс определен внутри одного файла *.cs, может получиться
очень длинный файл. В процессе создания классов нередко большая часть кода может
быть проигнорирована, будучи однажды написанной. Например, данные полей, свой­
ства и конструкторы, как правило, остаются неизменными во время эксплуатации, в то
время как методы имеют тенденцию модифицироваться довольно часто.
При желании можно разнести единственный класс на несколько файлов С#, что­
бы изолировать рутинный код от более ценных полезных членов. Для примера загру­
зим ранее созданный проект EmployeeАрр в Visual Studio и откроем файл Employee.сs
для редактирования. Сейчас этот единственный файл содержит код для всех аспектов
класса:
class Employee
{
//
//
//
//
Поля данных
Конструкторы
Методы
Свойства
}
Механизм частичных классов позволяет вынести конструкторы и поля данных в со­
вершенно новый файл по имени Employee.Internal.cs (обратите внимание, что имя
файла не имеет значения: здесь оно выбрано в соответствии с назначением класса).
Первый шаг состоит в добавлении ключевого слова partial к текущему определению
класса и вырезании кода, который должен быть помещен в новый файл:
// Employee.cs
partial class Employee
{
// Методы
// Свойства
}
Предполагая, что новый класс добавлен к проекту, можно переместить поля данных
и конструкторы в новый файл посредством простой операции вырезания и вставки.
Кроме того, необходимо добавить ключевое слово partial к этому аспекту определе­
ния класса.
// Employee.Internal.cs
partial class Employee
{
230
Часть II, Главные конструкции программирования на C#
// Поля данных
// Конструкторы
}
На заметку! Помните, что каждый аспект определения частичного класса должен быть помечен
ключевым словом p a r t i a l !
Скомпилировав модифицированный проект, вы не должны заметить никакой раз­
ницы. Основная идея, положенная в основу частичного класса, реализуется только во
время проектирования. Как только приложение скомпилировано, в сборке оказывается
один цельный класс. Единственное требование при определении частичных типов свя­
зано с тем, что разные части должны иметь одно и то же имя и находиться в пределах
одного и того же пространства имен .NET.
Откровенно говоря, определения частичных классов применяются нечасто. Однако
среда Visual Studio постоянно использует их в фоновом режиме. Позже в этой книге, ко­
гда речь пойдет о разработке приложений с графическим пользовательским интерфей­
сом посредством Windows Forms, Windows Presentation Foundation или ASP.NET, будет
показано, что Visual Studio изолирует сгенерированный визуальным конструктором код
в частичном классе, позволяя сосредоточиться на специфичной программной логике
приложения.
Исходный код. Проект Em ployeeАрр доступен в подкаталоге C hapter 5.
Резюме
Цель этой главы заключалась в ознакомлении с ролью типов классов С#. Вы видели,
что классы могут иметь любое количество конструкторов, которые позволяют пользо­
вателю объекта устанавливать состояние объекта при его создании. В главе также было
проиллюстрировано несколько приемов проектирования классов (и связанных с ними
ключевых слов). Ключевое слово t h i s используется для получения доступа к текущему
объекту, ключевое слово s t a t i c позволяет определять поля и члены, привязанные к
классу (а не объекту), а ключевое слово con st (и модификатор rea d on ly) дает возмож­
ность определять элементы данных, которые никогда не изменяются после первона­
чальной установки.
Большая часть главы была посвящена деталям первого принципа ООП: инкапсуля­
ции. Здесь вы узнали о модификаторах доступа C# и роли свойств типа, синтаксиса
инициализации объектов и частичных классов. Обладая всеми этими знаниями, теперь
вы готовы к тому, чтобы перейти к следующей главе, в которой узнаете о построении се­
мейства взаимосвязанных классов с использованием наследования и полиморфизма.
ГЛАВА
6
Понятия наследования
и полиморфизма
предыдущей главе рассматривался первый принцип ООП — инкапсуляция. Вы
узнали, как построить отдельный правильно спроектированный тип класса с
конструкторами и различными членами (полями, свойствами, константами и поля­
ми, доступными только для чтения). В настоящей главе мы сосредоточимся на осталь­
ных двух принципах объектно-ориентированного программирования: наследовании и
полиморфизме.
Прежде всего, вы узнаете, как строить семейства связанных классов с применением
наследования. Как будет показано, эта форма повторного использования кода позволя­
ет определять общую функциональность в родительском классе, которая может быть
использована и, возможно, изменена в дочерних классах. По пути вы узнаете, как ус­
танавливать полиморфный интерфейс в иерархиях классов, используя виртуальные и
абстрактные члены. Завершается глава рассмотрением роли начального родительского
класса в библиотеках базовых классов .NET — S ystem .O bject.
В
Базовый механизм наследования
Вспомните из предыдущей главы, что наследование — это аспект ООП, облегчающий
повторное использование кода. Строго говоря, повторное использование кода существу­
ет в двух видах: наследование (отношение “является”) и модель включения/делегации
(отношение “имеет”). Начнем главу с рассмотрения классической модели наследования
типа “является”.
При установке между классами отношения “является” строится зависимость между
двумя или более типами классов. Базовая идея, лежащая в основе классического на­
следования, заключается в том, что новые классы могут создаваться с использованием
существующих классов в качестве отправной точки. Давайте начнем с очень простого
примера, создав новый проект консольного приложения по имени B a s ic ln h e r ita n c e .
Предположим, что спроектирован класс по имени Саг, моделирующий некоторые базо­
вые детали автомобиля:
// Простой базовый класс.
class Саг
{
public readonly int maxSpeed;
private int currSpeed;
public Car (int max)
{
maxSpeed = max;
}
232
Часть II. Главные конструкции программирования на C#
public С а г ()
{
maxSpeed = 55;
}
public int Speed
{
get { return currSpeed; }
set
{
currSpeed = value;
if (currSpeed > maxSpeed)
{
currSpeed = maxSpeed;
}
}
Обратите внимание, что в Car применяется инкапсуляция для управления доступом
к приватному полю currSpead с использованием общедоступного свойства по имени
Speed. Имея такое определение, с типом Саг можно работать следующим образом:
static void Main(string[] args)
{
Console .WnteLine ("***** Basic Inheritance *****\n") ;
// Создать экземпляр типа Car и установить максимальную скорость.
Car myCar = new Car (80);
// Установить текущую скорость и вывести ее на консоль.
myCar.Speed = 50;
Console.WriteLine("My car is going {0} MPH", myCar.Speed);
Console.ReadLine();
}
Указание родительского класса для существующего класса
Теперь предположим, что планируется построить новый класс по имени MiniVan.
Подобно базовому классу Саг, необходимо, чтобы MiniVan поддерживал максимальную
скорость, текущую скорость и свойство по имени Speed, позволяющее пользователю
модифицировать состояние объекта. Ясно, что классы Саг и MiniVan взаимосвязаны;
фактически можно сказать, что MiniVan “является” Саг. Отношение “является” (фор­
мально называемое классическим наследованием) позволяет строить новые определе­
ния классов, расширяющие функциональность существующих классов.
Существующий класс, который будет служить основой для нового класса, называет­
ся базовым или родительским классом. Назначение базового класса состоит в опреде­
лении всех общих данных и членов для классов, которые расширяют его. Расширяющие
классы формально называются производными или дочерними классами. В C# для уста­
новки между классами отношения “является” используется операция двоеточия в опре­
делении класса:
// MiniVan 'является' Саг.
class MiniVan : Car
Так в чем же состоит выигрыш от наследования MiniVan от базового класса Саг?
Попросту говоря, объекты MiniVan имеют доступ ко всем общедоступным членам, оп­
ределенным в базовом классе.
Глава 6. Понятия наследования и полиморфизма
233
На заметку! Хотя конструкторы обычно определяются как общедоступные, производный класс ни­
когда не наследует конструкторы своего родительского класса.
Учитывая отношение между этими двумя типами классов, класс MiniVan можно ис­
пользовать следующим образом:
static void Main(string[] args)
{
Console.WriteLine("***** Basic Inheritance *****\n");
// Создать объект MiniVan.
MiniVan myVan = new MiniVan();
myVan.Speed = 10;
Console.WriteLine("My van is going {0} MPH",
myVan.Speed);
Console.ReadLine ();
}
Обратите внимание, что хотя к классу MiniVan не добавлены никакие члены, имеет­
ся прямой доступ к p u b lic -свойству Speed родительского класса, и таким образом, его
код используется повторно. Это намного лучше, чем создавать класс MiniVan, имеющий
в точности те же члены, что и Саг, такие как свойство Speed. В случае дублирования
кода в этих двух классах придется сопровождать два фрагмента одинакового кода, что
очевидно является непроизводительным расходом времени.
Всегда помните, что наследование предохраняет инкапсуляцию, а потому следую­
щий код вызовет ошибку компиляции, поскольку приватные члены никогда не могут
быть доступны через ссылку на объект:
static void Main(string[] args)
{
Console.WriteLine ("***** Basic Inheritance *****\n");
// Создать объект MiniVan.
MiniVan myVan = new MiniVan();
myVan.Speed = 10;
Console.WriteLine("My van is going {0} MPH",
myVan.Speed);
// Ошибка! Доступ к приватным членам невозможен!
myVan.currSpeed = 55;
Console.ReadLine();
}
Кстати говоря, если в MiniVan будет определен собственный набор членов, он не
получит доступа ни к одному приватному члену базового класса Саг. Опять-таки, при­
ватные члены могут быть доступны только в классе, в котором они определены.
// MiniVan унаследован от Саг.
class MiniVan : Car
{
public void TestMethod()
{
// OK! Доступ к p u b lic -членам родителя в производном типе возможен.
Speed = 10;
// Ошибка! Нельзя осуществлять доступ к p riv a te -членам родителя
//из производного типа!
currSpeed = 10;
}
234
Часть II. Главные конструкции программирования на C#
О множественном наследовании
ГЬворя о базовых классах, важно иметь в виду, что язык C# требует, чтобы любой
конкретный класс имел в точности один непосредственный базовый класс. Невозможно
создать тип класса, который напрямую унаследован от двух и более базовых классов
(эта техника, поддерживаемая в неуправляемом C++, называется множественным на­
следованием). Попытка создать класс, в котором указано два непосредственных роди­
тельских класса, как показано в следующем коде, приводит к ошибке компиляции:
//Не разрешается! Платформа .NET не допускает
// множественное наследование классов!
class WontWork
: BaseClassOne, BaseClassTwo
{}
Как будет показано в главе 9, платформа .NET позволяет конкретному классу или
структуре реализовывать любое количество дискретных интерфейсов. Благодаря этому,
тип C# может представлять набор поведений, избегая сложностей, присущих множест­
венному наследованию. Кстати говоря, хотя класс может иметь только один непосред­
ственный базовый класс, допускается наследование одного интерфейса от множества
других интерфейсов. Используя эту технику, можно строить изощренные иерархии ин­
терфейсов, моделирующих сложные поведения (см. главу 9).
Ключевое слово s e a le d
В C# поддерживается еще одно ключевое слово — sealed, которое предотвращает
наследование. Если класс помечен как sealed (запечатанный), компилятор не позволя­
ет наследовать от него. Например, предположим, решено, что нет смысла в дальнейшем
наследовании от класса MiniVan:
// Класс Minivan не может быть расширен!
sealed class MiniVan : Car
{
}
Если вы (или коллега по команде) попытаетесь унаследовать от этого класса, то по­
лучите ошибку времени компиляции:
// Ошибка! Нельзя расширять класс,
// помеченный ключевым словом sealed!
class DeluxeMiniVan
: MiniVan
{}
Чаще всего запечатывание имеет смысл при проектировании служебного класса.
Например, в пространстве имен System определено множество запечатанных классов.
В этом легко убедиться, открыв окно Object Browser в Visual Studio 2010 (через меню
View (Вид)) и выбрав класс String, определенный в пространстве имен System внутри
сборки mscorlib.dll. На рис. 6.1 обратите внимание на использование ключевого сло­
ва sealed, выделенного в окне Summary (Сводка).
Таким образом, подобно MiniVan, если попытаться построить новый класс, расши­
ряющий System.String, возникнет ошибка компиляции:
// Ошибка! Нельзя расширять класс, помеченный как sealed!
class MyString
: String
{}
Глава 6. Понятия наследования и полиморфизма
C ares
Object Browser X
Program <л
235
Start Page
■Ц
♦ CloneQ
V Compare(string. hil . unnq, int, mt, System.StringComparisoni
я ResolveEventHandler
Comparefstnng int, string int, mt. System.Globalization.Cultun
RuntimeArgumentKr
•$» RuntimeFieldHandle
•*
♦
Comparefstnng int string mt mt bool. System.Globalization.f
♦
Comparetstrmg mt string mt int bool)
RuntimeMethodHanr
W Comparefstnng int string mt int)
RuntimeTypeHandle
V Compare(strmg. string, bool System Globalization-Culturelnfo) T
SByte
^
SerializableAttnbute
^
STAThreadAttribute
Single
StackOverflowExceptr'^5
• v’ £ StrmgComparer
&
public sealed class S tring
Member of System
S um m ary:
Represents text as a series of Unicode characters
StnngComparison
Рис. 6.1. В библиотеках базовых классов определено множество запечатанных типов
На заметку! В главе 4 было показано, что структуры C# всегда неявно запечатаны (см. табл. 4.3).
Поэтому ни унаследовать одну структуру от другой, ни класс от структуры, ни структуру от
класса не получится. Структуры могут использоваться только для моделирования отдельных
атомарных, определенных пользователем типов. Для реализации отношения “является" необ­
ходимо применять классы.
Как можно догадаться, существует множество других деталей наследования, о кото­
рых вы узнаете в оставшейся части главы. А пока просто имейте в виду, что операция
двоеточия позволяет устанавливать между классами отношения “базовый-производный”, а ключевое слово sealed предотвращает наследование.
Изменение диаграмм классов Visual Studio
В главе 2 кратко упоминалось, что среда Visual Studio 2010 позволяет устанавливать
между классами отношения “базовый-производный” визуально во время проектирова­
ния. Чтобы использовать этот аспект IDE-среды, первый шаг состоит во включении
нового файла диаграммы классов в текущий проект. Для этого выберите в меню пункт
P ro je c t^ A d d N ew Item (ПроектОДобавить новый элемент) и затем пиктограмму C lass
D iagram (Диаграмма классов); на рис. 6.2 имя файла ClassDiagraml.cd было изменено
на Cars .cd.
installed Templates.
|r ^ j
Settings File
Visual C * Items
Text File
Visual C * Items
A is#rrb;y Inform ation File
Visual C# Items
Class Diagram
Type: Visual C * Items
л Visual C# Items
Code
Data
General
W eb
|
||j
jjj f^al
I
Class Diagram
W indows r 0 rms
W»F
’^ l
Reporting
Online Templates
Application M anifest F>i*
tt
I Class Diagram j
A Wanlc class diagram
Visual C# hems
Visual C * Items
j
W indows Script Host
Visual C# Items
m
Debugger Visualize*
Visual C * Items
Add
Рис. 6.2. Вставка в проект новой диаграммы классов
J
Cancel
236
Часть II. Главные конструкции программирования на C#
После щелчка на кнопке Add [Добавить) появится пустая поверхность проектиро­
вания. Для добавления классов к диаграмме просто перетаскивайте каждый файл из
окна Solution Explorer на эту поверхность. Также помните, что удаление элемента в ви­
зуальном конструкторе (за счет его выбора и нажатия клавиши <Delete>), не приводит к
удалению ассоциированного исходного кода, а просто убирает элемент из поверхности
проектирования. Текущая иерархия классов показана на рис. 6.3.
Рис. 6.3. Визуальный конструктор Visual Studio
На заметку! Итак, если необходимо автоматически добавить все текущие типы проекта на поверх­
ность проектирования, выберите узел P roject (Проект) в Solution Explorer и щелкните на кнопке
View Class Diagram (Показать диаграмму классов) в правом верхнем углу окна Solution Explorer.
Помимо простого отображения отношений между типами внутри текущего прило­
жения, вспомните из главы 2, что можно также создавать совершенно новые типы и
наполнять их членами, используя панель инструментов C lass D e s ig n e r (Конструктор
классов) и окно C lass D e ta ils (Детали класса).
Если хотите использовать эти визуальные инструменты в процессе дальнейшего
чтения книги — пожалуйста. Однако всегда анализируйте сгенерированный код, чтобы
четко понимать, что эти инструменты делают за вашей спиной.
Исходный код. Проект Basiclnheritance доступен в подкаталоге Chapter 6.
Второй принцип ООП: подробности о наследовании
Ознакомившись с базовым синтаксисом наследования, давайте рассмотрим более
сложный пример и узнаем о многочисленных деталях построения иерархий классов.
Для этого воспользуемся классом Employee, спроектированным в главе 5. Для начала
создадим новое консольное приложение C# по имени Employees.
Выберите пункт меню P ro je ct ^ A d d E xisting Item (Проекта Добавить существующий эле­
мент) и перейдите к месту нахождения файлов Employee.cs и Employee.Internals.cs,
которые были созданы в примере EmployeeApp из предыдущей главы. Выберите оба
Глава 6. Понятия наследования и полиморфизма
237
файла (щелкая на них при нажатой клавише < C trl> ) и щелкните на кнопке ОК. Среда
Visual Studio 2010 отреагирует копированием каждого файла в текущий проект.
Прежде чем начать построение производных классов, следует уделить внимание од­
ной детали. Поскольку первоначальный класс Employee был создан в проекте по имени
EmployeeApp, этот класс находится в идентично названном пространстве имен .NETT.
Пространства имен подробно рассматриваются в главе 14, а пока для простоты просто
переименуйте текущее пространство имен (в обоих файлах) на Employees, чтобы оно
соответствовало имени нового проекта:
// Не забудьте изменить название пространства имен в обоих файлах!
namespace Employees
{
partial class Employee
{ ••■}
)
На заметку! Чтобы подстраховаться, скомпилируйте и запустите новый проект, нажав <C trl+F 5> .
Пока программа ничего не делает, однако это позволит убедиться в отсутствии ошибок
компиляции.
Нашей целью будет создание семейства классов, моделирующих различные типы
сотрудников компании. Предположим, что необходимо воспользоваться функциональ­
ностью класса Employee при создании двух новых классов (Salesperson и Manager).
Иерархия классов, которую мы вскоре построим, будет выглядеть примерно так, как
показано на рис. 6.4 (имейте в виду, что в случае использования синтаксиса автомати­
ческих свойств C# отдельные поля в диаграмме не будут видны).
Рис. 6.4. Начальная иерархия классов
238
Часть II. Главные конструкции программирования на C#
Как показано на рис. 6.4, класс S a le sp erso n “является” Employee (как и Manager).
Вспомните, что в модели классического наследования базовые классы (вроде Employee)
используются для определения характеристик, общих для всех наследников. Подклассы
(такие как S ale sp erso n и Manager) расширяют общую функциональность, добавляя до­
полнительную специфическую функциональность.
Для нашего примера предположим, что класс Manager расширяет Employee, храня
количество опционов на акции, в то время как класс S alesperson поддерживает коли­
чество продаж. Добавьте новый файл класса (Manager.сс), определяющий тип Manager
следующим образом:
// Менеджерам нужно знать количество их опционов на акции.
class Manager : Employee
{
public int StockOptions { get; set; }
}
Затем добавьте новый файл класса (S a le s P e rs o n .c s ), в котором определен класс
S a lesp erson с соответствующим автоматическим свойством:
// Продавцам нужно знать количество продаж.
class Salesperson : Employee
{
public int SalesNumber { get; set; }
}
Теперь, после установки отношения “является”, S alesperson и Manager автоматиче­
ски наследуют все общедоступные члены базового класса Employee. Для иллюстрации
обновите метод M ain() следующим образом:
// Создание объекта подкласса и доступ к функциональности базового класса.
static void Main(string[] args)
{
Console.WriteLine ("***** The Employee Class Hierarchy *****\n ");
Salesperson danny = new Salesperson ();
danny.Age = 31;
danny.Name = "Danny";
danny.SalesNumber = 50;
Console.ReadLine();
}
Управление созданием базового класса
с помощью ключевого слова b a s e
Сейчас объекты S a le sp erso n и Manager могут быть созданы только с использова­
нием “бесплатного” конструктора по умолчанию (см. главу 5). Памятуя об этом, пред­
положим, что к типу Manager добавлен новый конструктор, который принимает шесть
аргументов и вызывается следующим образом:
static void Main(string [] args)
{
// Предположим, что у Manager есть конструктор со следующей сигнатурой:
// (s trin g fullName, in t age, in t empID,
// f lo a t currPay, strin g ssn, in t numbOfOpts)
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000);
Console.ReadLine ();
Глава 6. Понятия наследования и полиморфизма
239
Если взглянуть на список параметров, то ясно видно, что большинство из них долж­
но быть сохранено в переменных-членах, определенных в базовом классе Em ployee.
В этом случае для класса Manager можно реализовать специальный конструктор сле­
дующего вида:
public Manager(string fullName, int age, int empID,
float currPay, string ssn, int numbOfOpts)
{
// Это свойство определено в классе Manager.
StockOptions = numbOfOpts;
// Присвоим входные параметры, используя
// унаследованные свойства родительского класса.
ID = empID;
Age = age;
Name = fullName;
Pay = currPay;
// Здесь возникнет ошибка компиляции, поскольку
// свойство SSN доступно только для чтения!
SocialSecuntyNumber = ssn;
}
Первая проблема такого подхода состоит в том, что если определить какое-то свойст­
во как доступное только для чтения (например, S ocia lS ecu rityN u m b er), то присвоить
значение входного параметра s t r i n g соответствующему полю не удастся, как можно
видеть в финальном операторе специального конструктора.
Вторая проблема состоит в том, что был неявно создан довольно неэффективный
конструктор, учитывая тот факт, что в С#, если не указать иного, конструктор базо­
вого класса вызывается автоматически перед выполнением логики производного кон­
структора. После этого момента текущая реализация имеет доступ к многочисленным
p u b lic -свойствам базового класса Employee для установки его состояния. Таким обра­
зом, в действительности во время создания объекта Manager выполняется семь дейст­
вий (обращений к пяти унаследованным свойствам и двум конструкторам).
Для оптимизации создания производного класса необходимо хорошо реализовать
конструкторы подкласса, чтобы они явно вызывали специальный конструктор базо­
вого класса вместо конструктора по умолчанию. Поступая подобным образом, можно
сократить количество вызовов инициализаций унаследованных членов (что уменьшит
время обработки). Давайте модифицируем специальный конструктор класса Manager,
применив ключевое слово base:
public Manager(string fullName, int age, int empID,
float currPay, string ssn, int numbOfOpts)
: base (fullNam e, age, empID, currPay, ssn)
{
// Это свойство определено в классе Manager.
StockOptions = numbOfOpts;
Здесь ключевое слово base ссылается на сигнатуру конструктора (подобно синтак­
сису, используемому для сцепления конструкторов на единственном классе с исполь­
зованием ключевого слова t h is , как было показано в главе 5), что всегда указывает на
то, что производный конструктор передает данные конструктору непосредственного
родителя. В данной ситуации явно вызывается конструктор с пятью параметрами, оп­
ределенный в Employee, что избавляет от излишних вызовов во время создания экзем­
пляра базового класса. Специальный конструктор S a le s p e rs o n выглядит в основном
идентично:
240
Часть II. Главные конструкции программирования на C#
/ / В качестве общего правила, все подклассы должны юно вызывать
// соответствующий конструктор базового класса.
public Salesperson(string fullName, int age, int empID,
float currPay, string ssn, int numbOfSales)
: base (fullName, age, empID, currPay, ssn)
{
// Это касается нас!
SalesNumber = numbOfSales;
}
На заметку! Ключевое слово base можно использовать везде, где подкласс желает обратиться к
общедоступному или защищенному члену, определенному в родительском классе. Применение
этого ключевого слова не ограничивается логикой конструктора. Вы увидите примеры исполь­
зования base в такой манере далее в главе, во время рассмотрения полиморфизма.
И, наконец, вспомните, что как только в определении класса появляется специальный
конструктор, конструктор по умолчанию из класса молча удаляется. Следовательно, не
забудьте переопределить конструктор по умолчанию для типов Salesperson и Manager.
Например:
// Вернуть классу Manager конструктор по умолчанию.
public Salesperson () {}
Хранение фамильных тайн: ключевое слово p r o t e c t e d
Как уже должно быть известно, общедоступные (public) элементы непосредственно
доступны отовсюду, в то время как приватные (private) могут быть доступны только в
классе, где они определены. Вспомните из главы 5, что C# следует примеру многих дру­
гих современных объектных языков и предлагает дополнительное ключевое слово для
определения доступности членов, а именно — protected (защищенный).
Когда базовый класс определяет защищенные данные или защищенные члены, он
устанавливает набор элементов, которые могут быть доступны непосредственно любо­
му наследнику. Например, чтобы позволить дочерним классам Salesperson и Manager
непосредственно обращаться к разделу данных, определенному в Employee, можете из­
менить исходный класс Employee следующим образом:
// Защищенные данные состояния.
partial class Employee
{
// Теперь
protected
protected
protected
protected
protected
protected
производные классы могут напрямую обращаться к этой информации.
string empName;
int empID;
float currPay;
int empAge;
string empSSN;
static string companyName;
}
Преимущество определения защищенных членов в базовом классе состоит в том, что
производным типам больше не нужно обращаться к данным опосредованно, используя
общедоступные методы и свойства. Возможным минусом такого подхода, конечно же, яв­
ляется то, что когда производный тип имеет прямой доступ к внутренним данным своего
родителя, возникает вероятность непреднамеренного нарушения существующих бизнесправил, которые реализованы в общедоступных свойствах. При определении защищен­
ных членов создается уровень доверия между родительским и дочерним классами, по­
скольку компилятор не перехватит никаких нарушений существующих бизнес-правил.
Глава 6. Понятия наследования и полиморфизма
241
И, наконец, имейте в виду, что с точки зрения пользователя объекта защищенные
данные трактуются как приватные (поскольку пользователь находится “вне” семейства).
Потому следующий код некорректен:
static void Main(string [] args)
{
// Ошибка! Доступ к защищенным данным через экземпляр объекта невозможен!
Employee emp = new Employee();
emp.empName = "Fred";
}
На заметку! Хотя p r o t e c t e d -поля данных могут нарушить инкапсуляцию, объявлять p r o t e c t e d методы достаточно безопасно (и полезно). При построении иерархий классов очень часто при­
ходится определять набор методов, которые используются только производными типами.
Добавление запечатанного класса
Вспомните, что запечатанный (sealed) класс не может быть расширен другими клас­
сами. Как уже упоминалось, эта техника чаще всего применяется при проектировании
служебных классов. Тем не менее, при построении иерархий классов можно обнару­
жить, что некоторая ветвь в цепочке наследования нуждается в “отсечении”, поскольку
дальнейшее ее расширение не имеет смысла. Например, предположим, что в приложе­
ние добавлен еще один класс (PTSalesPerson), который расширяет существующий тип
Salesperson. На рис. 6.5 показано текущее добавление.
Рис. 6.5. Класс PTSalesPerson
Класс PTSalesPerson представляет продавца, который работает с частичной заня­
тостью. Предположим, что необходимо гарантировать отсутствие возможности наследо­
вания от класса PTSalesPerson. (В конце концов, какой смысл в “частичной занятости
от частичной занятости”?) Для предотвращения наследования от класса используется
ключевое слово sealed:
sealed class PTSalesPerson : Salesperson
{
public PTSalesPerson(string fullName, int age, int empID,
float currPay, string ssn, int numbOfSales)
:base (fullName, age, empID, currPay, ssn, numbOfSales)
{
}
// Остальные члены класса. ..
}
242
Часть II. Главные конструкции программирования на C#
Учитывая, что запечатанные классы не могут быть расширены, может возникнуть
вопрос: каким образом повторно использовать функциональность, если класс помечен
как sealed? Чтобы построить новый класс, использующий функциональность запеча­
танного класса, единственным вариантом является отказ от классического наследова­
ния в пользу модели включения/делегации (т.е. отношения “имеет”).
Реализация модели включения/делегации
Вспомните, что повторное использование кода возможно в двух вариантах. Только что
было рассмотрено классическое отношение “является”. Перед тем, как мы обратимся к
третьему принципу ООП (полиморфизму), давайте поговорим об отношении “имеет” (еще
известном под названием модели включения/делегации или агрегации). Предположим,
что создан новый класс, который моделирует пакет льгот для сотрудников:
// Этот новый тип будет работать как включаемый класс.
class BenefitPackage
{
// Предположим, что есть другие члены, представляющие
// медицинские/стоматологические программы и т.д.
public double ComputePayDeduction()
{
return 125.0;
Очевидно, что было бы довольно нелепо устанавливать отношение “является” между
классом BenefitPackage и типами сотрудников. (Employee “является” BenefitPackage?
Вряд ли). Однако должно быть ясно, что какие-то отношения между ними должны быть
установлены. Короче говоря, понадобится выразить идею, что каждый сотрудник “име­
ет” BenefitPackage. Для этого можно модифицировать определение класса Employee
следующим образом:
// Сотрудники имеют льготы.
partial class Employee
{
// Содержит объект BenefitPackage.
protected BenefitPackage empBenefits = new BenefitPackage() ;
Таким образом, один объект успешно содержит в себе другой объект. Однако что­
бы представить функциональность включенного объекта внешнему миру, потребуется
делегация. Делегация — это просто акт добавления общедоступных членов к включаю­
щему классу, которые используют функциональность включенного объекта. Например,
можно было бы обновить класс Employee, чтобы он представлял включенный объект
empBenefits с помощью специального свойства, а также пользоваться его функцио­
нальностью внутренне, через новый метод по имени GetBenefitCost ():
public partial class Employee
{
// Содержит объект BenefitPackage.
protected BenefitPackage empBenefits = new BenefitPackage();
// Представляет некоторое поведение, связанное с включенным объектом.
public double GetBenefitCost ()
{ return empBenefits.ComputePayDeduction(); }
Глава 6. Понятия наследования и полиморфизма
243
// Представляет объект через специальное свойство.
public BenefitPackage Benefits
{
get { return empBenefits; }
set { empBenefits = value; }
В следующем обновленном методе M ain() обратите внимание на взаимодействие с
внутренним типом B en efitsP a ck a ge, который определен в типе Employee:
static void Main(string[] args)
{
Console .WnteLine ("**** * The Employee Class Hierarchy *****\n ");
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000);
double cost = chucky.GetBenefitCost();
Console.ReadLine();
}
Определения вложенных типов
В предыдущей главе была кратко упомянута концепция вложенных типов, которая
является разновидностью только что рассмотренного отношения “имеет”. В C# (как и
в других языках .NET) допускается определять тип (перечисление, класс, интерфейс,
структуру или делегат) непосредственно внутри контекста класса или структуры. При
этом вложенный (или “внутренний”) тип считается членом охватывающего (или “внеш­
него”) класса, и в глазах исполняющей системы им можно манипулировать как любым
другим членом (полем, свойством, методом и событием). Синтаксис, используемый для
вложения типа, достаточно прост:
public class OuterClass
{
// Общедоступный вложенный тип может использоваться повсюду.
public class PublicInnerClass {}
// Приватный вложенный тип может использоваться
// только членами включающего класса.
private class PnvatelnnerClass {}
}
Хотя синтаксис ясен, понять, для чего это может потребоваться, не так-то просто.
Чтобы разобраться с этой техникой, рассмотрим характерные особенности вложенных
типов.
• Вложенные типы позволяют получить полный контроль над уровнем доступа
внутреннего типа, поскольку они могут быть объявлены как приватные (вспом­
ните, что не вложенные классы не могут быть объявлены с использованием клю­
чевого слова p r iv a te ).
• Поскольку вложенный тип является членом включающего класса, он может иметь
доступ к приватным членам включающего класса.
• Часто вложенные типы удобны в качестве вспомогательных для внешнего класса
и не предназначены для использования внешним миром.
Когда тип включает в себя другой тип класса, он может создавать переменные-чле­
ны этого типа, как любой другой элемент данных. Однако если вложенный тип нужно
использовать вне включающего типа, его понадобится квалифицировать именем вклю­
чающего типа. Взгляните на следующий код:
244
Часть II. Главные конструкции программирования на C#
static void Main(string [] args)
{
// Создать и использовать общедоступный вложенный класс. Верно!
OuterClass.PublicInnerClass inner;
inner = new OuterClass.PublicInnerClass ();
// Ошибка компилятора! Доступ к приватному классу невозможен!
OuterClass.PrivatelnnerClass inner2;
inner2 = new OuterClass.PrivatelnnerClass ();
}
Чтобы использовать эту концепцию в рассматриваемом примере с сотрудниками,
предположим, что теперь определение BenefitPackage вложено непосредственно в
класс Employee:
partial class Employee
{
public class BenefitPackage
{
// Предположим, что есть другие члены, представляющие
// медицинские/стоматологические программы и т.д.
public double ComputePayDeduction ()
{
return 125.0;
Вложение может иметь произвольную глубину. Например, пусть требуется создать
перечисление по имени BenefitPackageLevel, документирующее различные уровни
льгот, которые могут быть предоставлены сотруднику. Чтобы программно установить
тесную связь между Employee, BenefitPackage и BenefitPackageLevel, можно вло­
жить перечисление следующим образом:
// В Employee вложен BenefitPackage.
public partial class Employee
{
// В BenefitPackage вложено BenefitPackageLevel.
public class BenefitPackage
{
public enum BenefitPackageLevel
{
Standard, Gold, Platinum
}
public double ComputePayDeduction ()
{
return 125.0;
Из-за отношений вложения обратите внимание на то, как приходится использовать
это перечисление:
static void Main(string[] args)
{
// Определить уровень льгот.
Employee.BenefitPackage.BenefitPackageLevel myBenefitLevel =
Employee.BenefitPackage.BenefitPackageLevel.Platinum;
Console.ReadLine ()
Глава 6. Понятия наследования и полиморфизма
245
Блестяще! К этому моменту вы познакомились с множеством ключевых слов (и кон­
цепций), которые позволяют строить иерархии взаимосвязанных типов через класси­
ческое наследование, включение и вложенные типы. Если пока не все детали ясны, не
переживайте. На протяжении оставшейся части книги вы построите еще много допол­
нительных иерархий. А теперь давайте перейдем к рассмотрению последнего принципа
ООП: полиморфизма.
Третий принцип ООП:
поддержка полиморфизма в C#
Вспомните, что в базовом классе Employee был определен метод по имени
GiveBonusO со следующей первоначальной реализацией:
public partial class Employee
{
public void GiveBonus(float amount)
{
currPay += amount;
}
Поскольку этот метод был определен с ключевым словом public, теперь можно раз­
давать бонусы продавцам и менеджерам (а также продавцам с частичной занятостью):
static void Main(string[] args)
{
Console.WnteLine ("**** * The Employee Class Hierarchy *****\n ");
// Дать каждому сотруднику бонус?
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000);
chucky.GiveBonus(300);
chucky.DisplayStats();
Console.WnteLine () ;
Salesperson fran = new Salesperson ("Fran", 43, 93, 3000, "932-32-3232", 31);
fran.GiveBonus(200);
fran.DisplayStats();
Console.ReadLine();
}
Проблема текущего кода состоит в том, что общедоступно унаследованный метод
GiveBonusO работает идентично для всех подклассов. В идеале при подсчете бонуса
для штатного продавца и частично занятого продавца должно приниматься во вни­
мание количество продаж. Возможно, менеджеры должны получать дополнительные
опционы на акции вместе с денежным вознаграждением. Учитывая это, вы однажды
столкнетесь с интересным вопросом: “Как сделать так, чтобы связанные типы по-раз­
ному реагировали на один и тот же запрос?”. Попробуем отыскать на него ответ.
Ключевые слова v i r t u a l и o v e r r i d e
Полиморфизм предоставляет подклассу способ определения собственной версии ме­
тода, определенного в его базовом классе, с использованием процесса, который назы­
вается переопределением метода (method overriding). Чтобы пересмотреть текущий ди­
зайн, нужно понять значение ключевых слов virtual и override. Если базовый класс
желает определить метод, который может быть (но не обязательно) переопределен в
подклассе, он должен пометить его ключевым словом virtual:
246
Часть II. Главные конструкции программирования на C#
partial class Employee
{
// Этот метод теперь может быть переопределен производным классом.
public virtual void GiveBonus(float amount)
{
currPay += amount;
}
На заметку! Методы, помеченные ключевым словом virtual, называются виртуальными
методами.
Когда класс желает изменить реализацию деталей виртуального метода, он делает это с
помощью ключевого слова override. Например, Salesperson и Manager могли бы переоп­
ределить GiveBonus (), как показано ниже (предполагая, что PTSalesPerson не будет пере­
определять GiveBonus (), а потому просто наследует версию, определенную Salesperson):
class Salesperson : Employee
// Бонус продавца зависит от количества продаж.
public override void GiveBonus(float amount)
{
int salesBonus = 0;
if (numberOfSales >= 0 && numberOfSales <= 100)
salesBonus = 10;
else
{
if (numberOfSales >= 101 && numberOf Sales <= 200)
salesBonus = 15;
else
salesBonus = 20;
}
base.GiveBonus(amount * salesBonus);
}
}
class Manager : Employee
public override void GiveBonus(float amount)
{
base.GiveBonus(amount);
Random r = new Random () ;
numberOfOptions += r.Next (500);
}
}
Обратите внимание на использование каждым переопределенным методом поведе­
ния по умолчанию через ключевое слово base. Таким образом, полностью повторять
реализацию логики GiveBonus () вовсе не обязательно, а вместо этого можно повторно
использовать (и, возможно, расширять) поведение по умолчанию родительского класса.
Также предположим, что текущий метод DisplayStatus () класса Employee объявлен
виртуальным. При этом каждый подкласс может переопределять этот метод в расчете
на отображение количества продаж (для продавцов) и текущих опционов на акции (для
менеджеров). Например, рассмотрим версию метода DisplayStatus () в классе Manager
(класс Salesperson должен реализовать DisplayStatus () аналогичным образом, чтобы
вывести на консоль количество продаж):
Глава 6. Понятия наследования и полиморфизма
247
public override void DisplayStats ()
{
base.DisplayStats();
Console.WnteLine ("Number of Stock Options: {0}", numberOf Options);
}
Теперь, когда каждый подкласс может интерпретировать, что именно эти виртуаль­
ные методы означают для него, каждый экземпляр объекта ведет себя как более неза­
висимая сущность:
static void Main(string [] args)
{
Console.WnteLine ("***** The Employee Class Hierarchy *****\n ");
// Лучшая система бонусов!
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000);
chucky.GiveBonus(300);
chucky.DisplayStats();
Console.WriteLine();
Salesperson fran = new Salesperson("Fran", 43, 93, 3000, "932-32-3232", 31);
fran.GiveBonus(200);
fran.DisplayStats();
Console.ReadLine();
}
Ниже показан результат тестового запуска приложения в нынешнем виде:
***** The Employee Class Hierarchy *****
Name: Chucky
ID: 92
Age: 50
Pay: 100300
SSN: 333-23-2322
Number of Stock Options: 9337
Name: Fran
ID: 93
Age: 43
Pay: 5000
SSN: 932-32-3232
Number of Sales: 31
Переопределение виртуальных членов в Visual Studio 2010
Как вы уже, возможно, заметили, при переопределении члена класса необходимо
помнить типы всех параметров, а также соглашения о передаче параметров (ref, out и
params). В Visual Studio 2010 имеется очень полезное средство, которое можно использо­
вать при переопределении виртуального члена. Если набрать слово override внутри кон­
текста типа класса (и нажать клавишу пробела), то IntelliSense автоматически отобразит
список всех переопределяемых членов родительского класса, как показано на рис. 6.6.
После выбора члена и нажатия клавиши <Enter> среда IDE реагирует автоматиче­
ским заполнением шаблона метода вместо вас. Обратите внимание, что также добавля­
ется оператор кода, который вызывает родительскую версию виртуального члена (эту
строку можно удалить, если она не нужна). Например, при использовании этой техники
во время переопределения метода DisplayStatus () добавится следующий автоматиче­
ски сгенерированный код:
public override void DisplayStats ()
{
base.DisplayStats () ;
248
Часть II. Главные конструкции программирования на C#
Рис. 6.6 . Быстрый просмотр переопределяемых методов в Visual Studio 2010
Запечатывание виртуальных членов
Вспомните, что ключевое слово sealed применяется к типу класса для предотвра­
щения расширения другими типами его поведения через наследование. Ранее класс
PTSalesPerson был запечатан на основе предположения, что разработчикам не имеет
смысла дальше расширять эту линию наследования.
Иногда требуется не запечатывать класс целиком, а просто предотвратить пере­
определение некоторых виртуальных методов в производных типах. Например, пред­
положим, что продавцы с частичной занятостью не должны получать определенные
бонусы. Чтобы предотвратить переопределение виртуального метода GiveBonus() в
классе PTSalesPerson, можно запечатать этот метод в классе Salesperson следующим
образом:
//
S a le s p e r s o n
зап е ч ат ал м етод G iv e B o n u s ()1
class Salesperson : Employee
{
public override sealed void GiveBonus(float amount)
{
}
}
Здесь Salesperson действительно переопределяет виртуальный метод GiveBonus(),
определенный в классе Employee, однако он явно помечен как sealed. Поэтому попыт­
ка переопределения этого метода в классе PTSalesPerson приведет к ошибке во время
компиляции:
sealed class PTSalesPerson : Salesperson
{
public PTSalesPerson(string fullName, int age, int empID,
float currPay, string ssn, int numbOfSales)
:base (fullName, age, empID, currPay, ssn, numbOfSales)
{
}
Глава 6. Понятия наследования и полиморфизма
249
// Ошибка! Этот метод переопределять нельзя1
public override void GiveBonus(float amount)
}
Абстрактные классы
В настоящее время базовый класс Employee спроектирован так, что поставляет раз­
личные данные-члены своим наследникам, а также предлагает два виртуальных метода
(GiveBonus() и DisplayStatus()), которые могут быть переопределены наследниками.
Хотя все это хорошо и замечательно, у данного дизайна есть один неприятный побоч­
ный эффект: можно непосредственно создавать экземпляры базового класса Employee:
// Что это будет означать?
Employee X = new Employee() ;
В нашем примере базовый класс Employee имеет единственное назначение — опре­
делить общие члены для всех подклассов. По всем признакам вы не намерены позво­
лять кому-либо создавать прямые экземпляры этого класса, поскольку тип Employee
слишком общий по своей природе. Например, если кто-то скажет: “Я сотрудник!”, то тут
же возникнет вопрос: “Какой конкретно сотрудник?” (консультант, инструктор, админи­
стративный работник, редактор, советник в правительстве и т.п.).
Учитывая, что многие базовые классы склонны быть довольно неопределенными
сущностями, намного лучший дизайн для рассматриваемого примера не должен разре­
шать непосредственное создание в коде нового объекта Employee. В C# можно добиться
этого с использованием ключевого слова abstract в определении класса, создавая, та­
ким образом, абстрактный базовый класс:
П р е в р а щ е н и е класса Employee в абстрактный
// для предотвращения прямого создания экземпляров.
abstract partial class Employee
/ /
}
После этого попытка создать экземпляр класса Employee приведет к ошибке во вре­
мя компиляции:
// Ошибка! Нельзя создавать экземпляр абстрактного класса!
Employee X = new Employee();
На первый взгляд может показаться очень странным, зачем определять класс, экзем­
пляр которого нельзя создать непосредственно. Однако вспомните, что базовые классы
(абстрактные или нет) очень полезны тем, что содержат общие данные и общую функ­
циональность унаследованных типов. Используя эту форму абстракции, можно также
моделировать общую “идею” сотрудника, а не обязательно конкретную сущность. Также
следует понимать, что хотя непосредственно создать абстрактный класс нельзя, он все
же присутствует в памяти, когда создан экземпляр его производного класса. Таким об­
разом, совершенно нормально (и принято) для абстрактных классов определять любое
количество конструкторов, вызываемых опосредованно при размещении в памяти эк­
земпляров производных классов.
Теперь получилась довольно интересная иерархия сотрудников. Позднее в этой гла­
ве, при рассмотрении правил приведения типов С#, мы добавим немного больше функ­
циональности к этому приложению. А пока на рис. 6.7 показан основной дизайн типов
на данный момент.
Исходный код. Проект Employees доступен в подкаталоге Chapter 6.
250
Часть II. Главные конструкции программирования на C#
Полиморфный интерфейс
Когда класс определен как абстрактный базовый (с помощью ключевого слова
abstract), в нем может определяться любое количество абстрактных членов. Абстрактные
члены могут использоваться везде, где необходимо определить член, которые не предла­
гает реализации по умолчанию. За счет этого вы навязываете полиморфный интерфейс
каждому наследнику, возлагая на них задачу реализации конкретных деталей абстракт­
ных методов. Полиморфный интерфейс абстрактного базового класса просто ссылается на
его набор виртуальных и абстрактных методов. На самом деле это интереснее, чем может
показаться на первый взгляд, поскольку данная особенность ООП позволяет строить легко
расширяемое и гибкое программное обеспечение. Для иллюстрации реализуем (и слегка
модифицируем) иерархию фигур, кратко описанную в главе 5 при обзоре принципов ООП.
Для начала создадим новый проект консольного приложения C# по имени Shapes.
Обратите внимание на рис. 6.8, что типы Hexagon и Circle расширяют базовый
класс Shape. Подобно любому базовому классу, в Shape определен набор членов (в дан­
ном случае свойство PetName и метод Draw()), общих для всех наследников.
Подобно иерархии классов сотрудников, нужно запретить непосредственное созда­
ние экземпляров Shape, поскольку этот тип представляет слишком абстрактную кон­
цепцию. Чтобы предотвратить прямое создание экземпляров Shape, можно определить
его как абстрактный класс. Также, учитывая, что производные типы должны уникаль­
ным образом реагировать на вызов метода Draw(), давайте пометим его как virtual и
определим реализацию по умолчанию.
Employer
я
A b stract Class
: в Fields
I ^ P ro p e rtie s
• ® M e th o d s
13 N e s te d T y pes
B e n e fr tP a c k a g e
A
Class
71 M e th o d s
¥
C o m p u te P a y D e d u c tio n
j - N e s te d Ty p e s
T
m e e D C ir d e
Class
Circle
Рис. 6.7.
Иерархия классов E m p l o y e e
Рис. 6.8.
Иерархия классов фигур
у
Глава 6. Понятия наследования и полиморфизма
251
// Абстрактный базовый класс иерархии.
abstract class Shape
{
public Shape(string name = "NoName")
{ PetName - name; }
public string PetName { get; set; }
// Единственный виртуальный метод,
public virtual void Draw()
{
Console.WriteLine("Inside Shape.Draw()");
Обратите внимание, что виртуальный метод Draw() предоставляет реализацию по
умолчанию, которая просто выводит ан консоль сообщение, информирующее о том, что
вызван метод Draw() базового класса Shape. Теперь вспомните, что когда метод поме­
чен ключевым словом virtual, он предоставляет реализацию по умолчанию, которую
автоматически наследуют все производные типы. Если дочерний класс так решит, он
может переопределить такой метод, но он не обязан это делать. Учитывая это, рас­
смотрим следующую реализацию типов Circle и Hexagon:
// C irc le не переопределяет D ra w ().
class Circle : Shape
{
public Circle () {}
public Circle(string name) : base(name){}
}
// Hexagon переопределяет D ra w ().
class Hexagon : Shape
{
public Hexagon () {}
public Hexagon(string name) : base(name){}
public override void Draw()
{
Console.WriteLine("Drawing {0} the Hexagon", PetName);
}
Польза от абстрактных методов становится совершенно ясной, как только вы запом­
ните, что подклассы никогда не обязаны переопределять виртуальные методы (как в
случае Circle). Поэтому если создать экземпляр типа Hexagon и Circle, обнаружит­
ся, что Hexagon знает, как правильно “рисовать” себя (или, по крайней мере, выводит
на консоль соответствующее сообщение). Однако реакция Circle слегка приведет в
замешательство:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Polymorphism *****\n");
Hexagon hex = new Hexagon("Beth");
hex.Draw();
Circle cir = new Circle("Cindy");
// Вызывает реализацию базового класса1
cir.Draw();
Console.ReadLine();
}
Вывод этого метода Main() выглядит следующим образом:
***** pun W1th Polymorphism *****
Drawing Beth the Hexagon
Inside Shape.Draw ()
252
Часть II. Главные конструкции программирования на C#
Ясно, что это не особо интеллектуальный дизайн для текущей иерархии. Чтобы
заставить каждый класс переопределить метод Draw(), можно определить Draw() как
абстрактный метод класса Shape, а это означает отсутствие какой-либо реализации
по умолчанию. Для пометки метода как абстрактного в C# служит ключевое слово
a b s t r a c t . Не забывайте, что абстрактные методы не предусматривают вообще ника­
кой реализации:
abstract class Shape
{
// Вынудить все дочерние классы определить свою визуализацию.
public abstract void Draw();
На заметку! Абстрактные методы могут определяться только в абстрактных классах. Попытка по­
ступить иначе приводит к ошибке во время компиляции.
Методы, помеченные как abstract, являются чистым протоколом. Они просто оп­
ределяют имя, возвращаемый тип (если есть) и набор параметров (при необходимости).
Здесь абстрактный класс Shape информирует типы-наследники о том, что у него есть
метод по имени Draw(), который не принимает аргументов и ничего не возвращает.
О необходимых деталях должен позаботиться наследник.
С учетом этого метод Draw() в классе Circle теперь должен быть обязательно пере­
определен. В противном случае Circle также должен быть абстрактным типом и осна­
щен ключевым словом abstract (что очевидно не подходит в данном примере). Ниже
показаны необходимые изменения в коде:
// Если не реализовать здесь абстрактный метод D ra w (), то C irc le также
// должен считаться абстрактным, и тогда должен быть помечен как abstract!
class Circle : Shape
{
public Circle () {}
public Circle(string name) : base(name) {}
public override void Draw()
{
Console .WnteLine ("Drawing {0} the Circle", PetName) ;
}
Выражаясь кратко, теперь делается предположение о том, что любой унаследован­
ный от Shape класс должен иметь уникальную версию метода Draw(). Для демонстра­
ции полной картины полиморфизма рассмотрим следующий код:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Polymorphism *****\n");
// Создать массив совместимых c Shape объектов.
Shape[] myShapes = {new Hexagon (), new Circle(), new Hexagon("Mick"),
new Circle("Beth"), new Hexagon("Linda")};
// Пройти циклом no всем элементам и взаимодействовать
/ / с полиморфным интерфейсом.
foreach (Shape s in myShapes)
{
s .Draw ();
}
Console.ReadLine ();
Глава 6. Понятия наследования и полиморфизма
253
Ниже показан вывод этого метода Main():
***** Fun with Polymorphism *****
Drawing
Drawing
Drawing
Drawing
Drawing
NoName the Hexagon
NoNapie the Circle
Mick the Hexagon
Beth the Circle
Linda the Hexagon
Этот метод Main () иллюстрирует использование полиморфизма в чистом виде. Хотя
невозможно напрямую создавать экземпляры абстрактного базового класса (Shape),
можно свободно сохранять ссылки на объекты любого подкласса в абстрактной базовой
переменной. Таким образом, созданный массив объектов Shape может хранить объек­
ты, унаследованные от базового класса Shape (попытка поместить в массив объекты,
несовместимые с Shape, приводит к ошибке во время компиляции).
Учитывая, что все элементы в массиве my Shapes действительно наследуются от
Shape, известно, что все они поддерживают один и тот же полиморфный интерфейс
(или, говоря конкретно — все они имеют метод Draw()). Выполняя итерацию по масси­
ву ссылок Shape, исполняющая система сама определяет, какой конкретный тип имеет
каждый его элемент. И в этот момент вызывается корректная версия метода Draw(,).
Эта техника также делает очень простой и безопасной задачу расширения текущей
иерархии. Например, предположим, что от абстрактного базового класса Shape унас­
ледовано еще пять классов (T r ia n g le , Square и т.д.). Благодаря полиморфному интер­
фейсу, код внутри цикла fo re a c h не потребует никаких изменений, если компилятор
увидит, что в массив myShapes помещены только Shape-совместимые типы.
Сокрытие членов
Язык C# предоставляет средство, логически противоположное переопределению ме­
тодов, которое называется сокрытием (shadowing). Выражаясь формально, если про­
изводный класс определяет член, который идентичен члену, определенному в базовом
классе, то производный класс скрывает родительскую версию. В реальном мире такая
ситуация чаще всего возникает при наследовании от класса, который создавали не вы
(и не ваша команда), например, в случае приобретения пакета программного обеспече­
ния .NET у независимого поставщика.
Для иллюстрации предположим, что вы получили от коллеги класс по им е­
ни T h r e e D C itc le , в котором определен метод по имени Draw(), не принимающий
аргументов:
class ThreeDCircle
{
public void Draw()
{
Console .WnteLine ("Drawing a 3D Circle");
}
}
Вы обнаруживаете, что T h re e D C irc le “является” C ir c le , поэтому наследуете его от
существующего типа C ir c le :
class ThreeDCircle : Circle
{
public void Draw()
{
Console .WnteLine ("Drawing a 3D Circle");
}
}
254
Часть II. Главные конструкции программирования на C#
После компиляции в окне ошибок Visual Studio 2010 появляется предупреждение
(рис. 6.9).
Рис. 6 .9 . Мы только что скрыли член родительского класса
Проблема в том, что в производном классе (ThreeDCircle) присутствует метод, иден­
тичный унаследованному методу. Точное предупреждение компилятора в этом случае
будет таким:
'jShapes .ThreeDCircle .Draw () ' hides inherited member 'Shapes .Circle .Draw ()' .
To make the current member override that implementation, add the override
keyword. Otherwise add the new keyword.
' Shapes. T h r e e D C ir c le . Draw () ' скрывает унаследованный член 'Shapes. C i r c l e . Draw () '.
Чтобы заставить текущий член переопределить эту реализацию , добавьте ключевое
сл о в о o v e r r id e . В противном случае добавьте ключевое сл о во new.
Существует два способа решения этой проблемы. Можно просто обновить родитель­
скую версию Draw (), используя ключевое слово override (как рекомендует компиля­
тор). При таком подходе тип ThreeDCircle может расширять родительское поведение
по умолчанию, как и требовалось. Однако если доступ к коду, определяющему базовый
класс, отсутствует (как обычно случается с библиотеками от независимых поставщи­
ков), то нет возможности модифицировать метод Draw(), сделав его виртуальным.
В качестве альтернативы можно добавить ключевое слово new в определение члена
Draw () производного типа (ThreeDCircle в данном случае). Делая это явно, вы уста­
навливаете, что реализация производного типа преднамеренно спроектирована так,
чтобы игнорировать родительскую версию (в реальном проекте это может помочь, если
внешнее программное обеспечение .NET каким-то образом конфликтует с вашим про­
граммным обеспечением).
// Это класс расширяет Circle и скрывает унаследованный метод Draw().
class ThreeDCircle : Circle
{
// Скрыть любую реализацию Draw() , находящуюся выше в иерархии.
public new void Draw О
{
Console.WriteLine("Drawing a 3D Circle");
}
}
Можно также применить ключевое слово new к любому члену типа, унаследованному
от базового класса (полю, константе, статическому члену или свойству). В качестве еще
одного примера предположим, что ThreeDCircle () желает скрыть унаследованное поле
shapeName:
// Этот класс расширяет C irc le и скрывает унаследованный метод Draw() .
class ThreeDCircle : Circle
Глава 6. Понятия наследования и полиморфизма
255
// Скрыть поле shapeName, определенное выше в иерархии.
protected new string shapeName;
// Скрыть любую реализацию Draw() , находящуюся выше в иерархии.
public new void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}
И, наконец, имейте в виду, что всегда можно обратиться к реализации базового
класса скрытого члена, используя явное приведение (описанное в следующем разделе).
Например, это демонстрируется в следующем коде:
static void Main(string [] args)
// Здесь вызывается метод Draw() из класса ThreeDCircle.
ThreeDCircle о = new ThreeDCircle();
о .Draw ();
// Здесь вызывается метод Draw() родителя!
((Circle)о) .Draw ();
Console.ReadLine();
Исходный код. Проект Shapes доступен в подкаталоге C h apter 6.
Правила приведения к базовому
и производному классу
Теперь, когда вы научились строить семейства взаимосвязанных типов классов, сле­
дует познакомиться с правилами, которым подчиняются операции приведения классов.
Для этого вернемся к иерархии классов Employee, созданной ранее в главе. На платфор­
ме .NETконечным базовым классом служит System .O bject. Поэтому все, что создается,
“является” O b ject и может трактоваться как таковой. Учитывая этот факт, в объектной
переменной можно хранить ссылку на экземпляр любого типа:
void CastingExamples ()
{
// Manager "является" System.Object, поэтому можно сохранять
// ссылку на Manager в переменной типа o bject.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);
}
В примере Em ployees типы Manager, S a le s p e rs o n и P T S a lesP e rso n расширяют
класс Employee, поэтому можно хранить любой из этих объектов в допустимой ссылке
на базовый класс. Это значит, что следующий код также корректен:
void CastingExamples ()
{
// Manager "является" System.Object, поэтому можно сохранять
// ссылку на Manager в переменной типа object.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);
// Manager также "является" Employee.
Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1);
// PTSalesPerson "является" Salesperson.
Salesperson Jill = new PTSalesPerson ("Jill", 834, 3002, 100000, "111-12-1119", 90);
}
256
Часть II. Главные конструкции программирования на C#
Первое правило приведения между типами классов гласит, что когда два класса свя­
заны отношением “является”, всегда можно безопасно сохранить производный тип в
ссылке базового класса. Формально это называется неявным приведением, поскольку
оно “просто работает” в соответствии с законами наследования. Это делает возможным
построение некоторых мощных программных конструкций. Например, предположим,
что в текущем классе Program определен новый метод:
static void GivePromotion(Employee emp)
{
// Повысить зарплату...
// Предоставить место на парковке компании. ..
Console .WnteLine (" {0 } was promoted!", emp. Name);
}
Поскольку этот метод принимает единственный параметр типа Employee, можно эф­
фективно передавать этому методу любого наследника от класса Employee, учитывая
отношение “является”:
static void CastingExamples ()
{
// Manager "является" System.Object, поэтому можно сохранять
// ссылку на Manager в переменной типа object.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5) ;
// Manager также "является" Employee.
Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1);
GivePromotion(moonUnit);
// PTSalesPerson "является" Salesperson.
Salesperson jill = new PTSalesPerson ("Jill", 834, 3002, 100000, "111-12-1119", 90);
GivePromotion(jill);
Предыдущий код компилируется, благодаря неявному приведению от типа базового
класса (Employee) к производному классу. Однако что если нужно также вызвать метод
G iveP ro m o tio n () для объекта fra n k (хранимого в данный момент в обобщенной ссылке
System .O bject)? Если вы передадите объект fran k непосредственно в G iveProm otion (),
как показано ниже, то получите ошибку во время компиляции:
// Ошибка!
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);
GivePromotion(frank);
Проблема в том, что предпринимается попытка передать переменную, которая яв­
ляется не Employee, а более общим объектом S ystem .O bject. Поскольку он находится
выше в цепочке наследования, чем Employee, компилятор не допустит неявного приве­
дения, стараясь обеспечить максимально возможную безопасность типов.
Несмотря на то что вы можете определить, что объектная ссылка указывает на
Em ployee-совместимый класс в памяти, компилятор этого сделать не может, посколь­
ку это не будет известно вплоть до времени выполнения. Чтобы удовлетворить компи­
лятор, понадобится выполнить явное приведение. Второе правило приведения гласит:
необходимо явно выполнять приведение “вниз”, используя операцию приведения С#.
Базовый шаблон, которому нужно следовать при выполнении явного приведения, вы­
глядит примерно так:
( Класс_к_которому_нужно_привести) существующаяСсылка
Таким образом, чтобы передать переменную o b je c t методу G iveP ro m o tio n (), потре­
буется написать следующий код:
// Корректно!
GivePromotion((Manager)frank);
Глава 6. Понятия наследования и полиморфизма
257
Ключевое слово a s
Помните, что явное приведение происходит во время выполнения а не во время ком­
пиляции. Поэтому показанный ниже код:
// Нет! Приводить frank к типу Hexagon нельзя, хотя код скомпилируется!
Hexagon hex = (Hexagon)frank;
компилируется нормально, но вызывает ошибку времени выполнения, или, более фор­
мально — исключение времени выполнения В главе 7 будут рассматриваться подробно­
сти структурированной обработки исключений, а пока следует лишь отметить, что при
выполнении явного приведения можно перехватывать возможные ошибки приведения,
применяя ключевые слова try и catch (см. главу 7):
// Перехват возможной ошибки приведения.
try
{
Hexagon hex = (Hexagon)frank;
}
catch (InvalidCastException ex)
{
Console .WnteLine (ex.Message) ;
}
Хотя это хороший пример защитного (defensive) программирования, C# предоставля­
ет ключевое слово as для быстрого определения совместимости одного типа с другим во
время выполнения. С помощью ключевого слова as можно определить совместимость,
проверив возвращенное значение на равенство null. Взгляните на следующий код:
// Использование аз для проверки совместимости.
Hexagon hex2 = frank as Hexagon;
if (hex2 == null)
Console .WnteLine ("Sorry, frank is not a Hexagon...");
Ключевое слово i s
Учитывая, что метод GivePromotionO был спроектирован для приема любого воз­
можного типа, производного от Employee, может возникнуть вопрос — как этот метод
может определить, какой именно производный тип был ему передан? И, кстати, если
входной параметр имеет тип Employee, как получить доступ к специализированным
членам типов Salesperson и Manager?
В дополнение к ключевому слову as, в C# предлагается ключевое слово is, кото­
рое позволяет определить совместимость двух типов. В отличие от ключевого слова
as, если типы не совместимы, ключевое слово is возвращает false, а не n u ll-ссылку.
Рассмотрим следующую реализацию метода GivePromotionO:
static void GivePromotion(Employee emp)
{
Console.WnteLine (" {0 } was promoted!", emp.Name);
if (emp is Salesperson)
{
Console .WnteLine ("{ 0 } made {1} sale(s) !", emp.Name,
((Salesperson)emp) .SalesNumber);
Console.WnteLine () ;
}
if (emp is Manager)
{
Console .WnteLine ("{ 0 } had {1} stock options ...", emp.Name,
((Manager)emp).StockOptions);
258
Часть II. Главные конструкции программирования на C#
Console.WriteLine();
}
}
Здесь во время выполнения производится проверка, на что именно в памяти указы­
вает ссылка типа базового класса. Определив, что примят Salesperson или Manager,
можно применить явное приведение и получить доступ к специализированным членам
класса. Также обратите внимание, что окружать операции приведения конструкцией
try/catch не обязательно! поскольку внутри контекста if, выполнившего проверку ус­
ловия, уже известно, что приведение безопасно.
Родительский главный класс S y s te m .O b je c t
В завершение этой главы исследуем детали устройства родительского главного клас­
са всей платформы .NET — Object. Возможно, вы уже заметили в предыдущих разде­
лах, что базовые классы всех иерархий (Car, Shape, Employee) никогда явно не указы­
вали свои родительские классы:
// Кто родитель Саг?
class Саг
{...}
В мире .NET каждый тип в конечном итоге наследуется от базового класса по имени
System.Object (который в C# может быть представлен ключевым словом object). Класс
Object определяет набор общих членов для каждого типа в каркасе. Фактически, при
построении класса, который явно не указывает своего родителя, компилятор автома­
тически наследует его от Object. Если нужно очень четко прояснить свои намерения,
можно определить класс, производный от Object, следующим образом:
// Явное наследование класса от System.Object.
class Car : object
{ . .. }
Как и в любом другом классе, в System.Object определен набор членов. В следую­
щем формальном определении C# обратите внимание, что некоторые из этих членов
определены как virtual, а это говорит о том, что данный член может быть переопре­
делен в подклассе, в то время как другие помечены как static (и потому вызываются
только на уровне класса):
public class Object
{
// Виртуальные члены.
public virtual bool Equals(object obj );
protected virtual void Finalize();
public virtual int GetHashCode();
public virtual string ToStringO;
// Уровень экземпляра, не виртуальные члены.
public Type GetTypeO;
protected object MemberwiseClone ();
// Статические члены.
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
}
В табл. 6.1 приведен перечень функциональности, предоставляемой некоторыми
часто используемыми методами.
Глава 6. Понятия наследования и полиморфизма
259
Таблица 6.1. Основные методы S y s te m .O b je c t
Метод экземпляра
Назначение
Equals ()
По умолчанию этот метод возвращает true, только если сравнивае­
мые элементы ссылаются в точности на один и тот же объект в памя­
ти. Таким образом, Equals () используется для сравнения объектных
ссылок, а не состояния объекта. Обычно этот метод переопределяет­
ся, чтобы возвращать true, только если сравниваемые объекты име­
ют одинаковые значения внутреннего состояния.
Следует отметить, что в случае переопределения Equals () потребу­
ется также переопределить метод GetHashCodeO, потому что эти
методы используются внутренне типами Hashtable для извлечения
подобъектов из контейнера.
Также вспомните из главы 4, что в классе ValueType этот метод пе­
реопределен для всех структур, чтобы он работал для сравнения на
базе значений
Finalize ()
На данный момент можно считать, что этот метод (будучи переопре­
деленным) вызывается для освобождения любых размещенных ре­
сурсов перед удалением объекта. Сборка мусора CLR более подробно
рассматривается в главе 8
GetHashCodeO
Этот метод возвращает значение int, идентифицирующее конкрет­
ный экземпляр объекта
ToStringO
Этот метод возвращает строковое представление объекта, используя
формат Пространство имен>.<имя типа> (так называемое
полностью квалифицированное имя). Этот метод часто переопреде­
ляется в подклассе для возврата строки, состоящей из пар “имя/значение” , которая представляет внутреннее состояние объекта, вместо
полностью квалифицированного имени
GetTypeO
Этот метод возвращает объект Туре, полностью описывающий объект,
на который в данный момент производится ссылка. Коротко говоря,
это метод идентификации типа во время выполнения (Runtime Туре
Identification — RTTI), доступный всем объектам (подробно обсуждается
в главе 15)
MemberwiseClone ()
Этот метод возвращает полную (почленную) копию текущего объекта и
часто используется для клонирования объектов (см. главу 9)
Чтобы проиллюстрировать поведение по умолчанию, обеспечиваемое базовым клас­
сом Object, создадим новое консольное приложение C# по имени ObjectOverrides.
Добавим в проект новый тип класса С#, содержащий следующее пустое определение
типа по имени Person:
// Помните: Person расширяет Object.
class Person {}
Теперь дополним метод Main() взаимодействием с унаследованными членами
System.Object, как показано ниже:
class Program
{
static void Main(string[] args)
Console.WnteLine ("***** Fun with System.Object *****\n");
Person pi = new Person ();
// Использовать унаследованные члены System.Object.
Console.WriteLine("ToString: {0}", p i .ToString ());
260
Часть II. Главные конструкции программирования на C#
Console.WriteLine ("Hash code: {0}", p i .GetHashCode());
Console.WnteLine ("Type : {0}", p i .GetType () );
// Создать другую ссылку на p i.
Person p2 = pi;
object о = p2;
// Указывают ли ссылки на один и тот же объект в памяти?
if (о.Equals (pi) && р 2 .Equals (о))
{
Console.WriteLine("Same instance1"); // один и тот же экземпляр
}
Console.ReadLine ();
}
Вывод этого метода Main() выглядит следующим образом:
***** Fun with System.Object *****
ToStnng: ObjectOverndes .Person
Hash code: 46104728
Type: ObjectOverndes .Person
Same instance!
Первым делом, обратите внимание, что реализация T o S t n n g () по умолчанию воз­
вращает полностью квалифицированное имя текущего типа (ObjectOverrides.Person).
Как будет показано позже, при рассмотрении построения специальных пространств
имен в главе 14, каждый проект C# определяет “корневое пространство имен”, назва­
ние которого совпадает с именем проекта. Здесь мы создали проект под названием
ObjectOverrides, поэтому тип Person (как и класс Program) помещен в пространство
имен ObjectOverrides.
Поведение Equals () по умолчанию заключается в проверке того, указывают ли
две переменных на один и тот же объект в памяти. Здесь создается новая переменная
Person по имени pi. В этот момент новый объект Person помещается в память управ­
ляемой кучи (managed heap). Переменная р2 также относится к типу Person. Однако вы
не создаете новый экземпляр, а вместо этого присваиваете этой переменной ссылку pi.
Таким образом, pi и р2 указывают на один и тот же объект в памяти, как и переменная
о (типа object). Учитывая, что pi, р2 и о указывают на одно и то же местоположение в
памяти, проверка эквивалентности дает положительный результат.
Хотя готовое поведение System.Object во многих случаях может удовлетворять всем
потребностям, довольно часто специальные типы переопределяют некоторые из этих
унаследованных методов. Для иллюстрации модифицируем класс Person, добавив не­
которые свойства, представляющие имя, фамилию и возраст лица; все они могут быть
установлены с помощью специального конструктора:
// Помните: Person расширяет Object.
class Person
{
public
public
public
public
string FirstName { get; set; }
string LastName { get; set; }
int Age { get; set; }
Person(string fName, string IName, int personAge)
{
FirstName = fName;
LastName = IName;
Age = personAge;
}
public Person () {}
Глава 6. Понятия наследования и полиморфизма
261
Переопределение S y s t e m . O b j e c t . T o S t r i n g O
Многие создаваемые классы (и структуры) выигрывают от переопределения
T o S trin gO для возврата строки с текстовым представлением текущего состояния эк­
земпляра типа. Помимо прочего, это может быть довольно полезно при отладке. Как
вы решите конструировать эту строку — дело персонального вкуса; однако рекомендо­
ванный подход состоит в разделении двоеточиями пар “имя/значение” и взятии всей
строки в квадратные скобки (многие типы из библиотек базовых классов .NETT следуют
этому принципу). Рассмотрим следующую переопределенную версию T o S tr in g O для
нашего класса Person:
public override string ToStringO
{
string myState;
myState = string.Format("[First Name: {0}; Last Name: {1}; Age: {2}]",
FirstName, LastName, Age);
return myState;
}
Эта реализация T o S tr in g O довольно прямолинейна, учитывая, что класс Person
состоит всего их трех фрагментов данных состояния. Однако всегда нужно помнить,
что правильное переопределение T o S trin g O должно также учитывать все данные, оп­
ределенные выше в цепочке наследования.
Когда вы переопределяете T o S trin g O для класса, расширяющего специальный ба­
зовый класс, первое, что следует сделать — получить возврат T o S trin g O от родитель­
ского класса, используя слово base. Получив строковые данные родителя, можно доба­
вить к ним специальную информацию производного класса.
Переопределение S y s t e m .Ob j e c t . E q u a l s ()
Давайте также переопределим поведение O b j e c t . E q u a l s () для работы с семан­
тикой на основе значений. Вспомните, что по умолчанию E qu als () возвращает tru e ,
только если два сравниваемых объекта ссылаются на один и тот же экземпляр объекта
в памяти. Для класса Person может быть полезно реализовать E qu als () для возврата
tru e, когда две сравниваемых переменных содержат одинаковые значения (т.е. фами­
лию, имя и возраст).
Прежде всего, обратите внимание, что входной аргумент метода E qu als () — это об­
щий S ystem .O bject. С учетом этого первое, что нужно сделать — удостовериться, что
вызывающий код действительно передал тип Person, и для дополнительной подстра­
ховки проверить, что входной параметр не является n u ll-ссылкой.
Установив, что передан размещенный Person, один подход состоит в реализации
E q u a ls() для выполнения сравнения поле за полем данных входного объекта с соот­
ветствующими данными текущего объекта:
public override bool Equals(ob]ect obj)
{
if (obj is Person && obj != null)
{
Person temp;
temp = (Person)obj;
if (temp.FirstName == this.FirstName
&& temp.LastName == this.LastName
&& temp.Age == this.Age)
{
return true;
262
Часть II. Главные конструкции программирования на C#
else
{
return false;
return false;
}
Здесь производится сравнение значения входного объекта с внутренними значения­
ми текущего объекта (обратите внимание на применение ключевого слова this). Если
имя, фамилия и возраст, записанные в двух объектах, идентичны, значит, есть два объ­
екта с одинаковыми данными, и потому возвращается true. Любые другие возможные
результаты возвратят false.
Хотя этот подход действительно работает, представьте, насколько трудоемкой была
бы реализация специального метода Equals () для нетривиальных типов, которые могут
содержать десятки полей данных. Распространенным сокращением является использо­
вание собственной реализации ToStringO. Если у класса имеется правильная реали­
зация ToStringO, которая учитывает все поля данных вверх по цепочке наследования,
можно просто сравнить строковые данные объектов:
public override bool Equals(object obj)
{
// Больше нет необходимости приводить obj к типу Person,
// поскольку у всех имеется метод ToStringO .
return obj.ToString () == this.ToString();
}
Обратите внимание, что в этом случае нет необходимости проверять входной аргу­
мент на принадлежность к корректному типу (в нашем примере — Person), поскольку
все классы в .NET поддерживают метод ToStringO . Еще лучше то, что больше не нужно
выполнять проверку равенства свойства за свойством, поскольку теперь просто прове­
ряются значения, возвращенные методом ToStringO.
Переопределение S y s t e m . O b je c t . G e t H a s h C o d e Q
Когда класс переопределяет метод Equals (), вы также обязаны переопределить реа­
лизацию по умолчанию GetHashCode (). ГЬворя упрощенно, хеш-код — это числовое
значение, представляющее объект как определенное состояние. Например, если созда­
ны две переменных string, хранящие значение Hello, они должны давать один и тот
же хеш-код. Однако если одна переменная string хранит строку в нижнем регистре
(hello), должны быть получены разные хеш-коды.
По умолчанию System.Object.GetHashCоde() использует текущее местоположение
объекта в памяти для порождения хеш-значения. Тем не менее, при построении специ­
ального типа, который нужно хранить в коллекции Hashtable (из пространства имен
System.Collect ions), этот член должен быть всегда переопределен, поскольку Hashtable
внутри вызывает Equals () HGetHashCodeO, чтобы извлечь правильный объект.
На заметку! Точнее говоря, класс System.Collections.Hashtable внутренне вызывает ме­
тод GetHashCode () для получения общего представления местоположения объекта, но по­
следующий вызов Equals () определяет точное соответствие.
Хотя мы не собираемся помещать Person в System.Collections .Hashtable, для
полноты давайте переопределим GetHashCode (). Существует немало алгоритмов, кото­
рые могут применяться для создания хеш-кода, одни из которых причудливы, а дру­
гие — не очень. В большинстве случаев можно сгенерировать значение хеш-кода, пола­
гаясь на реализацию System.String.GetHashCode().
Глава 6. Понятия наследования и полиморфизма
263
Исходя из того, что класс S t r in g уже имеет солидный алгоритм хеширования, ис­
пользующий символьные данные S tr in g для сравнения хеш-значений, если вы можете
идентифицировать часть данных полей класса, которая должна быть уникальной для
всех экземпляров (вроде номера карточки социального страхования), просто вызови­
те GetHashCode () на этой части полей данных. Поскольку в классе Person определено
свойство SSN, можно написать следующий код:
// Вернуть хеш-код на основе уникальных строковых данных.
public override int GetHashCode()
{
return this .ToStnng () .GetHashCode () ;
}
Если же выбрать уникальный строковый элемент данных затруднительно, но есть
переопределенный метод T o S tn n g O , вызовите GetHashCode() на собственном строко­
вом представлении:
// Возвращает хеш-код на основе значения ToStringO персоны,
public override int GetHashCode ()
{
return this .T o Stnng () .GetHashCode () ;
}
Тестирование модифицированного класса P e r s o n
Теперь, когда виртуальные члены O b jec t переопределены, давайте обновим M ain()
для добавления проверки внесенных изменений.
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with System.Object *****\n");
// ПРИМЕЧАНИЕ: эти объекты идентичны для проверки
// методов E quals() и GetHashCode( ) .
Person pi = new Person ("Homer", "Simpson", 50);
Person p2 = new Person("Homer", "Simpson", 50);
// Получить строковые версии объектов.
Console.WriteLine("pi.ToString () = {0}", p i .ToString ());
Console .WriteLine ("p2 .ToStnng () = {0}", p2 .ToString ()) ;
// Проверить переопределенный метод E qu als( ) .
Console.WriteLine("pi = p2?: {0}", p i .Equals(p2));
// Проверить хеш-коды.
Console.WriteLine ("Same hash codes?: {0}", pi.GetHashCode () == p2.GetHashCode ()) ;
Console.WriteLine();
// Изменить возраст p2 и проверить снова.
р 2 .Age = 45;
Console.WriteLine("pi.ToString () = {0}", p i .ToString());
Console .WriteLine ("p2 .ToString () = {0}", p 2 .ToString());
Console.WriteLine("pi =p2?: {0}", p i .Equals(p2));
Console.WriteLine("Same hash codes?: {0}", pi .GetHashCode () == p 2 .GetHashCode ()) ;
Console.ReadLine();
}
Ниже показан вывод:
***** Fun with System.Ob]ect *****
pi.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50]
p2 .ToString() = [First Name: Homer; Last Name: Simpson; Age: 50]
pi = p2?: True
Same hash codes?: True
264
Часть II. Главные конструкции программирования на C#
p i .ToString () = [First Name: Homer; Last Name: Simpson; Age: 50]
p 2 .T oStnng () = [First Name: Homer; Last Name: Simpson; Age: 45]
pi = p2?: False
Same hash codes?: False
Статические члены S y s t e m .O b je c t
В дополнение к только что рассмотренным членам уровня экземпляра, в System.
O b je c t также определены два очень полезных статических члена, которые проверяют
эквивалентность на основе значений или на основе ссылок. Рассмотрим следующий
код:
static void StaticMembersOfObject ()
{
// Статические члены System.Object.
Person рЗ = new Person("Sally", "Jones", 4);
Person p4 = new Person("Sally", "Jones", 4);
Console.WriteLine("P3 and P4 have same state: {0}", object.Equals(p3, p4) );
Console.WnteLine ("P3 and P4 are pointing to same object: {0}",
object.ReferenceEquals (p 3, p4));
}
Здесь можно просто передать два объекта (любого типа) и позволить классу System.
O b je c t автоматически определить детали. Эти методы могут быть очень полезны при
переопределении эквивалентности для специального типа, когда нужно сохранить воз­
можность быстрого выяснения того, указывают ли две ссылочных переменных на одно
и то же местоположение в памяти (через статический метод R e fe re n c e E q u a ls ()).
Исходный код. Проект O b je c tO v e r n d e s доступен в подкаталоге C hapter 6.
Резюме
В этой главе рассматривалась роль и подробности наследования и полиморфизма.
Были представлены многочисленные новые ключевые слова и лексемы для поддержки
каждой этой техники. Например, вспомните, что двоеточие применяется для установ­
ки родительского класса для заданного типа. Родительские типы могут определять лю­
бое количество виртуальных и/или абстрактных членов для установки полиморфного
интерфейса. Производные типы переопределяют эти члены, используя ключевое слово
o v e r r id e .
В дополнение к построению многочисленных иерархий классов, в главе также рас­
сматривалось явное приведение между базовым и производным типом. Кроме того,
было дано описание главного класса среди всех родительских типов библиотеки базо­
вых классов .NET — S ystem .O bject.
ГЛАВА
7
Структурированная
обработка исключений
настоящей главе речь пойдет о способах обработки аномалий, возникающих во
время выполнения, в коде на C# с применением методики так называемой струк­
турированной обработки исключений (structured exception handling — SEH). Здесь бу­
дут описаны не только ключевые слова в С#, которые предназначены для этого (t r y ,
catch , throw, f i n a l l y ) , но и отличия исключений уровня приложения и системы, а
также роль базового класса S ystem .E xcep tion . Вдобавок будет показано, как создавать
специальные исключения, и рассмотрены инструменты, доступные в Visual Studio 2010
для выполнения отладки.
В
Ода ошибкам и исключениям
Чтобы не нашептывало наше (порой раздутое) эго, ни один программист не идеа­
лен. Написание кода программного обеспечения является сложным делом, и из-за этой
сложности довольно часто даже самые лучшие программы поставляются с различными,
так сказать, проблемами. В одних случаях причиной этих проблем служит “плохо напи­
санный” код (например, в нем происходит выход за пределы массива), а в других — ввод
пользователями неправильных данных, которые не были предусмотрены в коде прило­
жения (например, приводящий к присваиванию полю для ввода телефонного номера
значения вроде “Chucky”).
Что бы ни служило причиной проблем, в конечном итоге приложение начинает ра­
ботать не так, как ожидается. Прежде чем переходить к рассмотрению структуриро­
ванной обработки исключений, давайте сначала ознакомимся с тремя наиболее часто
применяемыми для описания аномалий терминами.
• Программные ошибки (bugs). Так обычно называются ошибки, которые допуска­
ет программист. Например, предположим, что приложение создается с помощью
неуправляемого языка C++. Если динамически выделяемая память не освобожда­
ется, что чревато утечкой памяти, появляется программная ошибка.
• Пользовательские ошибки (user errors). В отличие от программных ошибок, поль­
зовательские ошибки обычно возникают из-за тех, кто запускает приложение, а
не тех, кто его создает. Например, ввод конечным пользователем в текстовом поле
неправильно оформленной строки может привести к генерации ошибки подобно­
го рода, если в коде не была предусмотрена возможность обработки некорректно­
го ввода.
266
Часть II. Главные конструкции программирования на C#
• Исключения (exceptions). Исключениями, или исключительными ситуациями,
обычно называются аномалии, которые могут возникать во время выполнения
1 и которые трудно, а порой и вообще невозможно, предусмотреть во время про­
граммирования приложения. К числу таких возможных исключений относятся
попытки подключения к базе данных, которой больше не существует, попытки
открытия поврежденного файла или попытки установки связи с машиной, кото­
рая в текущий момент находится в автономном режиме. В каждом из этих случа­
ев программист (и конечный пользователь) мало что может сделать с подобными
“исключительными” обстоятельствами.
По приведенным выше описаниям должно стать понятно, что структурированная
обработка исключений в .NET представляет собой методику, предназначенную для ра­
боты с исключениями, которые могут возникать на этапе выполнения. Даже в случае
программных и пользовательских ошибок, которые ускользнули от глаз программиста,
однако, CLR будет часто автоматически генерировать соответствующее исключение с
описанием текущей проблемы. В библиотеках базовых классов .NET определено множе­
ство различных исключений, таких как FormatException, IndexOutOfRangeException,
FileNotFoundException, ArgumentOutOfRangeException и т.д.
В терминологии .NET под “исключением” подразумеваются программные ошибки,
пользовательские ошибки и ошибки времени выполнения, несмотря на то, что мы, про­
граммисты, можем считать каждый из этих видов ошибок совершенно отдельным ти­
пом проблем. Прежде чем погружаться в детали, давайте посмотрим, какую роль игра­
ет структурированная обработка исключений, и чем она отличается от традиционных
методик обработки ошибок.
На заметку! Чтобы упростить примеры кода, абсолютно все исключения, которые может выдавать
тот или иной метод из библиотеки базовых классов, перехватываться не будут. В реальных про­
ектах следует поступать согласно существующим требованиям.
Роль обработки исключений в .NET
До появления .NET обработка ошибок в среде операционной системы Windows пред­
ставляла собой весьма запутанную смесь технологий. Многие программисты вклю­
чали собственную логику обработки ошибок в контекст интересующего приложения.
Например, команда разработчиков могла определять набор числовых констант для
представления известных сбойных ситуаций и затем применять эти константы в каче­
стве возвращаемых значений методов. Для примера рассмотрим следующий фрагмент
кода на языке С.
/* Типичный механизм отлавливания ошибок в С. */
#define E_FILENOTFOUND 1000
int SomeFunction()
{
// Предполагаем, что в этой функции происходит нечто
// такое, что приводит к возврату следующего значения.
return E_FILENOTFOUND;
}
void main()
{
int retVal = SomeFunction();
i f (retVal == E_FILENOTFOUND)
p n n t f ("Cannot find file...");
/ / H e удается найти файл...
}
Глава 7. Структурированная обработка исключений
267
Такой подход далеко не идеален из-за того факта, что константа E FILENOTFOUND
представляет собой не более чем просто числовое значение, но уж точно не агента, спо­
собного помочь в решении проблемы. В идеале хотелось бы, чтобы название ошибки,
сообщение с ее описанием и другой полезной информацией подавалось в одном удобном
пакете (что как раз и происходит в случае применения структурированной обработки
исключений).
Помимо приемов, изобретаемых самими разработчиками, в A P I-интерфейсе
Windows определены сотни кодов ошибок с помощью # define и НRESULT, а также
множество вариаций простых булевских значений (bool, BOOL, VARIANT BOOL и т.д.).
Более того, многие разработчики COM-приложений на языке C++ (а также VB 6) явно
или неявно применяют небольшой набор стандартных COM-интерфейсов (наподобие
ISupportErrorlnf о, IErrorlnfo или ICreateError Inf о) для возврата СОМ-клиенту
понятной информации об ошибках.
Очевидная проблема со всеми этими более старыми методиками — отсутствие сим­
метрии. Каждая из них более-менее вписывается в рамки какой-то одной технологии,
одного языка и, пожалуй, даже одного проекта. Чтобы положить конец всему этому бе­
зумству, в .NET была предложена стандартная методика для генерации и выявления
ошибок в исполняющей среде, называемая структурированной обработкой исключений
(SEH).
Прелесть этой методики состоит в том, что она позволяет разработчикам использо­
вать в области обработки ошибок унифицированный подход, который является общим
для всех языков, ориентированных на платформу .NET. Благодаря этому, программист
на C# может обрабатывать ошибки почти таким же с синтаксической точки зрения
образом, как и программист на VB и программист на C++, использующий C++/CLI.
Дополнительное преимущество состоит в том, что синтаксис, который требуется при­
менять для генерации и перехвата исключений за пределами сборок и машин, тоже
выглядит идентично. Например, при написании на C# службы Windows Communication
Fbundation (WCF) генерировать исключение SOAP для удаленного вызывающего кода
можно с использованием тех же ключевых слов, которые применяются для генерации
исключения внутри методов в одном и том же приложении.
Еще одно преимущество механизма исключений .NET состоит в том, что в отли­
чие от запутанных числовых значений, просто обозначающих текущую проблему, они
представляют собой объекты, в которых содержится читабельное описание проблемы,
а также детальный снимок стека вызовов на момент, когда изначально возникло исклю­
чение. Более того, конечному пользователю можно предоставлять справочную ссылку,
которая указывает на определенный URL-адрес с описанием деталей ошибки, а также
специальные данные, определенные программистом.
Составляющие процесса обработки исключений в .NET
Программирование со структурированной обработкой исключений подразумевает
использование четырех следующих связанных между собой сущностей:
• тип класса, который представляет детали исключения;
• член, способный генерировать (throw) в вызывающем коде экземпляр класса ис­
ключения при соответствующих обстоятельствах;
• блок кода на вызывающей стороне, ответственный за обращение к члену, в кото­
ром может произойти исключение;
• блок кода на вызывающей стороне, который будет обрабатывать (или перехваты­
вать (catch)) исключение в случае его возникновения.
268
Часть II. Главные конструкции программирования на C#
При генерации и обработке исключений в C# используются четыре ключевых слова
(t r y , ca tch , throw и f i n a l l y ) . Любой объект, отражающий обрабатываемую пробле­
му, должен обязательно представлять собой класс, унаследованный от базового класса
S ystem . E x c e p tio n (или от какого-то его потомка). По этой причине давайте сначала
рассмотрим роль этого базового класса в обработке исключений.
Базовый класс S y s t e m . E x c e p t i o n
Все определяемые на уровне пользователя и системы исключения в конечном ито­
ге всегда наследуются от базового класса System . E xception , который, в свою очередь,
наследуется от класса System. O b ject. Ниже показано, как в целом выглядит этот класс
(обратите внимание, что некоторые его члены являются виртуальными и, следователь­
но, могут переопределяться в производных классах):
public class Exception : ISenalizable, _Exception
{
// Общедоступные конструкторы.
public Exception(string message, Exception innerException);
public Exception(string message);
public Exception ();
// Методы.
public virtual Exception GetBaseException() ;
public virtual void GetObjectData(Serializationlnfо info,
StreamingContext context);
// Свойства.
public virtual IDictionary Data { get; }
public virtual string HelpLink { get; set; }
public Exception InnerException { get; }
public virtual string Message { get; }
public virtual string Source { get; set; }
public virtual string StackTrace { get; }
public MethodBase TargetSite { get; }
Нетрудно заметить, что многие из содержащихся в S ystem .E x cep tio n свойств яв­
ляются по своей природе доступными только для чтения. Это объясняется тем, что для
каждого из них значения, используемые по умолчанию, обычно поставляются в произ­
водных классах. Например, в производном классе IndexO utO fRangeException постав­
ляется сообщение по умолчанию “Index was outside the bounds of the array” (“Индекс
вышел за границы массива”).
На заметку! В классе E x c e p tio n реализованы два интерфейса .NET. Хотя интерфейсы подробно
рассматриваются в главе 9, сейчас главное понять, что интерфейс _ E x c e p tio n позволяет
сделать так, чтобы исключение .NET обрабатывалось неуправляемым кодом (таким как прило­
жение СОМ), а интерфейс I S e r i a l i z a b l e — чтобы объект исключения сохранялся за пре­
делами границ (например, границ машины).
В табл. 7.1 приведено краткое описание некоторых наиболее важных.свойств класса
S ystem . E x cep tio n .
Глава 7. Структурированная обработка исключений
269
Таблица 7.1. Ключевые свойства System .Exception
Свойство
Описание
Data
Это свойство, доступное только для чтения, позволяет извлекать кол­
лекцию пар "ключ/значение” (представленную объектом, реализующим
интерфейс ID ic tio n a r y ), которая предоставляет дополнительную опре­
деляемую программистом информацию об исключении. По умолчанию эта
коллекция является пустой
HelpLink
Это свойство позволяет получать или устанавливать URL-адрес, по которому
доступен справочный файл или веб-сайт с детальным описанием ошибки
InnerE xception
Это свойство, доступное только для чтения, может применяться для полу­
чения информации о предыдущем исключении или исключениях, которые
послужили причиной возникновения текущего исключения. Запись преды­
дущих исключений осуществляется путем их передачи конструктору само­
го последнего исключения
Message
Это свойство, доступное только для чтения, возвращает текстовое опи­
сание соответствующей ошибки. Само сообщение об ошибке задается в
передаваемом конструктору параметре
Source
Это свойство позволяет получать или устанавливать имя сборки или объ­
екта, который привел к выдаче исключения
S tackTrace
Это свойство, доступное только для чтения, содержит строку с описанием
последовательности вызовов, которая привела к возникновению исклю­
чения. Как нетрудно догадаться, это свойство очень полезно во время
отладки или для сохранения информации об ошибке во внешнем журнале
ошибок
T a r g e t S it e
Это свойство, доступное только для чтения, возвращает объект
MethodBase с описанием многочисленных деталей метода, который
привел к выдаче исключения (вызов вместе с ним T o S tn n g () позволяет
идентифицировать этот метод по имени)
Простейший пример
Для иллюстрации пользы от структурированной обработки исключений необходи­
мо создать класс, который будет выдавать исключение при надлежащих (или, мож­
но сказать, и с к л ю ч и т е л ь н ы х ) обстоятельствах. Создадим новый проект типа C o n s o le
A p p lica tio n (Консольное приложение) на C# по имени S im p leE xcep tion и определим в
нем два класса (Саг (автомобиль) и Radio (радиоприемник)), связав их между собой от­
ношением принадлежности (“has-a”). В классе Radio определим единственный метод,
отвечающий за включение и выключение радиоприемника:
class Radio
{
public void TurnOn(bool on)
{
if(on)
Console .WnteLine ("Jamming...") ;
else
Console .WnteLine ("Quiet time...");
// работает
// отключен
}
В классе Car (показанном ниже) помимо использования класса Radio через принад­
лежность/ делегирование, сделаем так, чтобы в случае превышения объектом Саг пре­
270
Часть II. Главные конструкции программирования на C#
допределенной максимальной скорости (отражаемой с помощью константы экземпляра
Max Speed) двигатель выходил из строя, приводя его в нерабочее состояние (отражаемое
приватной переменной экземпляра b o o l по имени ca r Is Dead). Кроме того, включим в
Саг свойства для представления текущей скорости и указанного пользователем “друже­
ственного названия” автомобиля и различные конструкторы для установки состояния
нового объекта Саг. Ниже приведено полное определение Саг вместе с поясняющими
комментариями.
public class Car
{
// Константа, отражающая допустимую максимальную скорость.
public const int MaxSpeed = 100;
// Свойства автомобиля.
public int CurrentSpeed {get; set;}
public string PetName {get; set;}
//He вышел ли автомобиль из строя?
private bool carlsDead;
/ / В автомобиле есть радиоприемник.
private Radio theMusicBox = new Radio();
// Конструкторы.
public C a r () { }
public Car(string name, int speed)
{
CurrentSpeed = speed;
PetName = name;
}
public void CrankTunes(bool state)
{
// Запрос делегата к внутреннему объекту.
theMusicBox.TurnOn(state);
}
// Проверка, не перегрелся ли автомобиль.
public void Accelerate(int delta)
{
if (carlsDead)
Console.WriteLine ("{0} is out of order...", PetName); // вышел из строя
else
{
CurrentSpeed += delta;
if (CurrentSpeed > MaxSpeed)
{
Console.WriteLine ("{0} has overheated1", PetName); // перегрелся
CurrentSpeed = 0;
carlsDead = true;
}
else
// Вывод текущей скорости.
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
}
Теперь реализуем метод Main ( ) , в котором объект Саг будет превышать заданную
максимальную скорость (установленную равной 100 в классе Саг), как показано ниже:
Глава 7. Структурированная обработка исключений
271
static void Main(string[] args)
{
Console.WriteLine ("***** Simple Exception Example *****");
Console.WriteLine("=> Creating a car and stepping on i t 1");
Car myCar = new Car ("Zippy" , 20);
myCar.CrankTunes(true);
for (int i = 0; i < 10; i++)
myCar.Accelerate(10) ;
Console.ReadLine();
}
Вьшод будет выглядеть следующим образом:
***** Simple Exception Example *****
=> Creating a car and stepping on it!
Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 7 0
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100
Zippy has overheated!
Zippy is out of order. ..
Генерация общего исключения
Имея функционирующий класс Саг, давайте рассмотрим простейший способ генера­
ции исключения. Текущая реализация A c c e le r a t e () предусматривает просто отобра­
жение сообщения об ошибке, когда предпринимается попытка разогнать автомобиль
(объект Саг) до скорости, превышающей максимальный предел.
Для изменения этого метода так, чтобы при попытке разогнать автомобиль до ско­
рости, превышающий установленный в классе Саг предел, генерировалось исключение,
потребуется создать и сконфигурировать новый экземпляр класса S y stem .E x cep tio n и
установить значение доступного только для чтения свойства Message через конструктор
класса. Чтобы объект ошибки отправлялся обратно вызывающей стороне, в C# использует­
ся ключевое слово throw. Ниже показан код модифицированного метода A c c e le r a t e ().
// На этот раз, в случае превышения пользователем указанного
// в MaxSpeed предела должно генерироваться исключение.
public void Accelerate(int delta)
{
if (carlsDead)
Console.WriteLine ("{0 } is out of order...", PetName);
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
carlsDead = true;
CurrentSpeed = 0;
// Использование ключевого слова throw для генерации исключения.
throw new Exception(string.Format("{0} has overheated!", PetName));
}
else
Console.WriteLine ("=> CurrentSpeed = {0}", CurrentSpeed);
272
Часть II. Главные конструкции программирования на C#
Прежде чем переходить к рассмотрению перехвата данного исключения в вызы­
вающем коде, необходимо отметить несколько интересных моментов. При генерации
исключения то, как будет выглядеть ошибка и когда она должна выдаваться, решает
программист. В рассматриваемом примере предполагается, что при попытке увеличить
скорость автомобиля (объекта Саг), который уже вышел из строя, должен генерировать­
ся объект S y stem .E x cep tio n для уведомления о том, что метод A c c e le r a t e () не мо­
жет быть продолжен (это предположение может оказаться как подходящим, так и нет, в
зависимости от создаваемого приложения).
В качестве альтернативы метод A c c e l e r a t e () можно было бы реализовать и так,
чтобы он производил автоматическое восстановление, не выдавая перед этим никакого
исключения. По большому счету, исключения должны генерироваться только в случае
возникновения более критичных условий (например, отсутствии нужного файла, невоз­
можности подключиться к базе данных и т.п.). Принятие решения о том, что должно
служить причиной генерации исключения, требует серьезного продумывания и поиска
веских оснований на стадии проектирования. Для преследуемых сейчас целей давайте
считать, что попытка увеличить скорость неисправного автомобиля является вполне
оправданной причиной для выдачи исключения.
Перехват исключений
Поскольку теперь метод A c c e l e r a t e () способен генерировать исключение, вызы­
вающий код должен быть готов обработать его, если оно вдруг возникнет. При вызо­
ве метода, который может генерировать исключение, должен использоваться блок
try / c a tc h . После перехвата объекта исключения можно вызывать различные его чле­
ны и извлекать детальную информацию о проблеме.
Что делать с этими деталями дальше по большей части нужно решать самостоя­
тельно. Может возникнуть желание занести их в специальный файл отчета, записать в
журнал событий Windows, отправить по электронной почте системному администрато­
ру или отобразить конечному пользователю. Давайте для простоты выведем их в окне
консоли.
// Обработка сгенерированного исключения.
static void Main(string[] args)
{
Console .WnteLine ("**** * Simple Exception Example *****");
Console.WriteLine("=> Creating a car and stepping on it!");
Car myCar = new Car ("Zippy", 20);
myCar.CrankTunes(true);
// Разгон до скорости, превышающей максимальный
// предел автомобиля, для выдачи исключения.
try
{
for(int 1 = 0; i < 10; i++)
myCar.Accelerate(10);
}
catch (Exception e)
{
Console.WriteLine ("\n*** Error! ***");
Console.WriteLine("Method: {0}", e .TargetSite);
Console.WriteLine("Message: {0}", e.Message);
Console.WriteLine ("Source: {0}", e.Source);
//
//
//
//
ошибка
метод
сообщение
источник
}
/ / Ошибка была обработана, продолжается выполнение следующего оператора.
Console.WriteLine("\n***** Out of exception logic *****");
Console.ReadLine();
}
Глава 7. Структурированная обработка исключений
273
По сути, блок t r y представляет собой раздел операторов, которые в ходе выполнения
могут выдавать исключение. Если обнаруживается исключение, управление переходит
к соответствующему блоку catch . С другой стороны, в случае, если код внутри блока
t r y не приводит к генерации исключения, блок ca tch полностью пропускается, и все
проходит “гладко”. Ниже показано, как будет выглядеть вывод в результате тестового
выполнения данной программы.
*ж*** Simple Exception Example *****
=> Creating a car and stepping on it!
Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 4 0
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
*** Error! ***
Method: Void Accelerate(Int32)
Message: Zippy has overheated!
Source: SimpleException
***** Out of exception logic *****
Как здесь видно, после обработки исключения приложение может продолжать свою
работу с того оператора, который идет сразу после блока catch . В некоторых случаях
исключение может оказаться достаточно серьезным и стать причиной для завершения
работы приложения. Чаще всего, однако, логика внутри обработчика исключений по­
зволяет приложению спокойно продолжать работу (хотя, возможно, и менее функцио­
нальным образом, например, без возможности устанавливать соединение с каким-ни­
будь удаленным источником данных).
Конфигурирование состояния исключения
В настоящий момент объект System.Exception, сконфигурированный в методе
Accelerate (), просто устанавливает значение, предоставляемое свойству Message (че­
рез параметр конструктора). Как показывалось ранее в табл. 7.1, в классе Exception
доступно множество дополнительных членов (TargetSite, StackTrace, HelpLink и
Data), которые могут помочь еще больше уточнить природу проблемы. Чтобы усовер­
шенствовать текущий пример, давайте рассмотрим возможности каждого из этих чле­
нов более подробно.
Свойство T a r g e t S i t e
Свойство System.Exception.TargetSite позволяет получать различные детали о
методе, в котором было сгенерировано данное исключение. Как было показано в пре­
дыдущем методе Main () , вывод значения свойства TargetSite приводит к отобра­
жению возвращаемого значения, имени и параметров выдавшего исключение мето­
да. Вместо простой строки свойство TargetSite возвращает строго типизированный
объект System. Ref lection .MethodBase. Объект такого типа может применяться для
сбора многочисленных деталей, связанных с проблемным методом, а также классом, в
котором он содержится. Для примера изменим предыдущую логику в блоке catch сле­
дующим образом:
static void Main(string [] args)
274
Часть II. Главные конструкции программирования на C#
// Свойство TargetSite на самом деле
// возвращает объект MethodBase.
catch (Exception е)
{
Console .WriteLme ("\n* ** Error1 ***");
Console . W n t e L m e ("Member name: {0}", e .TargetSite) ; // имя члена
Console.WriteLme ("Class defining member: {0}",
e .TargetSite .DeclanngType) ;
// класс, определяющий член
Console.WriteLme ("Member type: {0}",
e .TargetSite.MemberType);
// тип члена
Console .WriteLme ("Message: {0}", e.Message); // сообщение
Console .WriteLme ("Source : {0}", e.Source);
// источник
}
Console .WriteLme (" \n* * ** * Out of exception logic •*****");
Console.ReadLine();
На этот раз в коде с помощью свойства MethodBase .DeclaringType получается пол­
ностью определенное имя выдавшего ошибку класса (в данном случае SimpleException.
Саг), а с помощью свойства MemberType объекта MethodBase выясняется тип члена
(свойство или метод), в котором возникло исключение. Ниже показано, как теперь будет
выглядеть вывод в результате выполнения логики в блоке catch.
*** Error! ***
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car
Member type: Method
Message: Zippy has overheated!
Source: SimpleException
Свойство S t a c k T r a c e
Свойство System. Exception.StackTrace позволяет определить последовательность
вызовов, которая привела к возникновению исключения. Значение этого свойства ни­
когда самостоятельно не устанавливается — это делается автоматически во время соз­
дания исключения. Чтобы проиллюстрировать это, модифицируем логику в блоке catch
следующим образом:
catch(Exception е)
Console.WriteLme ("Stack: {0}", e .StackTrace) ; // вывод стека
}
Если теперь снова запустить программу, можно будет увидеть в окне консоли сле­
дующие данные трассировки стека (номера строк и пути к файлам, конечно же, на раз­
ных машинах выглядят по-разному):
Stack: at SimpleException.Car.Accelerate (Int32 delta)
in c :\MyApps\SimpleException\car.cs:line 65 at SimpleException.Program.Main()
in c :\MyApps\SimpleExceptmn\Program. cs :line 21
Строка, возвращаемая из StackTrace, отражает последовательность вызовов, кото­
рая привела к выдаче данного исключения. Обратите внимание, что самый нижний но­
мер в этой строке указывает на место возникновения первого вызова в последователь­
ности, а самый верхний — на место, где точно находится породивший проблему член.
Очевидно, что такая информация очень полезна при выполнении отладки или просмот­
ре журналов в конкретном приложении, поскольку позволяет прослеживать весь путь,
приведший к возникновению ошибки.
Глава 7. Структурированная обработка исключений
275
Свойство H e l p L i n k
Хотя свойства TargetSite и StackTrace позволяют программистам понять, почему
возникло то или иное исключение, пользователям выдаваемая ими информация мало
что дает. Как уже показывалось ранее, для получения удобной для человеческого вос­
приятия и потому пригодной для отображения конечному пользователю информации
может применяться свойство System.Exception .Message. Кроме него, также может
использоваться свойство HelpLink, которое позволяет направить пользователя на кон­
кретный URL-адрес или стандартный справочный файл Windows, где содержатся более
детальные сведения о возникшей проблеме.
По умолчанию значением свойства HelpLink является пустая строка. Присваивание
этому свойству какого-то более интересного значения должно делаться перед генераци­
ей исключения типа System.Exception. Чтобы посмотреть, как это делается, изменим
метод Car .Accelerate () следующим образом:
public void Accelerate (int delta)
{
if (carlsDead)
Console .WnteLine ("{ 0 } is out of order...", PetName);
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
carlsDead = true;
CurrentSpeed = 0;
// Создание локальной переменной перед
// выдачей объекта Exception для получения
// возможности о б р а щ е н и я к свойству HelpLink.
Exception ex =
new Exception(string.Format("{0} has overheated!", PetName));
ex.HelpLink = "http://www.CarsRUs.com";
throw ex;
}
else
// Вывод текущей скорости
Console .WnteLine ("= > CurrentSpeed = {0}", CurrentSpeed);
Теперь можно модифицировать логику в блоке catch так, чтобы информация из дан­
ного свойства HelpLink выводилась в окне консоли:
catch(Exception е)
// Ссылка для справки
Console.WriteLine("Help Link: {0}", e.HelpLink);
}
Свойство D a t a
Доступное в классе System.Exception свойство Data позволяет заполнять объект
исключения соответствующей вспомогательной информацией (например, датой и вре­
менем возникновения исключения). Оно возвращает объект, реализующий интерфейс
по имени IDictionary, который определен в пространстве имен System.Collections.
В главе 9 более подробно рассматривается программирование с использованием интер­
276
Часть II. Главные конструкции программирования на C#
фейсов, а также пространство имен System.Collections. На данный момент важно
понять лишь то, что коллекции типа словарей позволяют создавать наборы значений,
извлекаемых по ключу. Модифицируем метод Car .Accelerate (), как показано ниже:
public void Accelerate(int delta)
{
if (carlsDead)
Console .WnteLine ("{ 0 } is out of order...", PetName);
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
carlsDead = true;
CurrentSpeed = 0;
// Создание локальной переменной перед выдачей
// объекта Exception для обращения к свойству HelpLink.
Exception ex =
new Exception(string.Format("{0} has overheated!", PetName));
ex.HelpLink = "http://www.CarsRUs.com";
// Вставка специальных дополнительных данных,
// имеющих отношение к ошибке.
e x .Data.Add("TimeStamp",
string.Format("The car exploded at {0}", DateTime.Now));
// дата и время
e x .Data.Add("Cause", "You have a lead foot."); // причина
throw ex;
}
else
Console.WriteLine ("=> CurrentSpeed = {0}", CurrentSpeed);
}
Для успешного перечисления пар “ключ/значение” необходимо не забыть сослать­
ся на пространство имен System.Collections с помощью директивы using, посколь­
ку будет использоваться тип DictionaryEntry в файле с классом, реализующем метод
Main ():
using System.Collections;
Затем потребуется обновить логику catch так, чтобы в ней выполнялась проверка
на предмет того, не равно ли null значение свойства Data (null является значением по
умолчанию). После этого остается только воспользоваться свойствами Key и Value типа
DictionaryEntry для вывода специальных данных в окне консоли.
catch (Exception е)
//По умолчанию поле данных является пустым, поэтому
// выполняется проверка на предмет равенства n u ll.
Console .WnteLine ("\n-> Custom Data:");
if (e.Data != null)
{
foreach (DictionaryEntry de in e.Data)
Console .WnteLine ("-> {0}: {1}", de.Key, de.Value);
}
}
Ниже показано, как теперь будет выглядеть вывод программы:
Глава 7. Структурированная обработка исключений
277
***** Slmple Exception Example *****
=> Creating a car and stepping on it!
Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
CurrentSpeed = 90
*** Error! ***
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car
Member type: Method
Message: Zippy has overheated!
Source: SimpleException
Stack: at SimpleException.Car.Accelerate (Int32 delta)
at SimpleException.Program.Main(String[] args)
Help Link: http://www.CarsRUs.com
-> Custom Data:
-> TimeStamp: The car exploded at 1/12/2010 8:02:12 PM
-> Cause: You have a lead foot.
***** Out of exception logic *****
Свойство Data очень полезно, так как позволяет формировать специальную инфор­
мацию об ошибке, не прибегая к созданию совершенно нового класса, который расши­
ряет базовый класс Exception (до выхода версии .NET 2.0 это было единственным воз­
можным вариантом). Однако каким бы полезным ни было свойство Data, разработчики
.NET-приложений все равно довольно часто предпочитают создавать строго типизиро­
ванные классы исключений, в которых специальные данные обрабатываются с помо­
щью строго типизированных свойств.
При таком подходе вызывающий код получает возможность перехватывать конкрет­
ный производный от Exception тип, а не углубляться в коллекцию данных в поиске
дополнительных деталей. Чтобы понять, как это работает, сначала необходимо разо­
браться с отличиями между исключениями уровня системы и уровня приложений.
Исходный код. Проект SimpleException доступен в подкаталоге Chapter 7.
Исключения уровня системы
(System. SystemException)
В библиотеке базовых классов .NET содержится много классов, которые в конечном
итоге наследуются от System.Exception. Например, в пространстве имен System оп­
ределены ключевые классы исключений, такие как ArgumentOutOfRangeException,
IndexOutOfRangeException, StackOverflowException и т.д. В других простран­
ствах имен есть исключения, отражающие их поведение (например, в пространстве
имен System. Drawing. Printing содержатся исключения, возникающие при печати,
в System. 10 — исключения, возникающие во время ввода-вывода, в System. Data —
исключения, связанные с базами данных, и т.д.).
Исключения, которые генерируются самой платформой .NET, называются исключе­
ниями уровня системы. Эти исключения считаются неустранимыми фатальными ошиб­
ками. Они наследуются прямо от базового класса System. SystemException, который, в
свою очередь, наследуется от System.Exception (а тот — от класса System.Object):
278
Часть II. Главные конструкции программирования на C#
public class SystemException : Exception
{
// Различные конструкторы.
}
Из-за того, что в System.SystemException никакой дополнительной функциональ­
ности помимо набора специальных конструкторов больше не предлагается, может воз­
никнуть вопрос о том, а зачем он тогда вообще существует Попросту говоря, когда тип
исключения наследуется от System. SystemException, это дает возможность понять,
что сущностью, которая сгенерировала исключение, является исполняющая среда .NET,
а не кодовая база функционирующего приложения. В этом можно довольно легко удо­
стовериться с помощью ключевого слова is:
// Действительно1 Исключение NullReferenceException
// является исключением типа SystemException.
NullReferenceException nullRefEx = new NullReferenceException();
Console.WnteLine ("NullReferenceException is-а SystemException? : {0}",
nullRefEx is SystemException);
Исключения уровня приложения
(System.AppliestionException)
Поскольку все исключения .NET представляют собой типы классов, вполне допуска­
ется создавать собственные исключения, предназначенные для конкретного приложе­
ния. Из-за того, что базовый класс System. SystemException представляет исключе­
ния, генерируемые CLR-средой, может сложиться впечатление о том, что специальные
исключения тоже должны наследоваться от System.Exception. Поступать подоб­
ным образом действительно допускается, однако рекомендуется наследовать их не от
System.Exception,а от System.ApplicationException:
public class ApplicationException : Exception
{
// Различные конструкторы.
}
Как и в SystemException, в классе ApplicationException никаких дополнитель­
ных членов кроме набора конструкторов, не предлагается. С точки зрения функцио­
нальности единственной целью System. ApplicationException является указание на
источник ошибки. То есть при обработке исключения, унаследованного от System.
ApplicationException, программист может смело полагать, что исключение было вы­
звано кодом функционирующего приложения, а не библиотекой базовых классов .NET
или механизмом исполняющей среды .NET
Создание специальных исключений, способ первый
Хотя для уведомления о возникновении ошибки во время выполнения можно все­
гда генерировать экземпляры System.Exception (как было показано в первом приме­
ре), иногда гораздо выгоднее создавать строго типизированное исключение, способное
предоставлять уникальные детали по текущей проблеме. Например, предположим, что
понадобилось создать специальное исключение (по имени CarlsDeadException) для
предоставления деталей об ошибке, возникающей из-за увеличения скорости неис­
правного автомобиля. Для получения любого специального исключения в первую оче­
редь необходимо создать новый класс, унаследованный от класса System.Exception
или System. ApplicationException (по соглашению, имена всех классов исключений
оканчиваются суффиксом Exception; в действительности это является рекомендуемым
практическим приемом в .NET).
Глава 7. Структурированная обработка исключений
279
На заметку! Как правило, классы всех специальных исключений должны быть сделаны общедоступ­
ными, т.е. public (вспомните, что по умолчанию для не вложенных типов используется моди­
фикатор доступа internal, а не public). Объясняется это тем, что исключения часто пере­
даются за пределы сборки, следовательно, они должны быть доступны вызывающему коду.
Чтобы увидеть все на конкретном примере, давайте создадим новый проект типа
C o nso le A p p lic a tio n (Консольное приложение) по имени CustomException и скопируем
в него приведенные ранее файлы Car.cs и Radio, cs, выбрав в меню P ro je c t (Проект)
пункт A dd Existing Item (Добавить существующий элемент) и изменив для ясности назва­
ние пространства имен, в котором определяются типы Саг и Radio, с SimpleException
на CustomException. После этого добавим в него следующее определение класса:
// Это специальное исключение описывает детали условия
// выхода автомобиля из строя.
public class CarlsDeadException : ApplicationException
U
Как и в любой другой класс, в этот класс можно включать любое количество спе­
циальных членов, которые могли бы вызываться в блоке catch, а также переопреде­
лять в нем любые виртуальные члены, которые поставляются в родительских классах.
Например, реализовать CarlsDeadException можно было бы за счет переопределения
виртуального свойства Message.
Вместо заполнения словаря данных (через свойство Data) при выдаче исключения,
конструктор позволяет отправителю передавать данные о дате и времени и причине
возникновения ошибки, которые могут быть получены с помощью строго типизирован­
ных свойств:
public class CarlsDeadException : ApplicationException
{
private string messageDetails = String.Empty;
public DateTime ErrorTimeStamp {get; set;}
public string CauseOfError {get; set;}
public CarlsDeadException(){}
public CarlsDeadException(string message,
string cause, DateTime time)
{
messageDetails = message;
CauseOfError = cause;
ErrorTimeStamp = time;
}
// Переопределение свойства Exception.Message.
public override string Message
{
get
{
return string.Format("Car Error Message: {0}", messageDetails);
}
Здесь класс CarlsDeadException включает в себя приватное поле (message
Details), которое предоставляет данные о текущем исключении, которые могут уста­
навливаться с помощью специального конструктора. Генерация этого исключения из
метода Accelerate () производится довольно легко и заключается просто в выделении,
настройке и выдаче исключения типа CarlsDeadException, а не общего типа System.
Exception (обратите внимание, что в таком случае заполнять коллекцию данных вруч­
ную не понадобится):
280
Часть II. Главные конструкции программирования на C#
// Выдача специального исключения CarlsDeadException.
public void Accelerate(int delta)
CarlsDeadException ex =
new CarlsDeadException (string.Format("{0} has overheated!",
"You have a lead foot", DateTime.Now);
ex.HelpLink = "http://www.CarsRUs.com";
throw ex;
PetName),
}
Для перехвата такого поступающего исключения теперь можно модифицировать блок
catch, чтобы в нем перехватывалось именно исключение типа CarlsDeadException
(хотя из-за того, что System.CarlsDeadException является потомком System.
Exception, перехват в нем исключения типа System.Exception также допустим).
static void Main(string [] args)
{
Console .WnteLine ("**** * Fun with Custom Exceptions *****\n");
Car myCar = new Car ("Rusty" , 90);
try
{
// Отслеживание исключения.
myCar.Accelerate(50);
}
catch (CarlsDeadException e)
{
Console.WriteLine(e.Message);
Console .WnteLine (e .ErrorTimeStamp) ;
Console.WriteLine(e.CauseOfError);
}
Console.ReadLine ();
}
Теперь, когда известно, как в общем выглядит процесс создания специального ис­
ключения, может возникнуть вопрос о том, когда к нему следует прибегать. Обычно не­
обходимость в создании специальных исключений возникает, только если ошибка тесно
связана с генерирующим ее классом (например, специальный файловый класс может
выдавать набор специальных ошибок, связанных с файлами, класс Саг — ошибки, свя­
занные с автомобилем, объект доступа к данным — ошибки, связанные с отдельной
таблицей в базе данных, и т.д.). Их создание позволяет обеспечить вызывающий код
возможностью обрабатывать многочисленные исключения за счет описания каждой
ошибки по отдельности.
Создание специальных исключений, способ второй
В предыдущем примере в специальном типе CarlsDeadException переопределя­
лось свойство System. Exception .Message для настройки специального сообщения об
ошибке и поставлялось два специальных свойства для предоставления дополнитель­
ных фрагментов данных. В реальности, однако, переопределять виртуальное свойство
Message вовсе не требуется, поскольку можно также просто передавать поступающее
сообщение конструктору родителя, как показано ниже:
public class CarlsDeadException : ApplicationException
{
public DateTime ErrorTimeStamp { get; sec; }
public string CauseOfError { get; set; }
public CarlsDeadException () { }
Глава 7. Структурированная обработка исключений
281
// Передача сообщения конструктору родителя.
public CarlsDeadException(string message,
string cause, DateTime time)
:base(message)
{
CauseOfError = cause;
ErrorTimeStamp = time;
Обратите внимание, что на этот раз никакая строковая переменная для представ­
ления сообщения не определяется и никакое свойство Message не переопределяется.
Вместо этого производится передача соответствующего параметра конструктору ба­
зового класса. С таким дизайном специальный класс исключения представляет со­
бой уже нечто большее, чем просто класс с уникальным именем, унаследованный от
System.ApplicationException (и, при необходимости, имеющий дополнительные
свойства), поскольку не содержит никаких переопределений базового класса.
Не стоит удивляться, если многие (а то и все) специальные классы исключений при­
дется создавать именно по такой простой схеме. Во многих случаях роль специального
исключения состоит не в предоставлении дополнительной функциональности помимо
той, что унаследована от базовых классов, а в обеспечении строго именованного типа,
четко описывающего природу ошибки и тем самым позволяющего клиенту использо­
вать разную логику обработки для разных типов исключений.
Создание специальных исключений, способ третий
Если планируется создать действительно заслуживающий внимания специальный
класс исключения, необходимо позаботиться о том, чтобы он соответствовал наилуч­
шим рекомендациям .NET. В частности это означает, что он должен:
• наследоваться от ApplicationException;
• сопровождаться атрибутом [System. Serializable];
• иметь конструктор по умолчанию;
• иметь конструктор, который устанавливает значение унаследованного свойства
Message;
• иметь конструктор для обработки “внутренних исключений”;
• иметь конструктор для обработки сериализации типа.
Исходя из рассмотренного на текущий момент базового материла по .NET, роль ат­
рибутов и сериализации объектов может быть совершенно не понятна, в чем ничего
страшного нет, потому что эти темы будут подробно раскрываться далее в книге (в гла­
ве 15, которая посвящена атрибутам, и в главе 20, в которой рассматриваются служ­
бы сериализации). В завершение изучения специальных исключений ниже приведена
последняя версия класса CarlsDeadException, в которой поддерживается каждый из
упомянутых выше специальных конструкторов:
[Serializable]
public class CarlsDeadException : ApplicationException
{
public CarlsDeadException () { }
public CarlsDeadException(string message) : base ( message ) { }
public CarlsDeadException(string message,
System.Exception inner) : base ( message, inner ) { }
protected CarlsDeadException (
System.Runtime.Serialization.SerializationInfо info,
282
Часть II. Главные конструкции программирования на C#
System.Runtime.Serialization.StreamingContext context)
: base ( info, context ) { }
// Далее могут идти любые дополнительные специальные
// свойства, конструкторы и члены данных.
}
Поскольку специальные исключения, создаваемые в соответствии с наилучшими
практическими рекомендациям .NET, отличаются только именами, не может не радо­
вать тот факт, что в Visual Studio 2010 поставляется специальный шаблон фрагмента
кода под названием Exception (рис. 7.1), который позволяет автоматически генериро­
вать новый класс исключения, отвечающий требованиям наилучших практических
рекомендаций .NET. (Как рассказывалось в главе 2, для активизации фрагмента кода
необходимо ввести его имя, которым в данном случае является exception, и два раза
нажать клавишу <ТаЬ>.)
Рис. 7.1. Шаблон фрагмента кода под названием exception
Исходный код. Проект CustomException находится в подкаталоге Chapter 7.
Обработка многочисленных исключений
В простейшем варианте блок try сопровождается только одним блоком catch.
В реальности, однако, часто требуется, чтобы операторы в блоке try могли приво­
дить к срабатыванию нескольких возможных исключений. Чтобы рассмотреть при­
мер, создадим новый проект типа C o n s o le A p p lic a tio n (Консольное приложение) на C#
по имени ProcessMultipleExpceptions. Добавим в него файлы Car.cs, Radio.cs и
CarlsDeadException .cs из предыдущего примера CustomException (выбрав в меню
P ro je c t пункт A d d E xistin g Item ) и соответствующим образом модифицируем названия
пространств имен.
Далее изменим в классе Саг метод Accelerate () так, чтобы он выдавал и такое го­
товое исключение из библиотеки базовых классов, как ArgumentOutOfRangeException,
в случае передачи недействительного параметра (которым будет считаться любое зна­
чение меньше нуля). Обратите внимание, что конструктор этого класса исключения
принимает имя проблемного аргумента в качестве первого параметра string, следом
за которым идет сообщение с описанием ошибки:
// Выполнение проверки аргумента на предмет действительности перед продолжением.
public void Accelerate(int delta)
{
if(delta < 0)
// Скорость должна быть больше нуля!
throw new
ArgumentOutOfRangeException("delta", "Speed must be greater than zero1");
Глава 7. Структурированная обработка исключений
283
Теперь можно модифицировать логику в блоке catch так, чтобы в ней предусматри­
валось специфическая реакция на исключение каждого типа:
static void Main(string[] args)
{
Console.WriteLine("***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90) ;
try
{
// Отслеживание исключения ArgumentOutOfRangeException.
myCar.Accelerate(-10);
}
catch (CarlsDeadException e)
{
Console .WnteLine (e .Message) ;
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine (e.Message);
}
Console.ReadLine ();
}
При создании множества блоков catch следует иметь в виду, что в случае выда­
чи исключения оно будет обрабатываться “первым доступным” блоком catch. Чтобы
рассмотреть пример, изменим предыдущую логику, добавив еще один блок catch,
пытающийся обрабатывать все остальные исключения помимо CarlsDeadException
и ArgumentOutOfRangeException за счет перехвата исключения обобщенного типа
System.Exception, как показано ниже:
// Этот код компилироваться не будет!
static void Main(string[] args)
{
Console.WriteLine (''***** Handling Multiple Exceptions *****\n ");
Car myCar = new Car ("Rusty", 90) ;
try
{
// Приведение в действие исключения ArgumentOutOfRangeException.
myCar.Accelerate(-10);
}
catch(Exception e)
{
// Обработка всех остальных исключений?
Console.WriteLine(e.Message);
}
catch (CarlsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine() ;
}
Такая логика по обработке исключений будет приводить к ошибкам на этапе ком­
пиляции. Проблема в том, что первый блок catch может обрабатывать любые исклю­
чения, унаследованные от System.Exception, в том числе, следовательно, исключения
284
Часть II. Главные конструкции программирования на C#
типа CarlsDeadException и ArgumentOutOfRangeException. Из-за этого два послед­
них блока catch получаются недостижимыми.
При структурировании блоков catch необходимо помнить о том, что в первом блоке
должно обрабатываться наиболее конкретное исключение (т.е. исключение максималь­
но производного типа в цепочке наследования типов исключений), а в последнем бло­
ке — наиболее общее (т.е. исключение базового типа в текущей цепочке наследования,
каковым в данном случае является System.Exception).
Таким образом, если необходимо определить блок catch, способный обрабатывать
любые ошибки помимо CarlsDeadException и ArgumentOutOfRangeException, можно
написать следующий код:
// Этот код скомпилируется без проблем.
static void Main(string[] args)
{
Console.WriteLine ("***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90);
try
{
// Приведение в действие исключения ArgumentOutOfRangeException.
myCar.Accelerate (-10);
}
catch (CarlsDeadException e)
{
Console .WnteLine (e .Message) ;
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
// В этом блоке будут перехватываться любые другие исключения
// помимо CarlsDeadException и ArgumentOutOfRangeException.
catch (Exception е)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();
На заметку! Везде, где только возможно, следует отдавать предпочтение перехвату конкретных
классов исключений, а не общих исключений типа System.Exception. Хотя поначалу мо­
жет казаться, что это упрощает жизнь (поскольку охватывает все вещи, с которыми не хочется
возиться), со временем из-за того, что обработка более серьезной ошибки не была напрямую
предусмотрена в коде, могут возникать очень странные сбои во время выполнения. Не сле­
дует забывать о том, что последний блок catch, который отвечает за обработку исключений
System.Exception, имеет тенденцию оказываться чрезвычайно общим.
Общие операторы c a t c h
В C# поддерживается так называемый “общий” (универсальный) блок catch, в ко­
тором объект исключения, генерируемый тем или и н ы м членом, явным образом не
получается.
// Общий блок catch.
static void Main(string [] args)
{
Console.WriteLine (''***** Handling Multiple Exceptions *****\n");
Глава 7. Структурированная обработка исключений
285
Car myCar = new Car ("Rusty" , 90) ;
try
{
myCar.Accelerate(90);
}
catch
{
Console.WriteLine("Something bad happened...");
}
Console.ReadLine ();
}
Очевидно, что это не самый информативный способ обработки исключений, по­
скольку нет никакой возможности для получения более детальных сведений о возник­
шей ошибке (таких как имя метода, стек вызовов или специальное сообщение). Тем не
менее, в C# все-таки можно применять конструкцию подобного рода, так как она мо­
жет оказаться полезной, когда требуется обработать все ошибки в чрезвычайно общей
манере.
Передача исключений
При перехвате исключения внутри блока try допускается передавать (rethrow) ис­
ключение вверх по стеку вызовов предшествующему вызывающему коду. Для этого дос­
таточно воспользоваться в блоке catch ключевым словом throw. Это позволит передать
исключение вверх по цепочке логики вызовов, что может оказаться полезным, если блок
catch способен обрабатывать текущую ошибку только частично.
// Передача ответственности.
static void Main(string[] args)
{
try
{
// Логика, касающаяся увеличения скорости
// автомобиля...
}
catch(CarlsDeadException е)
{
// Выполнение любой частичной обработки данной ошибки
/ / и передача дальнейшей ответственности.
throw;
}
}
Следует иметь в виду, что в приведенном примере кода конечным получателем ис­
ключения CarlsDeadException будет CLR-среда из-за его передачи в методе Main ( ) .
Следовательно, конечному пользователю будет отображаться системное диалоговое
окно с информацией об ошибке. Обычно передача частичного обработанного исклю­
чения вызывающему коду осуществляется только в случае, если он способен обрабаты­
вать поступающее исключение более элегантно.
Обратите внимание на неявную передачу объекта CarlsDeadException и на приме­
нение ключевого слова throw без аргументов. Никакого нового объекта исключения не
создается, а производится просто передача самого исходного объекта исключения (со
всей его исходной информацией). Это позволяет сохранить контекст первоначального
целевого объекта.
286
Часть II. Главные конструкции программирования на C#
Внутренние исключения
Нетрудно догадаться, что вполне возможно генерировать исключение во время об­
работки какого-то другого исключения. Например, предположим, что производится об­
работка исключения CarlsDeadException в определенном блоке catch, и в ходе этого
процесса обработки предпринимается попытка записать данные трассировки стека в
файл carErrors .txt на диске С: (для получения доступа к таким ориентированным на
работу с вводом-выводом типам в директиве using должно быть указано пространство
имен System. 10).
catch(CarlsDeadException е)
{
// Попытка открыть файл c a rE rro rs.tx t на диске С:
FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);
}
Теперь, если указанный файл на диске С: отсутствует, вызов File.Open () приведет
к генерации исключения FileNotFoundException. Позже в книге будет более подробно
рассказываться о пространстве имен System. 10 и о том, как программно определить,
существует ли файл на жестком диске, перед тем как пытаться открыть его (это позво­
лит вообще избежать генерации исключения). Тем не менее, чтобы не отходить от темы
исключений, давайте считать, что такое исключение все-таки генерируется.
Если во время обработки исключения возникает какое-то другое исключение, со­
гласно наилучшим практическим рекомендациям, необходимо сохранить новый объект
исключения как “внутреннее исключение” в новом объекте того же типа, что у исход­
ного исключения. Причина, по которой необходимо выделять новый объект для обра­
батываемого исключения, связана с тем, что документировать внутреннее исключение
допускается только через параметр конструктора. Рассмотрим следующий код:
catch (CarlsDeadException е)
{
try
{
FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);
catch (Exception e2)
{
// Генерация исключения, записывающего новое
// исключение, а также сообщение первого исключения.
throw new CarlsDeadException(е.Message, е2) ;
}
}
В данном случае важно обратить внимание, что конструктору CarlsDeadException
в качестве второго параметра передается объект FileNotFoundException. После на­
стройки этоп объект передается вверх по стеку вызовов следующему вызывающему
коду, которым будет метод Main ().
Поскольку после Main () никакого “следующего вызывающего кода”, который мог бы
перехватить исключение, не существует, пользователю будет отображаться системное
диалоговое окно с сообщением об ошибке. Во многом подобно передаче исключения,
запись внутренних исключений обычно осуществляется только тогда, когда вызываю­
щий код способен обрабатывать данное исключение более элегантно. В этом случае в
вызывающем коде внутри catch может использоваться свойство InnerException для
извлечения деталей объекта внутреннего исключения.
Глава 7. Структурированная обработка исключений
287
Блок f i n a l l y
В контексте try/catch можно также определять необязательный блок finally. Это
гарантирует, что некоторый набор операторов будет выполняться всегда, независимо
от того, возникло исключение (любого типа) или нет. Для целей иллюстрации предпо­
ложим, что перед выходом из метода Main () должно всегда производиться выключение
радиоприемника в автомобиле, невзирая ни на какие обрабатываемые исключения.
static void Main(string[] args)
{
Console.WnteLine (''***** Handling Multiple Exceptions *****\n");
Car myCar = new Car ("Rusty", 90);
try
{
// Логика, связанная с увеличением скорости автомобиля.
}
catch(CarlsDeadException е)
{
// Обработка исключения CarlsDeadException.
}
catch(ArgumentOutOfRangeException е)
{
// Обработка исключения ArgumentOutOfRangeException.
}
catch(Exception e)
{
// Обработка любых других исключений.
}
finally
{
// Это код будет выполняться всегда, независимо
/ / о т того, будет возникать и обрабатываться
// какое-нибудь исключение или нет.
myCar.CrankTunes(false) ;
}
Console.ReadLine();
}
Если бы не был добавлен блок finally, тогда в случае возникновения любого ис­
ключения радиоприемник не выключался бы (что может как быть, так и не быть про­
блемой). В более реалистичном сценарии, когда необходимо удалить объекты, закрыть
файл, отключиться от базы данных (или чего-то подобного), блок finally представляет
собой идеальное место для выполнения надлежащей очистки.
Какие исключения могут выдавать методы
Из-за того, что каждый метод в .NET Framework может генерировать любое количе­
ство исключений (в зависимости от обстоятельств), возникает вполне логичный вопрос
о том, как узнать, какие исключения может выдавать тот или иной метод из библиотеки
базовых классов? Ответ прост: нужно заглянуть в документацию .NET Framework 4.0
SDK. В этой документации вместе с описанием каждого метода перечислены и все ис­
ключения, которые он может генерировать. В качестве более быстрой альтернативы,
для просмотра списка всех исключений, которые способен выдавать тот или иной член
библиотеки базовых классов, можно воспользоваться Visual Studio 2010, просто наведя
курсор мыши на имя интересующего члена в окне кода, как показано на рис. 7.2.
288
Насть II. Главные конструкции программирования на C#
Object Browse»
Start Page
Radio.cs
JfrProcessNfolt»pieExcept»ons.Program
CadsDeadExceptionxs
Carxs
**Mam{string[] args)
fin a lly
|
{
/ / Th is w i l l always o ccu r. Exception or not.
m y C a r.C ra n k T u n es(fa lse);
}
C o n so le . R e a d L ln e ();
>
}
strirg Console ReadL ref)
Reaos t"e •'ext line o' craracteT ^orr the standard nour st-ea^T.
L>
I'
1
Except ors
SystemlQIQExceptor
System. OjtOfMe'ro'yExcept on
1 System. Argjnr ertOutOTCangeException
Рис. 7.2. Просмотр списка исключений, которые может генерировать определенный метод
Тем, кто перешел на .NET с Java, важно понять, что в .NET члены типов не прототипируются с набором исключений, которые они могут выдавать (другими словами,
контролируемые исключения в C# не поддерживаются). Хорошо это или плохо, но обра­
батывать каждое исключение, генерируемое определенным членом, не требуется.
К чему приводят необрабатываемые исключения
К этому моменту наверняка возник вопрос о том, что произойдет, если выданное ис­
ключение обработано не будет? Для примера предположим, что в логике внутри Main ()
объект Саг разгоняется до скорости, превышающей допустимый максимальный предел,
и что логика try / c a tc h отсутствует:
static void Main(string[] args)
{
Console .WnteLine (''***** Handling Multiple Exceptions
Car myCar = new Car ("Rusty", 90);
myCar.Accelerate(500);
Console.ReadLine();
*****\n »);
}
Игнорирование исключения в таком случае станет серьезным препятствием для ко­
нечного пользователя приложения, поскольку ему будет отображаться диалоговое окно
с сообщением о необработанном исключении, показанное на рис. 7.3.
Д problem caused the program to stop working correctly.
Windows will close the program and notify you if a solution is
available.
[ Debug j
[
Close program
j
Рис. 7.3. Последствия не обрабатывания исключений
Глава 7. Структурированная обработка исключений
289
Отладка необработанных исключений
с помощью Visual Studio
Среда Visual-Studio 2010 предлагает набор инструментов, которые помогают отла­
живать необработанные специальные исключения. Для примера предположим, что ско­
рость объекта Саг была увеличена до предела, превышающего допустимый максимум.
После запуска сеанса отладки в Visual Studio 2010 (выбором в меню Debug (Отладка)
пункта Start (Начать)), выполнение программы на месте выдачи не перехватываемого
исключения будет автоматически прерываться. Кроме того, откроется окно (рис. 7.4), в
котором будет отображаться значение свойства Message.
a/iProces»Mulbp*eExcept»cn«.C«r
•I
•A cceieiaiei*n l ae*ta)
t l We n e e d t o c a l l t h e H e l p L i r * p r o p e r t y , t h u s we n e e d
/ / t o c r e a t e a l o c a l v a r i a b l e b e f o r e t h r o w in g t h e E x c e p t io n o b j e c t .
C a r Is O e a d lx re p tlo n e x ________
___ ____
.
n e » a r ID c a < J ( x c e p r _ . - r t C «is D « a d E x c e p tk > n w a s u n h a n d te d
" Y o u h a v e a l ^ a * f o o t ^ - T 7~~.
.
.
,
,
^
, , Rusty has overheated*
e x . H e lp li n J t » h t t p : / / * J
t h r o w ex
}
e ls e
Т м и и и ^ к ^ У п д tips.
f f a a t t h M O W h c a f d f T ! . v ------------- ------------ ----------------------------------------{H uety has overheated*"} |
CauseOlError
4 - "You have a lead foot*
lie.wrlu, 3 F
.
ErrorTvneStamp
{10/11/2009 9.-16:00 PM}
w| i
4 base
I
Search fo r m ore H elp Online...
Actions:
View Detail.
C opv exception detail to th e clipboard
Рис. 7.4. Отладка необработанных специальных исключений в Visual Studio 210
На заметку! Если обработать исключение, сгенерированное каким-то методом из библиотеки ба­
зовых классов .NET не удается, отладчик Visual Studio 2010 прерывает выполнение программы
на операторе, который вызвал этот проблемный метод.
Щелкнув в этом окне на ссылке View Detail (Показать подробности), можно увидеть
дополнительные сведения о состоянии объекта (рис. 7.5).
Рис. 7.5. Просмотр детальной информации об исключении
Исходный код. Проект ProcessMultipleExceptions доступен в подкаталоге Chapter 7.
290
Часть II. Главные конструкции программирования на C#
Несколько слов об исключениях,
связанных с поврежденным состоянием
(Corrupted State Exceptions)
В завершении изучения предлагаемой в C# поддержки для структурированной обра­
ботки исключений следует упомянуть о появлении в .NET 4.0 совершенно нового про­
странства имен под названием System.Runtime.ExceptionServices (которое постав­
ляется в составе сборки mscorlib.dll). Это довольно небольшое пространство имен
включает в себя всего два типа класса, которые могут применяться, когда необходимо
снабдить различные методы в приложении (вроде Main ( ) ) возможностью перехвата
и обработки “исключений, связанных с поврежденным состоянием” (Corrupted State
Exceptions — CSE).
Как говорилось в главе 1, платформа .NET всегда размещается в среде обслуживаю­
щей операционной системы (такой как Microsoft Windows). Имея опыт программирова­
ния приложений для Windows, можно вспомнить, что низкоуровневый API-интерфейс
Windows обладает очень уникальным набором правил по обработке ошибок времени
выполнения, которые немного похожи на предлагаемые в .NET приемы структуриро­
ванной обработки исключений.
В A PI-интерфейсе Windows можно перехватывать ошибки чрезвычайно низкого
уровня, которые как раз и представляют собой ошибки, связанные с “поврежденным
состоянием”. Попросту говоря, если ОС Windows передает такую ошибку, это означает,
что с программой что-то серьезно не так, причем настолько, что нет никакой надежды
на восстановление, и единственно возможной мерой является завершение ее работы.
На заметку! При работе с .NET ошибка CSE может появиться только в случае использования в
коде C# служб вызова платформы (для непосредственного взаимодействия с API-интерфейсом
Windows) или применения поддерживаемого в C# синтаксиса указателей (см. главу 12).
До выхода версии .NET 4.0 подобные низкоуровневые ошибки, специфичные для
операционной системы, можно было перехватывать только с помощью блока catch,
предусматривающего перехват общих исключений System.Exception. Однако с этим
подходом была связана проблема: если каким-то образом возникало исключение CSE,
которое перехватывалось в таком блоке catch, в .NET не предлагалось (и до сих пор не
предлагается) никакого элегантного кода для восстановления.
Теперь, с выходом версии .NET 4.0, среда CLR больше не разрешает автоматический
перехват исключений CSE в приложениях .NET. В большинстве случаев это именно то
поведение, которое нужно. Если же необходимо получать уведомления о таких ошибках
уровня ОС (обычно при использовании унаследованного кода, нуждающегося в таких
уведомлениях), понадобится применять атрибут [HandledProcessCorruptedState
Exceptions].
Хотя роль атрибутов в .NET рассматривается позже в книге (в главе 15), сейчас важ­
но понять, что данный атрибут может применяться к любому методу в приложении, и в
результате его применения соответствующий метод получит возможность иметь дело с
подобными низкоуровневыми ошибками, специфическими для операционной системы.
Чтобы увидеть хотя бы простой пример, давайте создадим следующий метод Main (), не
забыв перед этим импортировать в файл кода C# пространство имен System.Runtime.
ExceptionServices:
[HandledProcessCorruptedStateExceptions]
static int Mai n (string [] args)
{
Глава 7. Структурированная обработка исключений
291
try
{
// Предполагаем, что в Ма±п() вызывается метод,
// который отвечает за выполнение всей программы.
RunMyApplication();
}
catch (Exception ex)
{
// Если мы добрались сюда, значит, что-то произошло.
// Поэтому просто отображаем сообщение
/ / и выходим из программы.
Console .WnteLine ("Ack! Huge problem: (0}", ex.Message);
return -1;
}
return 0;
}
Задача приведенного выше метода Main () практически сводится только к вызову
второго метода, отвечающего за выполнение всего приложения. В данном примере бу­
дем полагать, что в этом втором методе RunMyApplication () интенсивно используется
логика try/catch для обработки любой ожидаемой ошибки. Поскольку метод Main ()
был помечен атрибутом [HandledProcessCorruptedStateExceptions], в случае воз­
никновения ошибки CSE перехват System.Exception получается последним шансом
сделать хоть что-то перед завершением работы программы.
Метод Main () здесь возвращает значение int, а не void. Как объяснялось в главе 3,
по соглашению возврат операционной системе нулевого значения свидетельствует о
завершении работы приложения без ошибок, в то время как возврат любого другого
значения (обычно отрицательного числа) — о том, что в ходе его выполнения возникла
какая-то ошибка.
В настоящей книге обработка подобных низкоуровневых ошибок, специфических для
операционной системы Windows, рассматриваться не будет, а потому и о роли System.
Runtime.ExceptionServices тоже подробно рассказываться не будет. Исчерпывающие
сведения по этому поводу можно найти в документации .NET 4.0 Framework SDK.
Резюме
В этой главе была показана роль, которую играет структурированная обработка ис­
ключений. Если необходимо, чтобы из метода вызывающему коду отправлялся объект,
описывающий ошибку, в методе нужно выделить, сконфигурировать и выдать конкрет­
ное исключение производного от System.Exception типа с применением такого под­
держиваемого в C# ключевого слова, как throw. В вызывающем коде любые поступаю­
щие исключения обрабатываются с помощью ключевого слова catch и необязательного
блока finally.
Для получения собственных специальных исключений, по сути, требуется создать
класс, унаследованный от класса System.ApplicationException. Этот новый класс
будет представлять исключения, генерируемые приложением, которое выполняется в
настоящий момент. Объекты ошибок, унаследованные от System.SystemException,
в свою очередь, позволяют представлять критические (и фатальные) ошибки, которые
выдает CLR. Напоследок в главе были продемонстрированы различные инструменты
внутри Visual Studio 2010, которые можно применять для создания специальных ис­
ключений (в соответствии с наилучшими практическими рекомендациями .NET), а так­
же для их отладки.
ГЛАВА О
Время жизни объектов
этому моменту было предоставлено немало сведений о создании специальных ти­
пов классов в С#. Теперь речь пойдет о том, как CLR-среда управляет размещен­
ными экземплярами классов (те. объектами) с помощью процесса сборки мусора (gar­
bage collection). Программистам на C# никогда не приходится непосредственно удалять
управляемый объект из памяти (в языке C# нет даже ключевого слова вроде delete).
Вместо этого объекты .NET размещаются в области памяти, которая называется управ­
ляемой кучей (managed heap), откуда они автоматически удаляются сборщиком мусора,
когда наступает “определенный момент в будущем”.
После рассмотрения ключевых деталей процесса сборки мусора в настоящей гла­
ве будет показано, как программно взаимодействовать со сборщиком мусора с помо­
щью класса System.GC, и как с применением виртуального метода System.Object.
Finalize () и интерфейса IDisposable создавать классы, способные освобождать
внутренние неуправляемые ресурсы в определенное время.
Кроме того, будут описаны некоторые новые функциональные возможности сборщи­
ка мусора, появившиеся в версии .NET 4.0, включая фоновую сборку мусора и отложен­
ную (ленивую) инициализацию с использованием обобщенного класса System.Lazyo.
После изучения материалов настоящей главы должно появиться вполне четкое пред­
ставление об управлении объектами .NET в среде CLR.
К
Классы, объекты и ссылки
Перед изучением тем, излагаемых в настоящей главе, сначала необходимо немного
больше прояснить различие между классами, объектами и ссылками. Вспомните, что
класс представляет собой ни что иное, как схему, которая описывает то, каким образом
экземпляр данного типа должен выглядеть и вести себя в памяти. Определяются классы
в файлах кода (которым по соглашению назначается расширение * .cs). Для примера
создадим новый проект типа C o n so le Application (Консольное приложение) на C# по
имени SimpleGC и определим в нем следующий простой класс Саг:
// Содержимое файла Car.cs
public class Car
{
public int CurrentSpeed {get; set;}
public string PetIJame {get; set;}
public Car () {}
public Car(string name, int speed)
{
PetName = name;
CurrentSpeed = speed;
Глава 8. Время жизни объектов
293
public override string ToStringO
{
return string.Format("{0 } is going {1} MPH",
petName, currSp);
PetName, CurrentSpeed);
}
\
Как только класс определен, с использованием ключевого слова new, поддерживае­
мого в С#, можно размещать в памяти любое количество его объектов. Однако при этом
следует помнить, что ключевое слово new возвращает ссылку на объект в куче, а не фак­
тический объект. Если ссылочная переменная объявляется как локальная переменная в
контексте метода, она сохраняется в стеке для дальнейшего использования в приложе­
нии. Для вызова членов объекта к сохраненной ссылке должна применяться операция
точки С#.
class Program
{
static void Main(string[] args)
{
Console.WriteLine (''***** GC Basics *****");
// Создание нового объекта Car в управляемой куче.
// Возвращается ссылка на этот объект (refToMyCar) .
Car refToMyCar = new Ca r ("Zippy", 50);
// Применение к переменной с этой ссылкой С#-операции
// точки (.)д л я вызова членов данного объекта.
Console .WriteLine (refToMyCar .ToStnng () ) ;
Console.ReadLine();
На рис. 8.1 схематично показаны отношения между классами, объектами и ссылка­
ми на них.
Рис. 8.1. Ссылки на объекты в управляемой куче
На заметку! Вспомните из главы 4, что структуры представляют собой типы значения, которые все­
гда размещаются прямо в стеке и никогда не попадают в управляемую кучу .NET. Размещение в
куче происходит только при создании экземпляров классов.
Базовые сведения о времени жизни объектов
При создании приложений на C# можно смело полагать, что исполняющая среда
.NET будет сама заботиться об управляемой куче без непосредственного вмешательства
со стороны программиста. На самом деле “золотое правило” по управлению памятью в
.NETT звучит просто.
294
Часть II. Главные конструкции программирования на C#
Правило. Размещайте объект в управляемой куче с использованием ключевого слова new и забы­
вайте об этом.
После создания объект будет автоматически удален сборщиком мусора тогда, когда
в нем отпадет необходимость. Разумеется, возникает вопрос о том, каким образом сбор­
щик мусора определяет момент, когда в объекте отпадает необходимость? В двух словах
на этот вопрос можно ответить так: сборщик мусора удаляет объект из кучи тогда, когда
тот становится недостижимым ни в одной части программного кода. Например, доба­
вим в класс Program метод, который размещает в памяти объект Саг:
public static void MakeACar()
// Если шуСаг является единственной ссылкой на объект
// Саг, тогда при возврате результата данным
// методом объект Саг *мояеет* быть уничтожен.
Car myCar = new Car();
}
Обратите внимание, что ссылка на объект Car (myCar) была создана непосредствен­
но внутри метода MakeACar () и не передавалась за пределы определяющей области
действия (ни в виде возвращаемого значения, ни в виде параметров ref/out). Поэтому
после завершения вызова данного метода ссылка myCar окажется недостижимой, а объ­
ект Саг, соответственно — кандидатом на удаление сборщиком мусора. Следует, однако,
понять, что иметь полную уверенность в немедленном удалении этого объекта из памя­
ти сразу же после выполнения метода MakeACar () нельзя. Все, что в данный момент
можно предполагать, так это то, что когда в CLR-среде будет в следующий раз произво­
диться сборка мусора, объект myCar может подпасть под процесс уничтожения.
Как станет ясно со временем, программирование в среде с автоматической сборкой
мусора значительно облегчает разработку приложений. Программистам на C++ хорошо
известно, что если они специально не позаботятся об удалении размещаемых в куче
объектов, вскоре обязательно начнут возникать “утечки памяти”. На самом деле отсле­
живание проблем, связанных с утечкой памяти, является одним из самых длительных
(и утомительных) аспектов программирования в неуправляемых средах. Благодаря на­
значению ответственным за уничтожение объектов сборщика мусора, обязанности по
управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду.
На заметку! Тем, кто ранее применял для разработки приложений технологию СОМ, следует знать,
что в .NET объекты не снабжаются никаким внутренним счетчиком ссылок и потому не поддер­
живают использования методов вроде AddRef () или Release ().
C IL-код, генерируемый для ключевого слова new
При обнаружении ключевого слова new компилятор C# вставляет в реализацию ме­
тода СIL-инструкцию newob j. Если скомпилировать текущий пример кода и заглянуть
в полученную сборку с помощью утилиты ildasm.exe, то можно обнаружить внутри
метода MakeACar () следующие CIL-операторы:
.method private hidebysig static void MakeACar () cil managed
{
// Code siz e 8 (0x8)
// Размер кода 8 (0x8)
.maxstack 1
Глава 8. Время жизни объектов
295
.locals init ([0] class SimpleGC.Car)
IL_0000: nop
IL_0001: newobj instance void SimpleGC.C a r c t o r ()
IL_0006: stloc.O
IL_0007: ret
} // end of method Program::MakeACar
// конец метода Program::MakeACar
Прежде чем ознакомиться с точными правилами, которые определяют момент, ко­
гда объект должен удаляться из управляемой кучи, давайте более подробно рассмотрим
роль CIL-инструкции newobj. Дна начала важно понять, что управляемая куча пред­
ставляет собой нечто большее, чем просто случайный фрагмент памяти, к которому
CLR получает доступ. Сборщик мусора .NET “убирает” «у ч у довольно тщательно, причем
(при необходимости) даже сжимает пустые блоки памяти с целью оптимизации. Чтобы
ему было легче это делать, в управляемой куче поддерживается указатель (обычно назы­
ваемый указателем на следующий объект или указателем на новый объект), который
показывает, где точно будет размещаться следующий объект.
Таким образом, инструкция newobj заставляет CLR-среду выполнить перечислен­
ные ниже ключевые операции.
• Вычислить, сколько всего памяти требуется для размещения объекта (в том числе
памяти, необходимой для членов данных и базовых классов).
• Проверить, действительно ли в управляемой куче хватает места для обслуживания
размещаемого объекта. Если хватает, вызвать указанный конструктор и вернуть
вызывающему коду ссылку на новый объект в памяти, адрес которого совпадает
с последней позицией указателя на следующий объект.
• И, наконец, перед возвратом ссылки вызывающему коду переместить указатель
на следующий объект, чтобы он указывал на следующую доступную позицию в
управляемой куче.
Весь описанный процесс схематично изображен на рис. 8.2.
Управляемая куча
s t a t i c v o id M a i n ( s t r in g [] a rg s )
{
Car c l = new C a r ( ) ; -----------------Car c2 = new C a r( ) ; -----------------}
C2
Cl
_ t
-
1
Указатель на
следующий объект
Рис. 8.2. Детали размещения объектов в управляемой куче
Из-за постоянного размещения объектов приложением пространство в управляемой
куче может со временем заполниться. В случае если при обработке следующей инструк­
ции newobj среда CLR обнаруживает, что в управляемой куче не хватает пространства
для размещения запрашиваемого типа, она приступает к сборке мусора и тем самым
пытается освободить хоть сколько-то памяти. Поэтому следующее правило, касающееся
сборки мусора, тоже звучит довольно просто.
Правило. В случае нехватки в управляемой куче пространства для размещения запрашиваемого
объекта начинает выполняться сборка мусора.
296
Часть II. Главные конструкции программирования на C#
Однако то, каким именно образом начнет выполняться сборка мусора, зависит от
версии .NET, под управлением которой функционирует приложение. Различия будут
описаны позже в настоящей главе.
Установка объектных ссылок в n u l l
Если ранее приходилось создавать COM-объекты в Visual Basic 6.0, то должно быть
известно, что по завершении их использования предпочтительнее устанавливать эти
ссылки в Nothing. На внутреннем уровне счетчик ссылок на объект СОМ уменьшалось
на единицу, и когда он становился равным нулю, объект можно было удалять из памя­
ти. Аналогичным образом программисты на C/C++ часто предпочитают устанавливать
для переменных указателей значение null, гарантируя, что они больше не будут ссы­
латься на неуправляемую память.
Из-за упомянутых фактов, вполне естественно, может возникнуть вопрос о том, что
же происходит в C# после установки объектных ссылок в null. Для примера изменим
метод MakeACar () следующим образом:
static void MakeACar ()
{
Car myCar = new Car () ;
myCar = null;
Когда объектные ссылки устанавливаются в n u ll, компилятор C# генерирует CILкод, который заботится о том, чтобы ссылка (в рассматриваемом примере myCar) боль­
ше не ссылалась ни на какой объект. Если теперь снова воспользоваться утилитой
ild a s m . ехе и заглянуть с ее помощью в CIL-код измененного метода MakeACar (), мож­
но обнаружить в нем код операции ld n u ll (который заталкивает значение n u ll в вир­
туальный стек выполнения) со следующим за ним кодом операции s t lo c .O (который
присваивает переменной ссылку n u ll):
.method private hidebysig static void MakeACar () cil managed
{
// Code size 10 (Oxa)
// Размер кода 10 (Оха)
.maxstack 1
.locals m i t ([0] class SimpleGC.Car myCar)
IL_0000: nop
IL_0001: newob] instance void SimpleGC.Car::.ctor ()
IL_0006: stloc.O
IL_0007: ldnull
IL_0008: stloc.O
IL_0009: ret
} // end of method Program::MakeACar
// конец метода Program::MakeACar
Однако обязательно следует понять, что установка ссылки в n u ll никоим образом
не вынуждает сборщик мусора немедленно приступить к делу и удалить объект из кучи,
а просто позволяет явно разорвать связь между ссылкой и объектом, на который она
ранее указывала. Благодаря этому, присваивание ссылкам значения n u ll в C# имеет
гораздо меньше последствий, чем в других языках на базе С (или VB 6.0), и совершенно
точно не будет причинять никакого вреда.
Глава 8. Время жизни объектов
297
Роль корневых элементов приложения
Теперь снова вернемся к вопросу о том, каким образом сборщик мусора определяет
момент, когда объект уже более не нужен. Чтобы разобраться в стоящих за этим дета­
лях, необходимо знать, что собой представляет корневые элементы приложения (appli­
cation roots). Попросту говоря, корневым элементом (root) называется ячейка в памяти,
в которой содержится ссылка на размещающийся в куче объект. Строго говоря, корне­
выми могут называться элементы любой из перечисленных ниже категорий.
• Ссылки на глобальные объекты (хотя в C# они не разрешены, CIL-код позволяет
размещать глобальные объекты).
• Ссылки на любые статические объекты или статические поля.
• Ссылки на локальные объекты в пределах кодовой базы приложения.
• Ссылки на передаваемые методу параметры объектов.
• Ссылки на объекты, ожидающие финализации (об этом подробно рассказываться
далее в главе).
• Любые регистры центрального процессора, которые ссылаются на объект.
Во время процесса сборки мусора исполняющая среда будет исследовать объекты
в управляемой куче, чтобы определить, являются ли они по-прежнему достижимыми
(т.е. корневыми) для приложения. Для этого среда CLR будет создавать графы объек­
тов, представляющие все достижимые для приложения объекты в куче. Более подробно
объектные графы будут описаны при рассмотрении процесса сериализации объектов в
главе 20. Пока главное усвоить то, что графы применяются для документирования всех
достижимых объектов. Кроме того, следует иметь в виду, что сборщик мусора никогда
не будет создавать граф для одного и того же объекта дважды, избегая необходимости
выполнения подсчета циклических ссылок, который характерен для программирования
в среде СОМ.
Чтобы увидеть все это на примере, предположим, что в управляемой куче содержится
набор объектов с именами А, В, С, D, Е, F и G. Во время сборки мусора эти объекты (а так­
же любые внутренние объектные ссылки, которые они могут содержать) будут исследо­
ваны на предмет наличия у них активных корневых элементов. После построения графа
все недостижимые объекты (которыми в примере пусть будут объекты С и F) помечаются
как являющиеся мусором. На рис. 8.3 показано, как примерно выглядит граф объектов в
только что описанном сценарии (линии со стрелками следует воспринимать как “зависит
от” или “требует”; например, “Е зависит от G и В”, ‘А не зависит ни от чего” и т.д.).
После того как объект помечен для уничтожения (в данном случае это объекты С и
F, поскольку в графе объектов они во внимание не принимаются), они будут удалены
из памяти. Оставшееся пространство в куче будет после этого сжиматься до компакт­
ного состояния, что, в свою очередь, вынудит CLR изменить набор активных корневых
элементов приложения (и лежащих в их основе указателей) так, чтобы они ссылались
на правильное место в памяти (это делается автоматически и прозрачно). И, наконец,
указатель на следующий объект тоже будет подстраиваться так, чтобы указывать на
следующий доступный участок памяти. На рис. 8.4 показано, как выглядит конечный
результат этих изменений в рассматриваемом сценарии.
На заметку! Собственно говоря, сборщик мусора использует две отдельных кучи, одна из которых
предназначена специально для хранения очень больших объектов. Доступ к этой куче во время
сборки мусора получается реже из-за возможных последствий в плане производительности, в
которые может выливаться изменение места размещения больших объектов. Невзирая на этот
факт, управляемая куча все равно может спокойно считаться единой областью памяти.
298
Часть II. Главные конструкции программирования на C#
Управляемая куча
А
В
С
D
Е
F
С
У казатель на следую!дий объект
Рис. 8.3. Графы объектов создаются для определения объектов,
достижимых для корневых элементов приложения
Управляемая куча
А
В
D
Е
G
Указатель на следую!щий объект
Рис. 8.4. Очищенная и сжатая до компактного состояния куча
Поколения объектов
При попытке обнаружить недостижимые объекты CLR-среда не проверяет буквально
каждый находящийся в куче объект. Очевидно, что на это уходила бы масса времени,
особенно в более крупных (реальных) приложениях.
Для оптимизации процесса каждый объект в куче относится к определенному “поко­
лению”. Смысл в применении поколений выглядит довольно просто: чем дольше объект
находится в куче, тем выше вероятность того, что он там и будет оставаться. Например,
класс, определенный в главном окне настольного приложения, будет оставаться в памя­
ти вплоть до завершения выполнения программы. С другой стороны, объекты, которые
были размещены в куче лишь недавно (как, например, те, что находятся в пределах
области действия метода), вероятнее всего будут становиться недостижимым довольно
быстро. Исходя из этих предположений, каждый объект в куче относится к одному из
перечисленных ниже поколений.
• Поколение О. Идентифицирует новый только что размещенный объект, который
еще никогда не помечался как подлежащий удалению в процессе сборки мусора.
• Поколение 1. Идентифицирует объект, который уже “пережил” один процесс сбор­
ки мусора (был помечен как подлежащий удалению в процессе сборки мусора, но
не был удален из-за наличия достаточного места в куче).
• Поколение 2. Идентифицирует объект, которому удалось пережить более одного
прогона сборщика мусора.
Глава 8. Время жизни объектов
299
На заметку! Поколения 0 и 1 называются эфемерными (недолговечными). В следующем разделе
будет показано, что в ходе процесса сборки мусора эфемерные поколения действительно об­
рабатываются по-другому.
Сборщик мусора сначала анализирует все объекты, которые относятся к поколению 0.
Если после их удаления остается достаточное количество памяти, статус всех осталь­
ных (уцелевших) объектов повышается до поколения 1. Чтобы увидеть, как поколение,
к которому относится объект, влияет на процесс сборки мусора, обратите внимание на
рис. 8.5, где схематически показано, как набору уцелевших объектов поколения О (А, В
и Е) назначается статус объектов следующего поколения после освобождения требуе­
мого объема памяти.
Поколение О
А
В
С
D
Е
F
G
Поколение 1
А
В
Е
Рис. 8.5. Объектам поколения 0, которые уцелели после сборки
мусора, назначается статус объектов поколения 1
Если все объекты поколения 0 уже были проверены, но все равно требуется допол­
нительное пространство, проверяться на предмет достижимости и подвергаться про­
цессу сборки мусора начинают объекты поколения 1. Объектам поколения 1, которым
удалось уцелеть после этого процесса, затем назначается статус объектов поколения 2.
Если же сборщику мусора все равно требуется дополнительная память, тогда на пред­
мет достижимости начинают проверяться и объекты поколения 2. Объектам, которым
удается пережить сборку мусора на этом этапе, оставляется статус объектов поколения
2, поскольку более высокие поколения просто не поддерживаются.
Из всего вышесказанного важно сделать следующий вывод: из-за отнесения объектов
в куче к определенному поколению, более новые объекты (вроде локальных переменных)
будут удаляться быстрее, а более старые (такие как объекты приложений) — реже.
Параллельная сборка мусора
в версиях .NET 1.0 — .NET 3.5
До выхода версии .NET 4.0 очистка неиспользуемых объектов в исполняющей сре­
де производилась с применением техники параллельной сборки мусора. В этой модели
при выполнении сборки мусора для любых объектов поколения 0 или 1 (т.е. эфемерных
поколений) сборщик мусора временно приостанавливал все активные потоки внутри
текущего процесса, чтобы приложение не могло получить доступ к управляемой куче
вплоть до завершения процесса сборки мусора.
Потоки более подробно рассматриваются в главе 19, а пока можно считать поток
просто одним из путей выполнения внутри функционирующей программы. По заверше­
300
Часть II. Главные конструкции программирования на C#
нии цикла сборки мусора приостановленным потокам разрешалось снова продолжать
работу. К счастью, в .NET 3.5 (и предшествующих версиях) сборщик мусора был хорошо
оптимизирован и потому связанные с этим короткие перерывы в работе приложения
редко становились заметными (а то и вообще никогда).
Как и оптимизация, параллельная сборка мусора позволяла производить очистку
объектов, которые не были обнаружены ни в одном из эфемерных поколений, в отдель­
ном потоке. Это сокращало (но не устраняло) необходимость в приостановке активных
потоков исполняющей средой .NET. Более того, параллельная сборка мусора позволяла
программам продолжать размещать объекты в куче во время сборки объектов не эфе­
мерных поколений.
Фоновая сборка мусора в версии .NET 4.0
В .NET 4.0 сборщик мусора по-другому решает вопрос с приостановкой потоков при
очистке объектов в управляемой куче, используя при этом технику фоновой сборки му­
сора. Несмотря на ее название, это вовсе не означает, что вся сборка мусора теперь
происходит в дополнительных фоновых потоках выполнения. На самом деле в случае
фоновой сборки мусора для объектов, относящихся к не эфемерному поколению, испол­
няющая среда .NET теперь может производить сборку объектов эфемерных поколений
в отдельном фоновом потоке.
Механизм сборки мусора в .NET 4.0 был улучшен так, чтобы на приостановку по­
тока, связанного с деталями сборки мусора, требовалось меньше времени. Благодаря
этим изменениям, процесс очистки неиспользуемых объектов поколения 0 или 1 стал
оптимальным. Он позволяет получать более высокие показатели по производительности
приложений (что действительно важно для систем, работающих в реальном времени и
нуждающихся в небольших и предсказуемых перерывах на сборку мусора).
Однако следует понимать, что ввод такой новой модели сборки мусора никоим обра­
зом не отражается на способе построения приложений .NET. Теперь практически всегда
можно просто позволять сборщику мусора .NET выполнять работу без непосредствен­
ного вмешательства со своей стороны (и радоваться тому, что разработчики в Microsoft
продолжают улучшать процесс сборки мусора прозрачным образом).
Тип System.GC
В библиотеках базовых классов доступен класс по имени System. GC, который по­
зволяет программно взаимодействовать со сборщиком мусора за счет обращения к его
статическим членам. Необходимость в непосредственном использовании этого клас­
са в разрабатываемом коде возникает крайне редко (а то и вообще никогда). Обычно
единственным случаем, когда нужно применять члены System. GC, является создание
классов, предусматривающих использование на внутреннем уровне неуправляемых ре­
сурсов. Это может быть, например, класс, работающий с основанным на С интерфейсом
Windows API за счет применения протокола вызовов платформы .NET, или какая-то низ­
коуровневая и сложная логика взаимодействия с СОМ. В табл. 8.1 приведено краткое
описание некоторых наиболее интересных членов класса System.GC (полные сведения
можно найти в документации .NET Framework 4.0 SDK).
На заметку! В .NET 3.5 с пакетом обновлений Service Pack 1 появилась дополнительная возможность
получать уведомления перед началом процесса сборки мусора за счет применения ряда новых
членов. И хотя данная возможность может оказаться полезной в некоторых сценариях, в боль­
шинстве приложений она не нужна, поэтому здесь подробно не рассматривается. Всю необхо­
димую информацию об уведомлениях подобного рода можно найти в разделе "Garbage Collection
Notifications” ("Уведомления о сборке мусора” ) документации .NET Framework 4.0 SDK.
Глава 8. Время жизни объектов
301
Таблица 8.1. Некоторые члены класса System. gc
Член
Описание
AddMemoryPressure(),
RemoveMemoryPressure()
Позволяют указывать числовое значение, отражающее "уро­
вень срочности” , который вызывающий объект применяет в
отношении к сборке мусора. Следует иметь в виду, что эти
методы должны изменять уровень давления в тандеме и, сле­
довательно, никогда не устранять больше давления, чем было
добавлено
Collect()
Заставляет сборщик мусора провести сборку мусора. Должен
быть перегружен так, чтобы указывать, объекты какого поко­
ления подлежат сборке, а также какой режим сборки исполь­
зовать (с помощью перечисления GCCollectionMode)
CollectionCount()
Возвращает числовое значение, показывающее, сколько раз
объектам данного поколения удалось переживать процесс
сборки мусора
GetGeneration()
Возвращает информацию о том, к какому поколению в на­
стоящий момент относится объект
GetTotalMemory()
Возвращает информацию о том, какой объем памяти (в бай­
тах) в настоящий момент занят в управляемой куче. Булевский
параметр указывает, должен ли вызов сначала дождаться вы­
полнения сборки мусора, прежде чем возвращать результат
MaxGeneration
Возвращает информацию о том, сколько максимум поколе­
ний поддерживается в целевой системе. В .NET 4.0 поддер­
живается всего три поколения: 0, 1 и 2
SuppressFinalize()
Позволяет устанавливать флаг, указывающий, что для данно­
го объекта не должен вызываться его метод Finalize ()
WaitForPendingFmalizers ()
Позволяет приостанавливать выполнение текущего потока до
тех пор, пока не будут финализированы все объекты, преду­
сматривающие финализацию. Обычно вызывается сразу же
после вызова метода GC.Collect ()
Рассмотрим применение System. GC для получения касающихся сборки мусора де­
талей на примере следующего метода Main (), в котором используются сразу несколько
членов System. GC:
static void Main(string[] args)
{
Console.WnteLine (”***** Fun with System.GC *****”);
// Вывод подсчитанного количества байтов в куче.
Console.WriteLine("Estimates bytes on heap: {0}”, G C .GetTotalMemory(false));
// Отсчет для MaxGeneration начинается с нуля,
// поэтому для удобства добавляется 1.
Console.WriteLine("This OS has {0} object generations.\n”,
(GC.MaxGeneration + 1));
Car refToMyCar = new Car(”Zippy”, 100);
Console.WriteLine(refToMyCar.ToString());
// Вывод информации о поколении объекта refToMyCar.
Console.WriteLine (’’Generation of refToMyCar is: {0 1”,
GC .GetGeneration(refToMyCar));
Console.FeadLine ();
302
Часть II. Главные конструкции программирования на C#
Принудительная активизация сборки мусора
Сборщик мусора .NET предназначен в основном для того, чтобы управлять памя­
тью вместо разработчиков. Однако в очень редких случаях требуется принудительно
запустить сборку мусора с помощью метода GC.Collect ( ) . Примеры таких ситуаций
приведены ниже.
• Приложение приступает к выполнению блока кода, прерывание которого возмож­
ным процессом сборки мусора является недопустимым.
• Приложение только что закончило размещать чрезвычайно большое количество объ­
ектов и нуждается в как можно скорейшем освобождении большого объема памяти.
Если выяснилось, что выполнение сборщиком мусора проверки на предмет наличия
недостижимых объектов может быть выгодным, можно инициировать процесс сборки
мусора явным образом, как показано ниже:
static void Main(string[] args)
{
// Принудительная активизация процесса сборки мусора и
// ожидание завершения финализации каждого из объектов.
GC.Collect ();
G C .WaitForPendingFinalizers ();
В случае принудительной активизации сборки мусора не забывайте вызвать метод
GC . WaitForPendingFinalizers (). Это дает возможность всем финализируемым объек­
там (рассматриваются в следующем разделе) произвести любую необходимую очистку
перед продолжением работы программы. Метод GC .WaitForPendingFinalizers () не­
заметно приостанавливает выполнение вызывающего “потока” во время процесса сбор­
ки мусора, что очень хорошо, поскольку исключает вероятность вызова в коде какихлибо методов на объекте, который в текущий момент уничтожается.
Методу GC .Collect () можно передать числовое значение, отражающее старейшее
поколение объектов, в отношении которого должен проводиться процесс сборки мусора.
Например, чтобы CLR-среда анализировала только объекты поколения 0, необходимо
использовать следующий код:
static void Main(string[] args)
{
// Исследование только объектов поколения 0.
GC.Collect (0);
G C .WaitForPendingFinalizers ();
}
Вдобавок методу Collect () во втором параметре может передаваться значение пе­
речисления GCCollectionMode, которое позволяет более точно указать, каким образом
исполняющая среда должна принудительно инициировать сборку мусора. Ниже пока­
заны значения, доступные в этом перечислении:
public enum
{
Default,
Forced,
Optimized
GCCollectionMode
// Текущим значением по умолчанию является Forced.
// Указывает исполняющей среде начать сборку мусора немедленно1
// Позволяет исполняющей среде выяснить, оптимален
/ / л и настоящий момент для удаления объектов.
Глава 8. Время жизни объектов
303
Как и при любой сборке мусора, в случае вызова G C .C o lle c tO уцелевшим объектам
назначается статус объектов более высокого поколения. Чтобы удостовериться в этом,
модифицируем метод Main () следующим образом:
static void Main(string[] args)
{
Console.WnteLine (''***** Fun with System.GC *****");
// Отображение примерного количества байтов в куче.
Console.WriteLine("Estimated bytes on heap: {0}",
GC.GetTotalMemory(false));
// Отсчет значения MaxGeneration начинается с нуля.
Console.WriteLine ("This OS has {0} object generations.\n",
(GC.MaxGeneration + 1));
Car refToMyCar = new Car("Zippy", 100);
Console .WriteLine (refToMyCar. ToStnng () ) ;
// Вывод информации о поколении, к которому
// относится refToMyCar.
Console.WriteLine ("\nGeneration of refToMyCar is: {0}",
GC.GetGeneration (refToMyCar));
// Создание большого количества объектов для целей тестирования.
object[] tonsOfObjects = new object[50000];
for (int i = 0; i < 50000; i++)
tonsOfObjects [i] = new object ();
// Выполнение сборки мусора в отношении только
// объектов, относящихся к поколению 0.
GC .Collect (0, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
// Вывод информации о поколении, к которому
// относится refToMyCar.
Console.WriteLine("\nGeneration of refToMyCar is: {0}",
GC .GetGeneration(refToMyCar));
//
//
//
if
Выполнение проверки, удалось ли
tonsOfObjects[9000] уцелеть
после сборки мусора.
(tonsOfObjects [9000] !=null)
{
// Вывод поколения tonsOfObjects [9000 ] .
Console.WriteLine("Generation of tonsOfObjects[9000] is: {0}",
GC .GetGeneration (tonsOfObjects [9000])) ;
}
else
// tonsOfObjects[9000] больше не существует.
Console.WriteLine("tonsOfObjects [9000] is no longer alive.");
// Вывод информации о том, сколько раз в отношении
// объектов каждого поколения выполнялась сборка мусора.
Console.WriteLine ("\nGen 0 has been swept {0} times",
G C .CollectionCount (0));
Console.WriteLine ("Gen 1 has been swept {0} times",
GC.CollectionCount (1));
Console.WriteLine("Gen 2 has been swept {0} times",
GC.CollectionCount(2));
Console.ReadLine ();
304
Часть II. Главные конструкции программирования на C#
Дя целей тестирования был создан очень большой массив типа object (состоящий
из 50 000 элементов). Как можно увидеть по приведенному ниже выводу, хотя в данном
методе Main () и был сделан только один явный запрос на выполнение сборки мусора
(с помощью метода GC.CollectO), среда CLR в фоновом режиме провела несколько та­
ких сборок.
***** Fun with System.GC *****
Estimated bytes on heap: 70240
This OS has 3 object generations.
Zippy is going 100 MPH
Generation of refToMyCar is: 0
Generation of refToMyCar is: 1
Generation of tonsOfObjects [9000] is: 1
Gen 0 has been swept 1 times
Gen 1 has been swept 0 times
Gen 2 has been swept 0 times
К этому моменту детали жизненного цикла объектов должны выглядеть более понят­
но. В следующем разделе исследование процесса сборки мусора продолжается, и будет
показано, как создавать фииализируемые (finalizable) и высвобождаемые (disposable) объ­
екты. Очень важно отметить, что описанные далее приемы подходят только в случае по­
строения управляемых классов, внутри которых используются неуправляемые ресурсы.
Исходный код. Проект SimpleGC доступен в подкаталоге Chapter 8.
Создание финализируемых объектов
В главе 6 уже рассказывалось о том, что в самом главном базовом классе .NET —
System. Object — имеется виртуальный метод по имени Finalize(). В предлагаемой
по умолчанию реализации он ничего особенного не делает:
// Класс System.Object
public class Object
{
protected virtual void Finalize () {}
}
За счет его переопределения в специальных классах устанавливается специфиче­
ское место для выполнения любой необходимой данному типу логики по очистке. Из-за
того, что метод Finalize () по определению является защищенным (protected), вызы­
вать его напрямую из класса экземпляра с помощью операции точки не допускается.
Вместо этого метод Finalize () (если он поддерживается) будет автоматически вызы­
ваться сборщиком мусора перед удалением соответствующего объекта из памяти.
На заметку! Переопределять метод Finalize () в типах структур нельзя. Это вполне логичное
ограничение, поскольку структуры представляют собой типы значения, которые изначально
никогда не размещаются в управляемой памяти и, следовательно, никогда не подвергаются
процессу сборки мусора. Однако в случае создания структуры, которая содержит ресурсы,
нуждающиеся в очистке, вместо этого метода можно реализовать интерфейс^iDisposable
(рассматривается далее в главе).
Разумеется, вызов метода Finalize () будет происходить (в конечном итоге) либо
во время естественной активизации процесса сборки мусора, либо во время его при­
нудительной активизации программным образом с помощью GC.CollectO. Помимо
Глава 8. Время жизни объектов
305
этого, финализатор типа будет автоматически вызываться и при выгрузке из памяти
домена, который отвечает за обслуживание приложения. Некоторым по опыту работы
с .NETT уже может быть известно, что домены приложений (AppDomain) применяются
для обслуживания исполняемой сборки и любых необходимых внешних библиотек кода.
Те, кто еще не знаком с этим понятием .NET, узнают все необходимое после прочтения
главы 16. Пока что необходимо обратить внимание лишь на то, что при выгрузке до­
мена приложения из памяти CLR-среда будет автоматически вызывать финализаторы
для каждого финализируемого объекта, который был создан во время существования
AppDomain.
Что бы не подсказывали инстинкты разработчика, в подавляющем большинстве
классов C# необходимость в создании явной логики по очистке или специального финализатора возникать не будет. Объясняется это очень просто: если в классах использу­
ются лишь другие управляемые объекты, все они рано или поздно все равно будут под­
вергаться сборке мусора. Единственным случаем, когда может возникать потребность в
создании класса, способного выполнять после себя процедуру очистки, является рабо­
та с неуправляемыми ресурсами (такими как низкоуровневые файловые дескрипторы,
низкоуровневые неуправляемые соединения с базами данных, фрагменты неуправляе­
мой памяти и т.п.). Внутри .NETT неуправляемые ресурсы появляются в результате непо­
средственного вызова API-интерфейса операционной системы с помощью служб PInvoke
(Platform Invocation Services — службы вызова платформы) или применения очень слож­
ных сценариев взаимодействия с СОМ. Ознакомьтесь со следующим правилом сборки
мусора.
Правило. Единственная причина переопределения Finalize () связана с использованием в клас­
се C# каких-то неуправляемых ресурсов через PInvoke или сложных процедур взаимодействия с
СОМ (обычно посредством членов типа System.Runtime .InteropServices .Marshal).
Переопределение S y s t e m . O b j e c t . F i n a l i z e ( )
В тех редких случаях, когда создается класс С#, в котором используются неуправ­
ляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение соот­
ветствующей памяти. Для примера создадим новый проект типа Console Application
на C# по имени SimpleFinalize, вставим в него класс MyResourceWrapper, в котором
будет использоваться какой-то неуправляемый ресурс. Теперь необходимо переопреде­
лить метод Finalize (). Как ни странно, применять для этого ключевое слово override
в C# не допускается:
class MyResourceWrapper
{
// Ошибка на этапе компиляции'
protected override void Finalize () { }
}
Вместо этого для достижения того же эффекта должен применяться синтаксис дест­
руктора (подобно C++). Объясняется это тем, что при обработке синтаксиса финализатора компилятор автоматически добавляет в неявно переопределяемый метод Finalize ()
приличное количество требуемых элементов инфраструктуры.
Финализаторы в C# очень похожи на конструкторы тем, что именуются идентично
классу, внутри которого определены. Помимо этого, они сопровождаются префиксом в
виде тильды (~). В отличие от конструкторов, однако, они никогда не снабжаются моди­
фикатором доступа (поскольку всегда являются неявно защищенными), не принимают
никаких параметров и не могут быть перегружены (в каждом классе может присутство­
вать только один финализатор).
306
Часть II. Главные конструкции программирования на C#
Ниже приведен пример написания спец иального ф инализатора для класса
MyResourceWrapper, который при вызове заставляет систему выдавать звуковой сиг­
нал. Очевидно, что данный пример предназначен только для демонстрационных целей.
В реальном приложении финализатор будет только освобождать любые неуправляемые
ресурсы и не будет взаимодействовать ни с какими другими управляемыми объекта­
ми, даже теми, на которые ссылается текущий объект, поскольку рассчитывать на то,
что они все еще существуют на момент вызова данного метода F in a liz e () сборщиком
мусора нельзя.
// Переопределение System .O bject.F in alize()
/ / с использованием синтаксиса финализатора.
class MyResourceWrapper
{
~MyResourceWrapper()
{
// Здесь производится очистка неуправляемых ресурсов.
// Обеспечение подачи звукового сигнала при
// уничтожении объекта (только в целях тестирования).
Console.Веер();
}
Если теперь просмотреть CIL-код данного деструктора с помощью утилиты ildasm .
ехе, обнаружится, что компилятор добавляет некоторый код для проверки ошибок. Вопервых, он помещает операторы из области действия метода F in a liz e () в блок t r y (см.
главу 7), и, во-вторых, добавляет соответствующий блок f i n a l l y , чтобы гарантировать
выполнение метода F in a l i z e () в базовых классах, какие бы исключения не возникали
в контексте tr y .
.method family hidebysig virtual instance void
F in a liz e () cil managed
{
// Code siz e
13 (Oxd)
// Размер кода 1 3 (Oxd)
.maxstack 1
.try
{
IL_0000: ldc.i4
0x4e20
IL_0005: ldc.i4
0x3e8
IL_000a: call
void [m scorlib]System .Console: : Beep(int32, int32)
IL_000f: nop
IL_0010: nop
IL_0011: leave.s
IL_001b
} // end .try
f i n a lly
{
IL_0013: ldarg.O
IL_0014:
c a l l instance void [m scorlib]System .O bject: : F in a liz e ()
IL_0019: nop
IL_001a: endfinally
} // end handler
IL 001b: nop
IL_001c: ret
} // end of method MyResourceWrapper::Finalize
// конец метода MyResourceWrapper::Finalize
Глава 8. Время жизни объектов
307
При тестировании класса MyResourceWrapper система выдает звуковой сигнал во
время завершения работы приложения, так как среда CLR автоматически вызывает финализаторы при выгрузке AppDomain.
static void Main(string [] args)
{
Console.WriteLine("***** Fun with Finalizers *****\n");
Console .WnteLine ("Hit the return key to shut down this app");
Console.WriteLine("and force the GC to invoke Finalize ()");
Console.WriteLine("for finalizable objects created in this AppDomain.");
// Нажмите клавишу <Enter>, чтобы завершить приложение
// и заставить сборщик мусора вызвать метод Finalize ()
// для всех финализируемых объектов, которые
// были созданы в домене этого приложения.
Console.ReadLine ();
MyResourceWrapper rw = new MyResourceWrapper ();
Исходный код. Проект SimpleFinalize доступен в подкаталоге Chapter 8.
Описание процесса финализации
Чтобы не делать лишнюю работу, следует всегда помнить, что задачей метода
Finalize () является забота о том, чтобы объект .NET мог освобождать неуправляемые
ресурсы во время сборки мусора. Следовательно, при создании типа, в котором никакие
неуправляемые сущности не используются (так бывает чаще всего), от финализации
оказывается мало толку. На самом деле, всегда, когда возможно, следует стараться про­
ектировать типы так, чтобы в них не поддерживался метод Finalize () по той очень
простой причине, что выполнение финализации отнимает время.
При размещении объекта в управляемой куче исполняющая среда автоматически
определяет, поддерживается ли в нем какой-нибудь специальный метод Finalize ().
Если да, тогда она помечает его как финализируемый (finalizable) и сохраняет указа­
тель на него во внутренней очереди, называемой очередью финализации (finalization
queue). Эта очередь финализации представляет собой просматриваемую сборщиком
мусора таблицу, где перечислены объекты, которые перед удалением из кучи должны
быть обязательно финализированы.
Когда сборщик мусора определяет, что наступило время удалить объект из памяти,
он проверяет каждую запись в очереди финализации и копирует объект из кучи в еще
одну управляемую структуру, называемую таблицей объектов, доступных для финали­
зации (finalization reachable table). После этого он создает отдельный поток для вызова
метода FinalizeO в отношении каждого из упоминаемых в этой таблице объектов при
следующей сборке мусора. В результате получается, что для окончательной финализа­
ции объекта требуется как минимум два процесса сборки мусора.
Из всего вышесказанного следует, что хотя финализация объекта действительно по­
зволяет гарантировать способность объекта освобождать неуправляемые ресурсы, она
все равно остается недетерминированной по своей природе, и по причине дополнитель­
ной выполняемой незаметным образом обработки протекает гораздо медленнее.
Создание высвобождаемых объектов
Как было показано, методы финализации могут применяться для освобождения не­
управляемых ресурсов при активизации процесса сборки мусора. Однако многие не­
управляемые объекты являются “ценными элементами” (например, низкоуровневые
308
Часть II. Главные конструкции программирования на C#
соединения с базой данных или файловые дескрипторы) и часто выгоднее освобождать
их как можно раньше, еще до наступления момента сборки мусора. Поэтому вместо пе­
реопределения Finalize () в качестве альтернативного варианта также можно реали­
зовать в классе интерфейс IDisposable, который имеет единственный метод по имени
Dispose() :
public interface IDisposable
{
void Dispose ();
}
Принципы программирования с использованием интерфейсов детально рассматри­
ваются в главе 9. Если объяснять вкратце, то интерфейс представляет собой коллекцию
абстрактных членов, которые может поддерживать класс или структура. Когда действи­
тельно реализуется поддержка интерфейса IDisposable, то предполагается, что после
завершения работы с объектом метод Dispose () должен вручную вызываться пользо­
вателем этого объекта, прежде чем объектной ссылке будет позволено покинуть об­
ласть действия. Благодаря этому объект может выполнять любую необходимую очистку
неуправляемых ресурсов без попадания в очередь финализации и без ожидания того,
когда сборщик мусора запустит содержащуюся в классе логику финализации.
На заметку! Интерфейс IDisposable может быть реализован как в классах, так и в структурах
(в отличие от метода Finalize ( ) , который допускается переопределять только в классах),
потому что метод Dispose () вызывается пользователем объекта (а не сборщиком мусора).
Рассмотрим пример использования этого интерфейса. Создадим новый проект
типа Console Application по имени SimpleDispose и добавим в него следующую мо­
дифицированную версию класса MyResourceWrapper, в которой теперь предусмотрена
реализация интерфейса IDisposable, а не переопределение метода System.Object.
Finalize() :
// Реализация интерфейса IDisposable.
public class MyResourceWrapper : IDisposable
{
// После окончания работы с объектом пользователь
// объекта должен вызывать этот метод.
public void Dispose ()
{
// Освобождение неуправляемых ресурсов...
// Избавление от других содержащихся внутри
/ / и пригодных для очистки объектов.
// Только для целей тестирования.
Console .WnteLine ("**** * In Dispose1 *****");
}
Обратите внимание, что метод Dispose () отвечает не только за освобождение не­
управляемых ресурсов типа, но и за вызов аналогичного метода в отношении любых
других содержащихся в нем высвобождаемых объектов. В отличие от Finalize (), в нем
вполне допустимо взаимодействовать с другими управляемыми объектами. Объясняется
это очень просто: сборщик мусора не имеет понятия об интерфейсе IDisposable и по­
тому никогда не будет вызывать метод Dispose (). Следовательно, при вызове данного
метода пользователем объект будет все еще существовать в управляемой куче и иметь
доступ ко всем остальным находящимся там объектам. Логика вызова этого метода вы­
глядит довольно просто:
Глава 8. Время жизни объектов
309
class Program
{
static void Main(string [] args)
{
Console .WnteLine ("***** Fun with Dispose *****\n");
// Создание высвобождаемого объекта и вызов метода
// Dispose() для освобождения любых внутренних ресурсов.
MyResourceWrapper rw = new MyResourceWrapper() ;
rw.Dispose();
Console.ReadLine ();
}
}
Разумеется, перед тем как пытаться вызывать метод Dispose () на объекте, нужно
проверить, поддерживает ли соответствующий тип интерфейс IDisposable. И хотя в
документации .NET Framework 4.0 SDK всегда доступна информация о том, какие типы
в библиотеке базовых классов реализуют IDisposable, такую проверку удобнее выпол­
нять программно с применением ключевого слова is или as (см. главу 6).
class Program
{
static void Main(string[] args)
{
Console.WnteLine (''***** Fun with Dispose *****\n");
MyResourceWrapper rw = new MyResourceWrapper ();
if (rw is IDisposable)
rw.Dispose();
Console.ReadLine();
}
}
Этот пример раскрывает еще одно правило относительно работы с подвергаемыми
сборке мусора типами.
Правило. Для любого создаваемого напрямую объекта, если он поддерживает интерфейс
IDisposable, следует всегда вызывать метод Dispose ( ) . Необходимо исходить из того,
что в случае, если разработчик класса решил реализовать метод Dispose ( ) , значит, классу
надлежит выполнять какую-то очистку.
К приведенному выше правилу прилагается одно важное пояснение. Некоторые из
типов, которые поставляются в библиотеках базовых классов и реализуют интерфейс
IDisposable, предусматривают использование для метода Dispose () (несколько сби­
вающего с толку) псевдонима, чтобы заставить отвечающий за очистку метод звучать
более естественно для типа, в котором он определяется. Для примера можно взять класс
System. 10. FileStream, в котором реализуется интерфейс IDisposable (и, следова­
тельно, поддерживается метод Dispose ()), но при этом также определяется и метод
Close (), каковой применяется для той же цели.
// Предполагается, что было импортировано пространство имен System.1 0 ...
static void DisposeFileStream()
{
FileStream fs = new FileStream("myFile.txt", FileMode.OpenOrCreate);
// Мягко говоря, сбивает с толку!
// Вызовы этих методов делают одно и то же!
fs.Close();
fs.Dispose() ;
310
Часть II. Главные конструкции программирования на C#
И хотя “закрытие” (close) файла действительно звучит более естественно, чем его
“освобождение” (dispose), нельзя не согласиться с тем, что подобное дублирование от­
вечающих за одно и то же методов вносит путаницу. Поэтому при использовании этих
нескольких типов, в которых применяются псевдонимы, просто помните о том, что если
тип реализует интерфейс IDisposable, то вызов метода Dispose () всегда является
правильным образом действия.
Повторное использование ключевого слова using в C#
При работе с управляемым объектом, который реализует интерфейс IDisposable,
довольно часто требуется применять структурированную обработку исключений, га­
рантируя, что метод Dispose () типа будет вызываться даже в случае возникновения
какого-то исключения:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
MyResourceWrapper rw = new MyResourceWrapper();
try
// Использование членов rw.
}
finally
{
// Обеспечение вызова метод D ispose() в любом случае,
/ / в том числе при возникновении ошибки.
rw.Dispose();
}
}
Хотя это является замечательными примером “безопасного программирования”, ис­
тина состоит в том, что очень немногих разработчиков прельщает перспектива заклю­
чать каждый очищаемый тип в блок try/finally лишь для того, чтобы гарантировать
вызов метода Dispose (). Для достижения аналогичного результата, но гораздо менее
громоздким образом, в C# поддерживается специальный фрагмент синтаксиса, кото­
рый выглядит следующим образом:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
// Метод D ispose() вызывается автоматически
// при выходе за пределы области действия using.
using(MyResourceWrapper rw = new MyResourceWrapper())
{
•
// Использование объекта rw.
}
}
Если теперь просмотреть CIL-код этого метода Main () с помощью утилиты ildasm.ехе,
то обнаружится, что синтаксис using в таких случаях на самом деле расширяется до
логики try/finally, которая включает в себя и ожидаемый вызов Dispose ():
.method private hidebysig static void Main(string [] args) cil managed
{
.try
} // end .try
Глава 8. Время жизни объектов
311
finally
IL_0012: callvirt instance void
Sim pleFinalize.MyResourceWrapper: : D ispose()
} // end handler
} // end of method Program::Main
На заметку! При попытке применить using к объекту, который не реализует интерфейс
IDisposable, на этапе компиляции возникнет ошибка.
Хотя применение такого синтаксиса действительно избавляет от необходимости
вручную помещать высвобождаемые объекты в рамки try/f inally, в настоящее время,
к сожалению, ключевое слово using в C# имеет двойное значение (поскольку служит и
для добавления ссылки на пространства имен, и для вызова метода Dispose ()). Тем не
менее, при работе с типами.NET, которые поддерживают интерфейс IDisposable, дан­
ная синтаксическая конструкция будет гарантировать автоматический вызов метода
Dispose () в отношении соответствующего объекта при выходе из блока using.
Кроме того, в контексте using допускается объявлять несколько объектов одного и
того ж е типа. Как не трудно догадаться, в таком случае компилятор будет вставлять
код с вызовом Dispose () для каждого объявляемого объекта.
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Dispose *****\n");
// Использование разделенного запятыми списка для
// объявления нескольких подлежащих освобождению объектов.
using(MyResourceWrapper rw = new MyResourceWrapper(),
rw2 = new MyResourceWrapper())
{
// Использование объектов rw и rw2.
}
}
Исходный код. Проект SimpleDispose доступен в подкаталоге Chapter 8.
Создание финализируемых
и высвобождаемых типов
К этому моменту были рассмотрены два различных подхода, которые можно при­
менять для создания класса, способного производить очистку и освобождать внутрен­
ние неуправляемые ресурсы. Первый подход заключается в переопределении метода
System. Object. Finalize () и позволяет гарантировать то, что объект будет очищать
себя сам во время процесса сборки мусора (когда бы тот не запускался) без вмешатель­
ства со стороны пользователя. Второй подход предусматривает реализацию интерфей­
са IDisposable и позволяет обеспечить пользователя объекта возможностью очищать
объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать
метод Dispose ( ) , неуправляемые ресурсы могут оставаться в памяти на неопределен­
ный срок.
Как не трудно догадаться, оба подхода можно комбинировать и применять вместе
в определении одного класса, получая преимущества от обеих моделей. Если пользо­
312
Часть II. Главные конструкции программирования на C#
ватель объекта не забыл вызвать метод Dispose () , можно проинформировать сбор­
щик мусора о пропуске финализации, вызвав метод G C .SuppressFinalize () . Если
же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут
финализации и получит возможность освободить внутренние ресурсы. Преимущество
такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда
освобождаться.
Ниже приведена очередная версия класса MyResourceWrapper, которая теперь пре­
дусматривает выполнение и финализации и освобождения и содержится внутри проек­
та типа Console Application по имени FinalizableDisposableClass.
// Сложный упаковщик ресурсов,
public class MyResourceWrapper : IDisposable
{
// Сборщик мусора будет вызывать этот метод, если
// пользователь объекта забыл вызвать метод Dispose () .
~MyResourceWrapper()
{
// Освобождение любых внутренних неуправляемых
// ресурсов. Метод Dispose () НЕ должен вызываться
/ / н и для каких управляемых объектов.
// Пользователь объекта будет вызывать этот метод для
// того, чтобы освободить ресурсы как можно быстрее,
public void Dispose ()
{
// Здесь осуществляется освобождение неуправляемых ресурсов
/ / и вызов Dispose()для остальных высвобождаемых объектов.
// Если пользователь вызвал Dispose (), то финализация не нужна,
// поэтому далее она подавляется.
G C .SuppressFinalize(this);
}
Здесь важно обратить внимание на то, что метод Dispose () был модифицирован
так, чтобы вызывать метод GC. SuppressFinalize () . Этот метод информирует CLRсреду о том, что вызывать деструктор при подвергании данного объекта сборке мусора
больше не требуется, поскольку неуправляемые ресурсы уже были освобождены посред­
ством логики Dispose ().
Формализованный шаблон очистки
В текущей реализации MyResourceWrapper работает довольно хорошо, но все рав­
но еще остается несколько небольших недочетов. Во-первых, методам Finalize () и
Dispose () требуется освобождать одни и те же неуправляемые ресурсы, а это чрева­
то дублированием кода, которое может существенно усложнить его сопровождение.
Поэтому в идеале не помешало бы определить приватную вспомогательную функцию,
которая могла бы вызываться в любом из этих методов.
Во-вторых, нелишне позаботиться о том, чтобы метод Finalize () не пытался из­
бавиться от любых управляемых объектов, а метод Dispose () — наоборот, обязательно
это делал. И, наконец, в-третьих, не помешало бы позаботиться о том, чтобы пользо­
ватель объекта мог спокойно вызывать метод Dispose () множество раз без получения
ошибки. В настоящий момент в методе Dispose () никаких подобных мер предосторож­
ностей пока не предусмотрено.
Для решения подобных вопросов с дизайном в Microsoft создали формальный шаб­
лон очистки, который позволяет достичь оптимального баланса между надежностью,
Глава 8. Время жизни объектов
313
удобством в обслуживании и производительностью. Ниже приведена окончательная
версия MyResourceWrapper, в которой применяется упомянутый формальный шаблон.
public class MyResourceWrapper : IDisposable
{
// Используется для выяснения того, вызывался ли уже метод Dispose () .
private bool disposed = false;
public void Dispose ()
{
// Вызов вспомогательного метода.
// Значение true указывает на то, что очистка
// была инициирована пользователем объекта.
Cleanup(true);
// Подавление финализации.
GC.SuppressFinalize (this);
}
private void Cleanup(bool disposing)
(
// Проверка, выполнялась ли очистка,
if (! this.disposed)
(
// Если disposing равно true, должно осуществляться
// освобождение всех управляемых ресурсов,
if (disposing)
{
// Здесь осуществляется освобождение управляемых ресурсов.
}
// Очистка неуправляемых ресурсов.
}
disposed = true;
}
-MyResourceWrapper()
{
// Вызов вспомогательного метода.
// Значение false указывает на то, что
// очистка была инициирована сборщиком мусора.
Cleanup(false);
}
}
Обратите внимание, что в MyResourceWrapper теперь определяется приватный вспо­
могательный метод по имени Cleanup ( ). Передача ему в качестве аргумента значения
true свидетельствует о том, что очистку инициировал пользователь объекта, следова­
тельно, требуется освободить все управляемые и неуправляемые ресурсы. Когда очистка
инициируется сборщиком мусора, при вызове Cleanup () передается значение false,
чтобы освобождения внутренних высвобождаемых объектов не происходило (поскольку
рассчитывать на то, что они по-прежнему находятся в памяти, нельзя). И, наконец, пе­
ред выходом из Cleanup () для переменной экземпляра типа bool (по имени disposed)
устанавливается значение true, что дает возможность вызывать метод Dispose () мно­
го раз без появления ошибки.
На заметку! После "освобождения" (dispose) объекта клиент по-прежнему может вызывать на нем
какие-нибудь члены, поскольку объект пока еще находится в памяти. Следовательно, в пока­
занном сложном классе-упаковщике ресурсов не помешало бы снабдить каждый член допол­
нительной логикой, которая бы, по сути, гласила: “если объект освобожден, ничего не делать,
а просто вернуть управление".
314
Часть II. Главные конструкции программирования на C#
Чтобы протестировать последнюю версию класса MyResourceW rapper, добавим в
метод финализации вызов C o n s o le . Веер ( ):
-MyResourceWrapper ()
{
Console.Веер ();
// Вызов вспомогательного метода.
// Указание значения fa ls e свидетельствует о том,
// что очистка была инициирована сборщиков мусора.
Cleanup(false);
}
Давайте обновим метод Main () следующим образом:
static void Main(string [] args)
{
Console.WriteLine (''***** Dispose () / Destructor Combo Platter *****");
// Вызов метода D ispose() вручную; метод финализации
/ / в таком случае вызываться не будет.
MyResourceWrapper rw = new MyResourceWrapper();
rw.Dispose ();
// Пропуск вызова метода D ispose( ) ; в таком случае будет
// вызываться метод финализации и выдаваться звуковой сигнал.
MyResourceWrapper rw2 = new MyResourceWrapper();
Обратите внимание, что в первом случае производится явный вызов метода
D ispose () на объекте rw и потому вызов деструктора подавляется. Однако во втором
случае мы “забываем” вызвать метод D ispose () на объекте rw2 и потому по окончании
выполнения приложения услышим однократный звуковой сигнал. Если закомментиро­
вать вызов метода D isp o seO на объекте rw, звуковых сигналов будет два.
Исходный код. Проект F in a liz a b le D is p o s a b le C la s s доступен в подкаталоге Chapter 8.
На этом тема управления объектами CLR-средой посредством сборки мусора за­
вершена. И хотя некоторые дополнительные (и довольно экзотические) детали, касаю­
щиеся сборки мусора (вроде слабых ссылок и восстановления объектов), остались не
рассмотренными, полученных базовых сведений должно оказаться достаточно, чтобы
продолжить изучение самостоятельно. Напоследок в главе предлагается исследовать
совершенно новую функциональную возможность, появившуюся в .NET 4.0, которая
называется отложенной (ленивой) инициализацией.
Отложенная инициализация объектов
На заметку! В настоящем разделе предполагается наличие у читателя знаний о том, что собой
представляют обобщения и делегаты в .NET. Исчерпывающие сведения о делегатах и обобще­
ниях можно найти в главах 10 и 11.
При создании классов иногда возникает необходимость предусмотреть в коде опреде­
ленную переменную-член, которая на самом деле может никогда не понадобиться из-за
того, что пользователь объекта не будет обращаться к методу (или свойству), в котором
она используется. Вполне разумное решение, однако, на практике его реализация мо­
жет оказываться очень проблематичной в случае, если инициализация интересующей
переменной экземпляра требует большого объема памяти.
Глава 8. Время жизни объектов
315
Для примера представим, что требуется создать класс, инкапсулирующий операции
цифрового музыкального проигрывателя, и помимо ожидаемых методов вроде P la y ( ) ,
Pause () и Stop () его нужно также обеспечить способностью возврата коллекции объ­
ектов Song (через класс по имени A llT r a c k s ), которые представляют каждый из имею­
щихся в устройстве цифровых музыкальных файлов.
Чтобы получить такой класс, создадим новый проект типа Console Application по
имени L a z y O b je c t I n s t a n t ia t io n и добавим в него следующие определения типов
классов:
// Представляет одну композицию.
class Song
{
public string Artist { get; set; }
public string TrackName { get; set; }
public double TrackLength { get; set; }
}
// Представляет все композиции в проигрывателе.
class AllTracks
{
//В нашем проигрывателе может содержаться
// максимум 10 000 композиций.
public AllTracks ()
{
// Предполагаем, что здесь производится заполнение
// массива объектов Song.
Console .WnteLine ("Filling up the songs!");
}
}
// Класс MediaPlayer включает объект AllTracks.
class MediaPlayer
{
// Предполагаем, что эти методы делают нечто полезное.
public void Play() { /* Воспроизведение композиции */ }
public void Pause() { /* Приостановка воспроизведения композиции */ }
public void Stop() { /* Останов воспроизведения композиции */ }
private AllTracks allSongs = new AllTracks ();
public AllTracks GetAllTracks ()
{
// Возвращаем все композиции.
return allSongs;
В текущей реализации M ed ia P la y er делается предположение о том, что пользовате­
лю объекта понадобится получать список объектов с помощью метода G e tA llT ra c k s ( ).
А что если пользователю объекта не нужен этот список? Так или иначе, но переменная
экземпляра A llT ra c k s будет приводить к созданию 10 000 объектов Song в памяти:
static void Main(string [] args)
{
//В этом вызывающем коде получение всех композиций не
// производится, но косвенно все равно создаются
// 10 000 объектов!
MediaPlayer myPlayer = new MediaPlayer();
myPlayer.Play();
Console.ReadLine ();
316
Часть II. Главные конструкции программирования на C#
Понятно, что создания 10 000 объектов, которыми никто не будет пользоваться, луч­
ше избежать, так как это изрядно прибавит работы сборщику мусора .NET. Хотя можно
вручную добавить код, который обеспечит создание объекта all Songs только в случае
его использования (например, за счет применения шаблона с методом фабрики), суще­
ствует и более простой путь.
С выходом .NET 4.0 в библиотеках базовых классов появился очень интересный
обобщенный класс по имени Lazyo, который находится в пространстве имен System
внутри сборки mscorlib.dll. Этот класс позволяет определять данные, которые не
должны создаваться до тех пор, пока они на самом деле не начнут использоваться в
кодовой базе. Поскольку он является обобщенным, при первом использовании в нем
должен быть явно указан тип элемента, который должен создаваться. Этим типом мо­
жет быть как любой из типов, определенных в библиотеках базовых классов .NET, так
и специальный тип, самостоятельно созданный разработчиком. Для обеспечения от­
ложенной инициализации переменной экземпляра AllTracks можно просто заменить
следующий фрагмент кода:
// Класс MediaPlayer включает объект AllTracks
class MediaPlayer
private AllTracks allSongs = new AllTracks ();
public AllTracks GetAllTracks()
{
// Возврат всех композиций.
return allSongs;
таким кодом:
// Класс MediaPlayer включает объект Lazy<AllTracks>.
class MediaPlayer
private Lazy<AllTracks> allSongs = new Lazy<AllTracks>();
public AllTracks GetAllTracks ()
{
// Возврат всех композиций.
return allSongs.Value;
}
Помимо того, что переменная экземпляра A l l Track теперь имеет тип L a z y o , важно
отметить, что реализация предыдущего метода G etA llT ra c k s () тоже изменилась. В ча­
стности, теперь требуется использовать доступное только для чтения свойство Value
класса L a z y o для получения фактических хранимых данных (в этом случае — объект
A llT r a c k s , обслуживающий 10 000 объектов Song).
Кроме того, обратите внимание, как благодаря этому простому изменению, пока­
занный ниже модифицированный метод Ma i n( ) будет незаметно размещать объек­
ты Song в памяти только в случае, когда был действительно выполнен вызов метода
G e t A l l T r a c k s ().
static void Main(string [] args)
{
Console .WriteLine (''***** Fun with Lazy Instantiation *****\n ");
// Никакого размещения объекта A llTracks!
MediaPlayer myPlayer = new MediaPlayer();
myPlayer.Play();
Глава 8. Время жизни объектов
317
// Размещение объекта AllTracks происходит
// только в случае вызова метода G etA llT racks( ) .
MediaPlayer yourPlayer = new MediaPlayer();
AllTracks yourMusic = yourPlayer.GetAllTracks();
Console.ReadLine();
}
Настройка процесса создания данных L a z y o
При объявлении переменной L a z y o фактический внутренний тип данных создает­
ся с помощью конструктора по умолчанию:
// Конструктор по умолчанию для AllTracks вызывается
// при использовании переменной LazyO .
private Lazy<AllTracks> allSongs = new Lazy<AllTracks>();
В некоторых случаях подобное поведение может быть вполне подходящим, но что
если класс AllTracks имеет дополнительные конструкторы, и нужно позаботиться о
том, чтобы вызывался подходящий из них? Более того, а что если необходимо, чтобы
при создании переменной Lazy () выполнялась какая-то дополнительная работа (поми­
мо просто создания объекта AllTracks)? К счастью, класс Lazy () позволяет предостав­
лять в качестве необязательного параметра обобщенный делегат, который указывает,
какой метод должен вызываться во время создания находящегося внутри него типа.
Этим обобщенным делегатом является тип System. Funco, который может указы­
вать на метод, возвращающий тот же тип данных, что создается соответствующей пе­
ременной Lazyo, и способный принимать вплоть до 16 аргументов (которые типизиру­
ются с помощью параметров обобщенного типа). В большинстве случаев необходимость
указывать параметры для передачи методу, на который ссылается Funco, возникать
не будет Более того, чтобы значительно упростить применение F u n c o , лучше исполь­
зовать лямбда-выражения (отношения между делегатами и лямбда-выражениями под­
робно рассматриваются в главе 11).
С учетом всего сказанного, ниже приведена окончательная версия MediaPlayer, в
которой теперь при создании внутреннего объекта AllTracks добавляется небольшой
специальный код. Следует запомнить, что данный метод должен обязательно возвра­
щать новый экземпляр указанного в L a z y o типа перед выходом, а использовать мож­
но любой конструктор (в коде для AllTracks по-прежнему вызывается конструктор по
умолчанию).
class MediaPlayer
// Использование лямбда-выражения для добавления
// дополнительного кода при создании объекта A llT ra c k s.
private Lazy<AllTracks> allSongs = new Lazy<AllTracks> ( () =>
{
Console .WnteLine ("Creating AllTracks object!");
return new AllTracks ();
public AllTracks GetAllTracks ()
{
// Возврат всех композиций.
return allSongs.Value;
318
Часть II. Главные конструкции программирования на C#
Хочется надеяться, что удалось продемонстрировать выгоду, которую способен
обеспечить класс Lazyo. В сущности, этот новый обобщенный класс позволяет де­
лать так, чтобы дорогостоящие объекты размещались в памяти только тогда, когда
они будут действительно нужны пользователю объекта. Если эта тема заинтересовала,
можно заглянуть в посвященный классу System.L a z y o раздел в документации .NET
Framework 4.0 SDK и найти там дополнительные примеры программирования отложен­
ной инициализации.
Исходный код. Проект LazyObjectInstantiation доступен в подкаталоге Chapter
8.
Резюме
Цель настоящей главы состояла в разъяснении, что собой представляет процесс
сборки мусора. Было показано, что сборщик мусора активизируется только тогда, ко­
гда ему не удается получить необходимый объем памяти из управляемой кучи (или ко­
гда происходит выгрузка домена соответствующего приложения из памяти). После его
активизации поводов для волнения возникать не должно, поскольку разработанный в
Microsoft алгоритм сборки мусора хорошо оптимизирован и предусматривает исполь­
зование поколений объектов, дополнительных потоков для финализации объектов и
управляемой кучи для обслуживания больших объектов.
В главе также было показано, каким образом программно взаимодействовать со
сборщиком мусора, применяя класс System.GC. Как отмечалось, единственным случа­
ем, когда в подобном может возникнуть необходимость, является создание финализируемых или высвобождаемых типов классов, в которых используются неуправляемые
ресурсы.
Вспомните, что под финализируемыми типами подразумеваются классы, в кото­
рых переопределен виртуальный метод System.Object.Finalize () для обеспечения
очистки неуправляемых ресурсов на этапе сборки мусора, а под высвобождаемыми —
классы (или структуры), в которых реализован интерфейс IDisposable, вызываемый
пользователем объекта по окончании работы с ним. Кроме того, в главе был продемон­
стрирован формальный шаблон “очистки”, позволяющий совмещать оба эти подхода.
И, наконец, в главе был описан появившийся в .NET 4.0 обобщенный класс по име­
ни Lazyo. Как здесь было показано, данный класс позволяет отложить создание до­
рогостоящих (в плане потребления памяти) объектов до тех пор, пока у вызывающей
стороны действительно не возникнет потребность в их использовании. Это помогает
сократить количество объектов, сохраняемых в управляемой куче, и облегчает нагрузку
на сборщик мусора.
ЧАСТЬ
III
Дополнительные
конструкции
программирования
на C#
В этой ч а сти ...
Глава 9. Работа с интерфейсами
Глава 10. Обобщения
Глава 11. Делегаты, события и лямбда-выражения
Глава 12. Расширенные средства языка C#
Глава 13. LINQ to Objects
ГЛАВА
9
Работа с интерфейсами
атериал этой главы основан на начальных знаниях объектно-ориентированной
разработки и посвящен концепциям программирования с использованием ин­
терфейсов. В главе будет показано, как определять и реализовать интерфейсы, а такж
описаны преимущества, которые дает построение типов, поддерживающих несколько
видов поведения. Также будут рассмотрены и другие связанные с этим темы, наподобие
того, как получать ссылки на интерфейсы, как реализовать интерфейсы явным образом
и как создавать иерархии интерфейсов.
Помимо этого, конечно же, будут описаны стандартные интерфейсы, которые пред­
лагаются в библиотеках базовых классов .NET. Как будет показано, специальные классы
и структуры могут реализовать эти готовые интерфейсы, что позволяет поддерживать
несколько дополнительных поведений, таких как клонирование, перечисление и сорти­
ровка объектов.
М
Что собой представляют типы интерфейсов
Для начала ознакомимся с формальным определением типа интерфейса. Интер­
фейс (interface) представляет собой не более чем просто именованный набор абстракт­
ных членов. Как упоминалось в главе 6, абстрактные методы являются чистым про­
токолом, поскольку не имеют никакой стандартной реализации. Конкретные члены,
определяемые интерфейсом, зависят от того, какое поведение моделируется с его по­
мощью. Это действительно так. Интерфейс выражает поведение, которое данный класс
или структура может избрать для поддержки. Более того, как будет показано далее в
главе, каждый класс (или структура) может поддерживать столько интерфейсов, сколько
необходимо, и, следовательно, тем самым поддерживать множество поведений.
Нетрудно догадаться, что в библиотеках базовых классов .NET поставляются сотни
предопределенных типов интерфейсов, которые реализуются в различных классах и
структурах. Например, как будет показано в главе 21, в состав ADO.NET входит мно­
жество поставщиков данных, которые позволяют взаимодействовать с определенной
системой управления базами данных. Это означает, что в ADO.NET на выбор доступно
множество объектов соединения (SqlConnection, OracleConnection, OdbcConnection
и Т.Д.).
Несмотря на то что каждый из этих объектов соединения имеет уникальное имя,
определяется в отдельном пространстве имен и (в некоторых случаях) упаковывается в
отдельную сборку, все они реализуют один общий интерфейс IDbConnection:
// Интерфейс IDbConnection имеет типичный ряд членов,
// которые поддерживают все объекты соединения.
public interface IDbConnection : IDisposable
{
Глава 9. Работа с интерфейсами
321
// Методы
IDbTransaction BeginTransaction ();
IDbTransaction BeginTransaction(IsolationLevel ll);
void ChangeDatabase(string databaseName);
void Close (_) ;
IDbCommand CreateCommand();
void Open ();
// Свойства
string Connectionstring { get; set;}
int ConnectionTimeout { get; }
string Database { get; }
ConnectionState State { get; }
На заметку! По соглашению имена всех интерфейсов в .NET сопровождаются префиксом в виде
заглавной буквы Т . При создании собственных специальных интерфейсов рекомендуется тоже
следовать этому соглашению.
Вдумываться, что именно делают все эти члены, пока не нужно. Сейчас главное про­
сто понять, что в интерфейсе IDbConnection предлагается набор членов, которые яв­
ляются общими для всех объектов соединений ADO.NET. Исходя из этого, можно точно
знать, что каждый объект соединения поддерживает такие члены, как Open (), Close (),
CreateCommand () и т.д. Более того, поскольку методы этого интерфейса всегда являют­
ся абстрактными, в каждом объекте соединения они могут быть реализованы собствен­
ным уникальным образом.
Другим примером может служить пространство имен System.Windows .Forms. В этом
пространстве имени определен класс по имени Control, который является базовым
для целого ряда интерфейсных элементов управления Windows Forms (DataGridView,
Label, StatusBar, TreeView и т.д.). Этот класс реализует интерфейс IDropTarget, оп­
ределяющий базовую функциональность перетаскивания:
public interface IDropTarget
{
// Методы
void OnDragDrop(DragEventArgs e);
void OnDragEnter(DragEventArgs e);
void OnDragLeave(EventArgs e);
void OnDragOver(DragEventArgs e);
С учетом этого интерфейса можно верно предполагать, что любой класс, который
расширяет System.Windows .Forms .Control, будет поддерживать четыре метода с име­
нами OnDragDrop(), OnDragEnter() ,OnDragLeave() и OnDragOver() .
В остальной части книги будут встречаться десятки таких интерфейсов, поставляе­
мых в библиотеках базовых классов .NET. Эти интерфейсы могут быть реализованы
в собственных классах и структурах, чтобы получать типы, тесно интегрированные с
.NET.
Сравнение интерфейсов и абстрактных базовых классов
Из-за материала, приведенного в главе 6, тип интерфейса может показаться очень
похожим на абстрактный базовый класс. Вспомните, что когда класс помечается как
абстрактный, в нем может определяться любое количество абстрактных членов для
предоставления полиморфного интерфейса для всех производных типов. Даже если в
типе класса действительно определяется набор абстрактных членов, в нем также может
322
Часть III. Дополнительные конструкции программирования на C#
спокойно определяться любое количество конструкторов, полей, неабстрактных членов
(с реализацией) и т.п. Интерфейсы, с другой стороны, могут содержать только опреде­
ления абстрактных членов.
Полиморфный интерфейс, предоставляемый абстрактным родительским классом,
обладает одним серьезным ограничением: определяемые в абстрактном родительском
классе члены поддерживаются только в производных типах. В более крупных про­
граммных системах, однако, довольно часто разрабатываются многочисленные иерар­
хии классов, не имеющие никаких общих родительских классов кроме System.Object.
Из-за того, что абстрактные члены в абстрактном базовом классе подходят только для
производных типов, получается, что настраивать типы в разных иерархиях так, чтобы
они поддерживали один и тот же полиморфный интерфейс, невозможно. Для примера
предположим, что определен следующий абстрактный класс:
abstract class CloneableType
{
// Только производные типы могут поддерживать этот
// "полиморфный интерфейс". Классы в других иерархиях
//н е будут иметь доступа к этому абстрактному члену.
public abstract object Clone();
}
Из-за такого определения поддерживать метод Clone () могут только члены, которые
расширяют CloneableType. В случае создания нового набора классов, которые не рас­
ширяют CloneableType, использовать в них этот полиморфный интерфейс, соответст­
венно, не получится. Кроме того, как уже неоднократно упоминалось, в C# не поддержи­
вается возможность наследования от нескольких классов. Следовательно, создать класс
MiniVan, унаследованный и от Саг, и от CLoneableType, тоже не получится:
// В C# наследование от нескольких классов не допускается.
public class MiniVan : Car, CloneableType
Нетрудно догадаться, что здесь на помощь приходят типы интерфейсов. После оп­
ределения интерфейс может быть реализован в любом типе или структуре, в любой ие­
рархии и внутри любого пространства имен или сборки (написанной на любом языке
программирования .NET). Очевидно, что это делает интерфейсы чрезвычайно полиморф­
ными. Для примера рассмотрим стандартный интерфейс .NET по имени ICloneable,
определенный в пространстве имен System. Этот интерфейс имеет единственный метод
Clone ():
public interface ICloneable
{
object Clone ();
Если заглянуть в документацию .NET Framework 4.0 SDK, можно обнаружить, что
очень многие по виду несвязанные типы (System.Array, System.Data .SqlClient.
SqlConnection, System.OperatingSystem, System.String и т.д.) реализуют этот ин­
терфейс. В результате, хотя у этих типов нет общего родителя (кроме System. Object), с
ними все равно можно работать полиморфным образом через интерфейс ICloneable.
Например, если есть метод по имени С1опеМе(), принимающий интерфейс
ICloneable в качестве параметра, этому методу можно передавать любой объект, кото­
рый реализует упомянутый интерфейс. Рассмотрим следующий простой класс Program,
определенный в проекте типа Console Application (Консольное приложение) по имени
ICloneableExample:
Глава 9. Работа с интерфейсами
323
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** A First Look at Interfaces *****\n");
// Все эти классы поддерживают интерфейс ICloneable.
string myStr = "Hello";
OperatingSystem unixOS =
new OperatingSystem(PlatformID.Unix, new Version ());
System.Data.SqlClient.SqlConnection sqlCnn =
new System.Data.SqlClient.SqlConnection();
// Поэтому все они могут передаваться методу,
// принимающему ICloneable в качестве параметра.
CloneMe(myStr);
CloneMe(unixOS);
CloneMe(sqlCnn);
Console.ReadLine ();
}
private static void CloneMe(ICloneable c)
{
// Клонируем любой получаемый тип
/ / и выводим его имя в окне консоли.
object theClone = c.CloneO;
Console.WriteLine("Your clone is a: {0}",
theClone.GetType().Name);
}
При запуске этого приложения в окне консоли будет выводиться имя каждого класса
посредством метода GetType ( ) , унаследованного от System.Object. (В главе 16 более
подробно рассказывается об этом методе и предлагаемых в .NET службах рефлексии.)
Исходный код. Проект ICloneableExample доступен в подкаталоге Chapter 9.
Другим ограничением традиционных абстрактных базовых классов является то, что
в каждом производном типе должен обязательно поддерживаться соответствующий на­
бор абстрактных членов и предоставляться их реализация. Чтобы увидеть, в чем заклю­
чается проблема, давайте вспомним иерархию фигур, которая приводилась в главе 6.
Предположим, что в базовом классе Shape определен новый абстрактный метод по име­
ни GetNumberOf Points () , позволяющий производным типам возвращать информацию
о количестве вершин, которое требуется для визуализации фигуры:
abstract class Shape
// Каждый производный класс теперь должен
// обязательно поддерживать такой метод!
public abstract byte GetNumberOfPoints();
}
Очевидно, что изначально единственным типом, который в принципе имеет верши­
ны, является Hexagon. Но теперь, из-за внесенного обновления, каждый производный
класс (Circle, Hexagon и ThreeDCircle) должен предоставлять конкретную реализа­
цию метода GetNumberOf Points ( ) , даже если в этом нет никакого смысла.
В этом случае снова на помощь приходит тип интерфейса. Определив интерфейс,
представляющий поведение “наличие вершин”, можно будет просто вставить его в тип
Hexagon и не трогать типы Circle и ThreeDCircle.
324
Часть III. Дополнительные конструкции программирования на C#
Определение специальных интерфейсов
Теперь, имея более четкое представление о роли интерфейсов, давайте рассмотрим
пример определения и реализации специальных интерфейсов. Создадим новый проект
типа Console Application по имени Customlnterface и, выбрав в меню Project (Проект)
пункт Add Existing Item (Добавить существующий элемент), вставим в него файл или
файлы, содержащие определения типов фигур (файл Shapes.cs в примерах кода), ко­
торые были созданы в главе 6. После этого переименуем пространство имен, в котором
содержатся определения отвечающих за фигуры типов, в Customlnterface (просто что­
бы не импортировать их из этого пространства имен в новый проект):
namespace Customlnterface
{
// Здесь должны идти определения
// созданных ранее типов фигур...
}
Теперь вставим в проект новый интерфейс по имени IPointy, выбрав в меню Project
пункт Add Existing Item, как показано на рис. 9.1.
Рис. 9.1. Интерфейсы, как и классы, могут определяться в любом файле * .cs
На синтаксическом уровне любой интерфейс определяется с помощью ключевого
слова interface. В отличие от классов, базовый класс (даже System.Object) для ин­
терфейсов никогда не указывается (хотя, как будет показано позже в главе, базовые
интерфейсы указываться могут). Более того, модификаторы доступа для членов интер­
фейсов тоже никогда не указываются (поскольку все члены интерфейсов всегда явля­
ются неявно общедоступными и абстрактными). Ниже показано, как должно выглядеть
определение специального интерфейса IPointyHaC#:
// Этот интерфейс определяет поведение "наличие вершин".
public interface IPointy
{
// Этот член является неюно общедоступным и абстрактным.
byte GetNumberOfPoints ();
Глава 9. Работа с интерфейсами
325
Вспомните, что при определении членов интерфейсов область их реализации не
задается. Интерфейсы являются чистым протоколом, и потому реализация в них ни­
когда не предоставляется (за это отвечает поддерживающий класс или структура).
Следовательно, использование показанной ниже версии IPointy привело бы к возник­
новению различных ошибок на этапе компиляции:
// Внимание! Полно ошибок!
public interface IPointy
// Ошибка! Интерфейсы не могут иметь поля!
public int numbOfPoints;
// Ошибка! Интерфейсы не могут иметь конструкторы!
public IPointy() { numbOfPoints = 0;};
// Ошибка! В интерфейсах не может предоставляться
// реализация методов!
byte GetNumberOfPoints() { return numbOfPoints; }
}
В любом случае, в начальном интерфейсе IPointy определен только один метод.
Однако в .NET допустимо определять в типах интерфейсов любое количество прототи­
пов свойств. Например, можно было бы создать интерфейс IPointy так, чтобы в нем
использовалось доступное только для чтения свойство, а не традиционный метода
доступа:
// Определение в IPointy свойства, доступного только для чтения.
public interface IPointy
{
// Свойство, доступное как для чтения, так и для записи,
// в этом интерфейсе может выглядеть так:
// retV al PropName { get; se t; }
// а свойство, доступное только для записи - так:
// retV al PropName { set; }
byte Points { get; }
На заметку! Типы интерфейсов также могут содержать определения событий (см. главу 11) и ин­
дексаторов (см. главу 12).
Сами по себе типы интерфейсов довольно бесполезны, поскольку представляют
собой не более чем просто именованную коллекцию абстрактных членов. Например,
размещать типы интерфейсов таким же образом, как классы или структуры, не
разрешается:
// Внимание! Размещать типы интерфейсов не допускается!
static void Main(string[] args)
{
IPointy p = new IPointy();
// Компилятор сообщит об ошибке!
}
Интерфейсы ничего особого не дают, если не реализуются в каком-то классе или
структуре. Здесь IPointy представляет собой интерфейс, который выражает поведение
“наличие вершин”. Стоящая за его созданием идея выглядит довольно просто: некото­
рые классы в иерархии фигур (например, Hexagon) должны иметь вершины, а некото­
рые (вроде Circle) — нет.
326
Часть III. Дополнительные конструкции программирования на C#
Реализация интерфейса
Чтобы расширить функциональность какого-то класса или структуры за счет обес­
печения в нем поддержки интерфейсов, необходимо предоставить в его определении
список соответствующих интерфейсов, разделенных запятыми. Следует иметь в виду,
что непосредственный базовый класс должен быть обязательно перечислен в этом
списке первым, сразу же после двоеточия. Когда тип класса наследуется прямо от
S ystem .O b ject, допускается перечислять только лишь поддерживаемый им интерфейс
или интерфейсы, поскольку компилятор C# автоматически расширяет типы возможно­
стями S y s te m .O b je c t в случае, если не было указано иначе. Из-за того, что структу­
ры всегда наследуются от класса S ystem .V alueType (см. главу 4), интерфейсы просто
должны перечисляться после определения структуры. Ниже приведены примеры.
// Этот класс унаследован от System.Object
/ / и реализует единственный интерфейс.
public class Pencil : IPointy
{ . . •}
// Этот класс тоже унаследован от System.Object
/ / и реализует единственный интерфейс.
public class SwitchBlade : object, IPointy
{ .. . }
// Этот класс унаследован от специального базового
// класса и реализует единственный интерфейс.
public class Fork : Utensil, IPointy
// Эта структура неюно унаследована от System.ValueType
/ / и реализует два интерфейса.
public struct Arrow : IClonable, IPointy
{ ...}
Важно понимать, что реализация интерфейса работает по принципу “все или ниче­
го”. Поддерживающий тип не способен выбирать, какие члены должны быть реализо­
ваны, а какие — нет. Из-за того, что в интерфейсе IPointy определено лишь одно дос­
тупное только для чтения свойство, нагрузка на поддерживающий тип получается не
такой уж большой. Однако в случае реализации интерфейса с десятью членами (такого
как показанный ранее IDbConnection) типу придется отвечать за воплощение деталей
всех десяти абстрактных сущностей.
Давайте вернемся к рассматриваемому примеру и добавим в него новый тип класса
по имени Triangle, унаследованный от Shape и поддерживающий интерфейс IPointy.
Обратите внимание, что реализация доступного только для чтения свойства Points в
нем предусматривает просто возврат соответствующего количества вершин (в данном
случае 3).
// Новый производный от Shape класс по имени T rian gle.
class Triangle : Shape, IPointy
{
public Triangle () { }
public Triangle(string name) : base(name) { }
public override void Draw()
{ Console.WnteLine ("Drawing {0} the Triangle", PetName) ; }
Глава 9. Работа с интерфейсами
//
327
Р еали зац и я и н тер ф ей са I P o in t y .
public byte Points
{
get { return 3; }
}
}
Модифицируем существующий тип Hexagon так, чтобы он тоже поддерживал интер­
фейс IPointy:
/ / H exagon те п е р ь р е а л и з у е т I P o in t y .
class Hexagon : Shape, IPointy
{
public Hexagon () { }
public Hexagon(string name) : base(name){ }
public override void Draw()
{ Console.WriteLine("Drawing {0} the Hexagon", PetName); }
}
//
Р еал и зац и я и н тер ф ей са I P o in t y .
public byte Points
{
get { return 6; }
}
}
Чтобы подвести итог всему изученному к этому моменту, на рис. 9.2 приведена соз­
данная с помощью Visual Studio 2010 диаграмма классов, на которой все совместимые
с IPointy классы представлены с помощью обозначения в виде “леденца на палочке”.
Обратите внимание на диаграмме, что в Circle и ThreeDCircle интерфейс IPointy
не реализован, потому что предоставляемое им поведение для этих классов не имеет
смысла.
Рис. 9.2. Иерархия фигур, теперь с интерфейсами
На заметку! Чтобы скрыть или отобразить имена интерфейсов в визуальном конструкторе классов,
щелкните правой кнопкой мыши на значке, представляющем интерфейс, и выберите в контек­
стном меню пункт C o lla p se (Свернуть) или E xpand (Развернуть).
328
Часть III. Дополнительные конструкции программирования на C#
Вызов членов интерфейса на уровне объектов
Теперь, когда уже есть несколько классов, поддерживающих интерфейс IPointy, да­
вайте посмотрим, как взаимодействовать с новой функциональностью. Самый простой
способ взаимодействия с функциональными возможностями, предлагаемыми заданным
интерфейсом, предусматривает вызов его членов прямо на уровне объектов (при усло­
вии, что члены этого интерфейса не реализованы явным образом, о чем более подробно
рассказывается в разделе “Устранение конфликтов на уровне имен за счет реализации
интерфейсов явным образом” далее в главе). Например, рассмотрим следующий метод
Main ():
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Interfaces *****\n");
// Вызов свойства Points, определенного в IPointy.
Hexagon hex = new Hexagon();
Console.WriteLine ("Points: {0}", hex.Points); // вывод числа вершин
Console.ReadLine ();
}
Такой подход конкретно в данном случае вполне подходит, поскольку здесь точно
известно, что в типе Hexagon был реализован запрашиваемый интерфейс и, следова­
тельно, поддерживается свойство Points. Однако в других случаях определить, какие
интерфейсы поддерживает данный тип, может быть невозможно. Например, предпо­
ложим, что имеется массив, содержащий 50 совместимых с Shape типов, при этом ин­
терфейс IPointy поддерживает только частью из них. Очевидно, что при попытке вы­
звать свойство Points для типа, в котором не был реализован IPointy, будет возникать
ошибка. Как динамически определить, поддерживает ли класс или структура нужный
интерфейс?
Одним из способов для определения во время выполнения того, поддерживает ли тип
конкретный интерфейс, является применение операции явного приведения. В случае
если тип не поддерживает запрашиваемый интерфейс, будет генерироваться исключе­
ние InvalidCastException, для аккуратной обработки которого можно использовать
методику структурированной обработки исключений, как, например, показано ниже:
static void Main(string[] args)
// Перехват возможного исключения InvalidCastException.
Circle с = new Circle ("Lisa");
IPointy ltfPt = null;
try
{
ltfPt = (IPointy)c;
Console.WriteLine(itfPt.Points);
}
catch (InvalidCastException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine ();
}
Разумеется, можно использовать логику try/catch и надеяться на лучшее, но в
идеале все-таки правильнее выяснять, какие интерфейсы поддерживаются, перед вы­
зовом их членов. Давайте рассмотрим два способа, которыми это можно делать.
Глава 9. Работа с интерфейсами
329
Получение ссылок на интерфейсы с помощью ключевого слова a s
Определить, поддерживает ли данный тип тот или иной интерфейс, можно с исполь­
зованием ключевого слова as, которое впервые рассматривалось в главе 6. Если объект
удается интерпретировать как указанный интерфейс, то возвращается ссылка на инте­
ресующий интерфейс, а если нет, то ссылка n u ll. Следовательно, перед продолжением
в коде необходимо предусмотреть проверку на n u ll:
static void Main(string[] args)
// Можно ли интерпретировать hex2 как I Pointy?
Hexagon hex2 = new Hexagon("Peter");
IPointy itfPt2 = hex2 as IPointy;
if (ltfPt2 != null)
// Вывод числа вершин.
Console.WriteLine ("Points: {0}", itfPt2.Points) ;
else
// Это не интерфейс IPointy.
Console.WriteLine ("OOPS! Not pointy...");
Console.ReadLine ();
Обратите внимание, что в случае применения ключевого слова as использовать л о ­
гику t r y / c a tc h нет никакой необходимости, поскольку возврат ссылки, отличной от
n u ll, означает, что вызов осуществляется с использованием действительной ссылки на
интерфейс.
Получение ссылок на интерфейсы с помощью ключевого слова i s
Проверить, был ли реализован нужный интерфейс, можно также с помощью клю­
чевого слова i s (которое тоже впервые упоминалось в главе 6). Если запрашиваемый
объект не совместим с указанным интерфейсом, возвращается значение f a l s e , а если
совместим, то можно спокойно вызывать члены этого интерфейса без применения л о ­
гики try / c a tc h .
Для примера предположим, что имеется массив типов Shape, некоторые из членов
которого реализуют интерфейс IP o in ty . Ниже показано, как можно определить, какие
из элементов в этом массиве поддерживают данный интерфейс с помощью ключевого
слова is внутри обновленной соответствующим образом версии метода Main ( ) :
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Interfaces *****\n");
// Создание массива типов Shapes.
Shape[] myShapes = { new Hexagon (), new Circle (), new Triangle("Joe"),
new Circle("JoJo")} ;
for(int i = 0; i < myShapes.Length; i++)
{
// Вспомините, что в базовом классе Shape определен абстрактный
// метод Draw( ) , поэтому все фигуры знают, как себя рисовать.
myShapes[i].Draw();
/ / У каких фигур есть вершины?
if (myShapes[i] is IPointy)
// Вывод числа вершин.
Console.WriteLine ("-> Points: {0}", ((IPointy) myShapes[l]).Points) ;
else
330
Часть III. Дополнительные конструкции программирования на C#
// Это не интерфейс IPointy.
Console.WriteLine ("-> {0}\'s not pointy!", myShapes[i].PetNamd);
Console.WriteLine();
}
Console.ReadLine();
Ниже приведен вывод, полученный в результате выполнения этого кода:
***** Fun with Interfaces *****
Drawing NoName the Hexagon
-> Points: 6
Drawing NoName the Circle
-> NoName's not pointy!
Drawing Joe the Triangle
-> Points: 3
Drawing JoJo the Circle
-> JoJo's not pointy!
Использование интерфейсов
в качестве параметров
Благодаря тому, что интерфейсы являются допустимыми типами .NET, можно
создавать методы, принимающие интерфейсы в качестве параметров, вроде метода
CloneMe (), который был показан ранее в главе. Для примера предположим, что опре­
делен еще один интерфейс по имени IDraw3D:
// Моделирует способность визуализировать тип в трехмерном формате.
public interface IDraw3D
{
void Draw3D();
Далее сконфигурируем две из трех наших фигур (а именно — Circle и Hexagon) та­
ким образом, чтобы они поддерживали это новое поведение:
// C irc le поддерживает IDraw3D.
class ThreeDCircle : Circle, IDraw3D
{
public void Draw3D()
{ Console.WriteLine("Drawing Circle in 3D!"); }
// Hexagon поддерживает IPointy и IDraw3D.
class Hexagon : Shape, IPointy, IDraw3D
{
public void Draw3D()
{ Console.WriteLine("Drawing Hexagon in 3D!"); }
На рис. 9.3 показано, как после этого выглядит диаграмма классов в Visual
Studio 2010.
Если теперь определить метод, принимающий интерфейс IDraw3D в качестве пара­
метра, то ему можно будет передавать, по сути, любой объект, реализующий интерфейс
IDraw3D (при попытке передать тип, не поддерживающий необходимый интерфейс,
компилятор сообщит об ошибке). Например, давайте определим в классе Program сле­
дующий метод:
Глава 9. Работа с интерфейсами
331
// Будет рисовать любую фигуру, поддерживающую IDraw3D.
static void DrawIn3D(IDraw3D itf3d)
{
Console .WnteLine ("-> Drawing IDraw3D compatible type");
itf3d.Qraw3D();
}
Shape
Abstract Class
Рис. 9.3. Обновленная иерархия фигур
Теперь можно проверить, поддерживает ли элемент в массиве Shape новый интер­
фейс, и если да, то передать его методу DrawIn3D () на обработку:
static void Main(string [] args)
{
Console.WriteLine (''***** Fun with Interfaces *****\n") ;
Shape[] myShapes = { new Hexagon (), new Circle (),
new Triangle (), new Circle("JoJo") } ;
for(int i = 0; i < myShapes.Length; i++)
{
// Можно ли нарисовать эту фигуру в трехмерном формате?
if(myShapes [i] is IDraw3D)
DrawIn3D((IDraw3D)myShapes [1 ]);
}
}
Ниже показано, как будет выглядеть вывод в результате выполнения этой модифи­
цированной версии приложения. Обратите внимание, что в трехмерном формате ото­
бражается только объект Hexagon, поскольку все остальные члены массива Shape не
реализуют интерфейса IDraw3D.
* * * * *
Fun with Interfaces *****
Drawing NoName the Hexagon
-> Points: 6
-> Drawing IDraw3D compatible type
Drawing Hexagon in 3D!
Drawing NoName the Circle
-> NoName1s not pointy!
Drawing Joe the Triangle
-> Points: 3
Drawing JoJo the Circle
-> JoJo 1s not pointy!
332
Часть III. Дополнительные конструкции программирования на C#
Использование интерфейсов в качестве
возвращаемых значений
Интерфейсы можно также использовать и в качестве возвращаемых значений ме­
тодов. Для примера создадим метод, который принимает в качестве параметра массив
объектов S y s te m .O b je c t и возвращает ссылку на первый элемент, поддерживающий
интерфейс I P o in ty :
// Этот метод возвращает первый объект в массиве,
// который реализует интерфейс IPointy.
{
foreach (Shape s in shapes)
{
if (s is IPointy)
return s as IPointy;
}
return null;
}
Взаимодействовать с этим методом можно следующим образом:
static void Main(string[] args)
{
Console .WnteLine ("***** Fun with Interfaces *****\n");
// Создание массива объектов Shapes.
Shape[] myShapes = { new Hexagon(), new Circle (),
new Triangle("Joe"), new Circle("JoJo")};
// Получение первого элемента, который имеет вершины.
// Ради безопасности не помешает предусмотреть проверку
// firstPointyltern на предмет равенства null.
IPointy firstPointyltem = FindFirstPointyShape(myShapes);
// Вывод числа вершин.
Console .WnteLine ("The item has {0} points", firstPointyltem. Points) ;
Массивы типов интерфейсов
Вспомните, что один и тот же интерфейс может быть реализован во множестве ти­
пов, даже если они находятся не в одной и той же иерархии классов и не имеют ника­
кого общего родительского класса, помимо System.Ob ject. Это позволяет формировать
очень мощные программные конструкции. Например, давайте создадим в текущем про­
екте три новых типа класса, два из которых (Knife (нож) и Fork (вилка)) будут представ­
лять кухонные принадлежности, а третий (PitchFork (вилы)) — инструмент для работы
в саду (рис. 9.4).
Имея определения типов PitchFork, Fork и Knife, можно определить массив объек­
тов, совместимых с IPointy. Поскольку все эти члены поддерживают один и тот же ин­
терфейс, можно выполнять проход по массиву и интерпретировать каждый его элемент
как совместимый с IPointy объект, несмотря на разницу между иерархиями классов.
static void Main(string [] args)
/ / В этом массиве могут содержаться только типы,
// которые реализуют интерфейс IPointy.
IPointy[] myPointyObjects = {new Hexagon (), new Knife (),
new Triangle (), new Fork (), new PitchFork () };
Глава 9. Работа с интерфейсами
333
foreach(IPointy i in myPointyObjects)
// Вывод числа вершин.
Console.WriteLine("Object has {0} points.", l.Points);
Console.ReadLine() ;
Shape
Abstract Class
^
-4
IPointy
IDrawBD
11' IPointy
I Circle
< Class
■♦Shape
£
1
Triangle
Class
■♦ Shape
( if
H exagon
Class
Shape
J IPointy
<y IPointy
Fork
Class
(if
K n ife
Class
'y IPointy
I P ftchFork
Class
Рис. 9.4. Вспомните, что интерфейсы могут “встраиваться" в любой тип внутри любой
части иерархии классов
Исходный код. Проект Customlnterface доступен в подкаталоге Chapter 9.
Реализация интерфейсов с помощью
Visual Studio 2010
Хотя программирование с применением интерфейсов и является очень мощной
технологией, реализация интерфейсов может сопровождаться довольно приличным
объемом ввода. Поскольку интерфейсы представляют собой именованные наборы аб­
страктных членов, для каждого метода интерфейса в каждом типе, который должен
поддерживать такое поведение, требуется вводить и определение, и реализацию.
К счастью, в Visual Studio 2010 поддерживаются различные инструменты, которые
существенно упрощают процесс реализации интерфейсов. Для примера давайте вста­
вим в текущий проект еще один класс по имени PointyTestClass. При реализации
для него интерфейса IPointy (или любого другого подходящего интерфейса) можно
будет заметить, как по окончании ввода имени интерфейса (или при размещении на
нем курсора мыши в окне кода) первая буква будет выделена подчеркиванием (или,
согласно формальной терминологии, снабжена так называемой контекстной меткой —
смарт-тегом (smart tag)). В результате щелчка на этом смарт-теге появится раскрываю­
щийся список с различными возможными вариантами реализации этого интерфейса
(рис. 9.5).
334
Часть III. Дополнительные конструкции программирования на C#
Рис. 9.5. Реализация интерфейсов в Visual Studio 2010
Обратите внимание, что в этом списке предлагаются два варианта, причем второй
из них (реализация интерфейса явным образом) подробно рассматривается в следую­
щем разделе. Пока что выберем первый вариант. В этом случае Visual Studio 2010 сге­
нерирует (внутри именованного раздела кода) показанный ниже удобный для дальней­
шего обновления код-заглушку (обратите внимание, что в реализации по умолчанию
предусмотрена генерация исключения S ystem .N otlm plem en tedE xception, что вполне
можно удалить).
namespace Customlnterfасе
{
class PointyTestClass : IPointy
{
#region IPointy Members
public byte Points
{
get { throw new NotlmplementedException() ; }
}
#endregion
}
}
На заметку! В Visual Studio 2010 также поддерживается опция рефакторинга типа выделения ин­
терфейса (Extract Interface), которая доступа в меню Refactoring (Рефакторинг). Она позволя­
ет извлекать определение нового интерфейса из существующего определения класса.
Устранение конфликтов на уровне имен за счет
реализации интерфейсов явным образом
Как было показано ранее в главе, единственный класс или структура может реали­
зовать любое количество интерфейсов. Из-за этого всегда существует вероятность реа­
лизации интерфейсов с членами, имеющими идентичные имена, и, следовательно, воз­
никает необходимость в устранении конфликтов на уровне имен. Чтобы ознакомиться с
различными способами решения этой проблемы, давайте создадим новый проект типа
C o n so le A p p lic a tio n по имени In terfa c eN a m e C la sh и добавим в него три специальных
интерфейса, представляющих различные места, в которых реализующий их тип может
визуализировать свой вывод:
Глава 9. Работа с интерфейсами
335
// Прорисовывание изображения в форме.
public interface IDrawToForm
{
void Draw();
}
// Отправка изображения в буфер в памяти.
public interface IDrawToMemory
{
void Draw();
// Вывод изображения на принтере.
public interface IDrawToPrinter
{
void Draw();
}
Обратите внимание, что в каждом из этих интерфейсов присутствует метод по име­
ни Draw () с идентичной сигнатурой (без аргументов). Если теперь необходимо, чтобы
каждый из этих интерфейсов поддерживался в одном классе по имени Octagon, компи­
лятор позволит использовать следующее определение:
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter
{
public void Draw()
{
// Совместно используемая логика рисования.
Console .WnteLine ("Drawing the Octagon...");
}
}
Хотя компиляция этого кода пройдет гладко, одна возможная проблема в нем всетаки присутствует. Попросту говоря, предоставление единой реализации метода Draw ()
не позволяет предпринимать уникальные действия на основе того, какой интерфейс по­
лучен от объекта Octagon. Например, следующий код будет приводить к вызову одного
и того же метода Draw (), какой бы интерфейс не был получен:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Interface Name Clashes *****\n");
//Во всех этих случаях будет вызываться один и тот же метод D ra w ()!
Octagon oct = new Octagon () ;
oct.Draw();
IDrawToForm itfForm = (IDrawToForm)oct;
itfForm.Draw();
IDrawToPrinter itfPriner = (IDrawToPrinter)oct;
itfPriner.Draw();
IDrawToMemory itfMemory = (IDrawToMemory)oct;
ltfMemory.Draw();
Console.ReadLine();
}
Очевидно, что код, требуемый для визуализации изображения в окне, довольно
сильно отличается от того, который необходим для визуализации изображения на сете­
вом принтере или в области памяти. При реализации нескольких интерфейсов, имею­
щих идентичные члены, можно разрешать подобный конфликт на уровне имен за счет
применения так называемого синтаксиса явной реализации интерфейсов. Например,
модифицируем тип Octagon следующим образом:
336
Часть III. Дополнительные конструкции программирования на C#
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPnnter
{
// Явная привязка реализаций Draw() к конкретному интерфейсу.
void IDrawToForm.Draw()
{
Console.WriteLine("Drawing to form...");
}
void IDrawToMemory.Draw ()
{
Console.WriteLine("Drawing to memory...");
}
void IDrawToPrinter.Draw()
{
Console.WriteLine("Drawing to a printer...");
}
Как здесь показано, при явной реализации члена интерфейса схема, которой нужно
следовать, в общем случае выглядит следующим образом:
воэвращаемыйТип ИмяИнтерфейса .ИмяМетода ( параметры)
Обратите внимание, что в этом синтаксисе указывать модификатор доступа не тре­
буется, поскольку члены, реализуемые явным образом, автоматически считаются при­
ватными. Например, следующий код является недопустимым:
// Ошибка! Модификатора доступа быть не должно!
public void IDrawToForm.Draw()
{
Console.WriteLine("Drawing to form...");
}
Из-за того, что реализуемые явным образом члены всегда неявно считаются приват­
ными, они перестают быть доступными на уровне объектов. На самом деле, если при­
менить к типу Octagon операцию точки, никаких членов Draw () в списке IntelliSense
отображаться не будет (рис. 9.6).
Рис. 9.6. Реализуемые явным образом члены интерфейсов перестают
быть доступными на уровне объектов
Как и следовало ожидать, для получения доступа к необходимым функциональным
возможностям в таком случае потребуется явное приведение, например:
Глава 9. Работа с интерфейсами
337
static void Main(string [] args)
{
Console.WnteLine (''***** Fun with Interface Name Clashes *****\n");
Octagon oct = new Octagon ();
// Теперь для получения доступа к членам Draw()
// должно использоваться приведение.
IDrawToForm itfForm = (IDrawToForm)oct;
itfForm.Draw();
// Сокращенная версия на случай, если переменную интерфейса
//н е планируется использовать позже.
((IDrawToPrinter)oct) .Draw();
// Можно было бы также использовать ключевое слово аз.
if (oct is IDrawToMemory)
((IDrawToMemory)oct).Draw();
Console.ReadLine ();
}
Хотя такой синтаксис довольно полезен, когда необходимо устранить конфликты на
уровне имен, приемом явной реализации интерфейсов можно пользоваться и просто
для сокрытия более “сложных” членов на уровне объектов. В таком случае при примене­
нии операции точки пользователь объекта будет видеть только некоторую часть общей
функциональности типа. Те же пользователи, которым необходим доступ к более слож­
ным поведениям, все равно смогут получать их из желаемого интерфейса через явное
приведение.
Исходный код. Проект InterfaceNameClash доступен в подкаталоге Chapter 9.
Проектирование иерархий интерфейсов
Интерфейсы могут быть организованы в иерархии. Как и в иерархии классов, в ие­
рархии интерфейсов, когда какой-то интерфейс расширяет существующий, он насле­
дует все абстрактные члены своего родителя (или родителей). Конечно, в отличие от
классов, производные интерфейсы никогда не наследуют саму реализацию. Вместо это­
го они просто расширяют собственное определение за счет добавления дополнительных
абстрактных членов.
Использовать иерархию интерфейсов может быть удобно, когда нужно расширить
функциональность определенного интерфейса без нарушения уже существующих ко­
довых баз. Для примера создадим новый проект типа C o n s o le A p p lic a tio n по имени
InterfaceHierarchy и добавим в него новый набор отвечающих за визуализацию ин­
терфейсов так, чтобы IDrawable был корневым интерфейсом в дереве этого семейства:
public interface IDrawable
{
void Draw();
}
Поскольку в интерфейсе IDrawable определяется лишь базовое поведение рисова­
ния, мы теперь можем создать производный от него интерфейс, который расширяет
его функциональность, добавляя возможность выполнения визуализации в других фор­
матах, например:
public interface IAdvancedDraw : IDrawable
{
void DrawInBoundingBox(int top, int left, int bottom, int right);
void DrawUpsideDown ();
}
338
Часть III. Дополнительные конструкции программирования на C#
При таком дизайне для реализации интерфейса IAdvancedDraw в классе потребует­
ся реализовать каждый из определенных в цепочке наследования членов (те. Draw (),
DrawInBoundingBox () и DrawUpsideDown()):
public class Bitmaplmage : IAdvancedDraw
{
public void Draw()
{
Console.WriteLine("Drawing...");
}
public void DrawInBoundingBox (int top, int left, int bottom, int right)
{
Console.WriteLine("Drawing in a box...");
}
public void DrawUpsideDown ()
{
Console.WriteLine("Drawing upside down1");
}
}
Теперь при использовании Bitmaplmage можно вызывать каждый метод на уровне
объекта (поскольку все они являются общедоступными), а также извлекать ссылку на
каждый поддерживаемый интерфейс явным образом с помощью приведения:
static void Main(string [] args)
{
Console.WriteLine ("*****Simple Interface Hierarchy *****");
// Выполнение вызова на уровне объекта.
Bitmaplmage myBitmap = new Bitmaplmage();
myBitmap.Draw();
myBitmap.DrawInBoundingBox(10, 10, 100, 150);
myBitmap.DrawUpsideDown () ;
// Получение IAdvancedDraw юным образом.
IAdvancedDraw iAdvDraw;
iAdvDraw = (IAdvancedDraw)myBitmap;
iAdvDraw.DrawUpsideDown();
Console.ReadLine();
}
Исходный код. Проект InterfaceHierarchy доступен в подкаталоге Chapter 9.
Множественное наследование в случае типов интерфейсов
В отличие от классов, один интерфейс может расширять сразу несколько базовых ин­
терфейсов, что позволяет проектировать очень мощные и гибкие абстракции. Для при­
мера создадим новый проект типа C o n so le A p p lic a tio n по имени MI InterfaceHierarchy
и добавим в него еще одну коллекцию интерфейсов, моделирующих различные свя­
занные с визуализацией и фигурами абстракции. Обратите внимание, что интерфейс
IShape в этой коллекции расширяет оба интерфейса IDrawable и IPrintable.
// Множественное наследование в случае типов
// интерфейсов является вполне допустимым.
interface IDrawable
{
void Draw();
Глава 9. Работа с интерфейсами
339
interface IPrintable
{
void Print();
void DrawO; // <-- Здесь возможен конфликт имен!
}
// Множественное наследование интерфейса. Все в порядке!
public interface IShape : IDrawable, IPrintable
{
int GetNumberOfSides ();
}
На рис. 9.7 показано, как теперь выглядит иерархия интерфейсов.
Рис. 9.7. В отличие от классов, интерфейсы могут расширять •
сразу несколько базовых интерфейсов
Теперь главный вопрос состоит в том, сколько методов потребуется реализовать при
создании класса, поддерживающего IShape? Ответ будет несколько. Если нужно пре­
доставить простую реализацию метода Draw ( ) , понадобится только реализовать три его
члена, как показано ниже на примере типа R ec ta n g le:
class Rectangle : IShape
{
public int GetNumberOfSides ()
{ return 4; }
public void Draw()
{ Console.WriteLine("Drawing...") ; }
public void Print()
{ Console.WriteLine("Prining.; }
}
При желании предоставить более конкретные реализации для каждого метода
Draw () (что в данном случае имеет больше всего смысла) придется разрешать конфликт
на уровне имен счет применения синтаксиса реализации интерфейсов явным образом,
как показано ниже на примере типа Square:
class Square : IShape
{
// Применение синтаксиса юной реализации для устранения
// конфликта между именами членов.
void IPrintable.Draw()
{
// Рисование на принтере ...
}
void IDrawable.Draw()
{
// Рисование на экране ...
340
Часть III. Дополнительные конструкции программирования на C#
public void Print ()
{
// Печать ...
}
public int GetNumberOfSides ()
{ return 4; }
К этому моменту процесс определения и реализации специальных интерфейсов на
C# должен стать более понятным. По правде говоря, на привыкание к программирова­
нию с применением интерфейсов может уйти некоторое время.
Однако уже сейчас важно уяснить, что интерфейсы являются фундаментальным
компонентом .NET Framework. Какого бы типа приложение не разрабатывалось (веб­
приложение, приложение с настольным графическим интерфейсом, библиотека досту­
па к данными и т.п.), работа с интерфейсами будет обязательной частью этого процесса.
Подводя итог всему изложенному, отметим, что интерфейсы могут приносить чрезвы­
чайную пользу в следующих случаях.
• При наличии единой иерархии, в которой только какой-то набор производных ти­
пов поддерживает общее поведение.
• При необходимости моделировать общее поведение, которое должно встречать­
ся в нескольких иерархиях, не имеющих общего родительского класса помимо
S ystem .O b ject.
Теперь, когда мы разобрались со специфическими деталями построения и реализа­
ции специальных интерфейсов, необходимо посмотреть, какие стандартные интерфей­
сы предлагаются в библиотеках базовых классов .NET.
Исходный код. Проект MI I n t e r fa c e H ie r a r c h y доступен в подкаталоге Chapter 9.
Создание перечислимых типов
(IEnumerable и IEnumerator)
Прежде чем приступать к исследованию процесса реализации существующих ин­
терфейсов .NET, давайте сначала рассмотрим роль типов IEnumerable и IEnumerator.
Вспомните, что в C# поддерживается ключевое слово f o r each, которое позволяет осу­
ществлять проход по содержимому массива любого типа:
// Итерация по массиву элементов.
int [] myArrayOf Ints = {10, 20, 30, 40};
foreach(int i in myArrayOfInts)
{
Console.WnteLine (i) ;
}
Хотя может показаться, что данная конструкция подходит только для массивов, на
самом деле с ее помощью можно анализировать любой тип, который поддерживает ме­
тод GetEnumerator (). Для примера создадим новый проект типа C o n so le A p p lic a tio n по
имени CustomEnumerator и добавим в него файлы C a r.c s и R a d io .c s , которые были
определены в примере S im pleE xception в главе 7 (выбрав в меню P ro je ct (Проект) пункт
A d d E xistin g Item (Добавить существующий элемент)).
На заметку! Может возникнуть желание переименовать содержащее типы Саг и Radio простран­
ство имен в CustomEnumerator, чтобы не импортировать в новый проект пространство имен
Custom Exception.
Глава 9. Работа с интерфейсами
341
Вставим новый класс Garage (гараж), обеспечивающий сохранение ряда объектов
Саг (автомобиль) внутри S ystem .A rray:
// Garage содержит набор объектов Саг.
public class Garage
{
private Car [] carArray = new Car [4];
// Заполнение какими-то объектами Car при запуске.
public Garage()
{
carArray[0]
carArray[l]
carArray[2]
carArray[3]
=
=
=
=
new
new
new
new
Car("Rusty", 30);
Car ("Clunker", 55);
Car("Zippy", 30);
Car("Fred", 30);
В идеале удобно было бы осуществлять проход по элементам объекта Garage как по
массиву значений данных с применением конструкции fo rea ch :
// Такой вариант кажется целесообразным. . .
public class Program
{
static void Main(string [] args)
{
Console .WnteLine ("***** Fun with IEnumerable / IEnumerator *****\n");
Garage carLot = new Garage ();
// Проход по всем объектам Car в коллекции?
foreach (Car c in carLot)
{
Console .WnteLine ("{ 0 } is going {1} MPH",
c.PetName, c. Speed);
}
Console.ReadLine ();
}
К сожалению, в этом случае компилятор сообщит, что в классе Garage не реализован
метод GetEnumerator (). Этот метод формально определен в интерфейсе IEnumerable,
который находится глубоко внутри пространства имен S y s te m .C o lle c tio n s .
На заметку! В следующей главе будет рассказываться о роли обобщений и пространства имен
System. C o l l e c t i o n s . G en eric. В этом пространстве имен содержатся обобщенные вер­
сии Enumerable и IEnum erator, которые предоставляют более безопасный в отношении
типов способ для реализации прохода по подобъектам.
Классы или структуры, которые поддерживают такое поведение, позиционируются
как способные предоставлять содержащиеся внутри них подэлементы вызывающему
коду (в данном примере это само ключевое слово fo rea ch ):
// Этот интерфейс информирует вызывающий код о том,
// что подэлементы объекта могут перечисляться.
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
Метод GetEnumerator () возвращает ссылку на еще один интерфейс под названи­
ем System. C o lle c t io n s . IEnumerator. Этот интерфейс обеспечивает инфраструктуру,
342
Часть III. Дополнительные конструкции программирования на C#
позволяющую вызывающему коду проходить по внутренним объектам, которые содер­
жатся в совместимом с I Enumerable контейнере:
// Этот интерфейс позволяет вызывающему коду получать
// содержащиеся в контейнере внутренние подэлементы.
public interface IEnumerator
{
bool MoveNextO;
object Current { get;}
void FesetO;
//
//
//
//
Перемещение внутренней позиции курсора,
Извлечение текущего элемента
(свойство, доступное только для чтения).
Сброс курсора перед первым членом.
При модификации типа Garage для поддержки этих интерфейсов можно пойти длин­
ным путем и реализовать каждый метод вручную. Хотя, конечно же, предоставлять спе­
циализированные версии методов GetEnumerator (), MoveNext (), Current и Reset ()
вполне допускается, существует более простой путь. Поскольку в типе System.Array
(как и во многих других классах коллекций) интерфейсы IEnumerable и IEnumerator
уже реализованы, можно просто делегировать запрос к System.Array показанным
ниже образом:
using System.Collections;
public class Garage : IEnumerable
{
// В System.Array интерфейс IEnumerator уже реализован!
private Car[] carArray = new Car[4];
public Garage()
carArray = new Car[4];
carArray[0] = new C a r ("FeeFee", 200, 0) ;
carArray[1] = new Car ("Clunker", 90, 0) ;
carArray[2]
= new Car ("Zippy", 30, 0) ;
carArray[3]
= new Ca r ("Fred", 30, 0) ;
public IEnumerator GetEnumerator()
{
// Возврат интерфейса IEnumerator объекта массива.
return carArray.GetEnumerator();
}
Изменив тип Garage подобным образом, теперь можно спокойно использовать его
внутри конструкции foreach. Более того, поскольку метод GetEnumerator () был оп­
ределен как общедоступный, пользователь объекта тоже может взаимодействовать с
IEnumerator:
// Работа с IEnumerator вручную
IEnumerator i = carLot.GetEnumerator() ;
i .MoveNext() ;
Car myCar = (Car)l.Current;
Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar. CurrentSpeed) ;
Если нужно скрыть функциональные возможности IEnumerable на уровне объекта,
достаточно применить синтаксис явной реализации интерфейса:
public IEnumerator IEnumerable.GetEnumerator ()
{
// Возврат интерфейса IEnumerator объекта массива.
return carArray.GetEnumerator();
Глава 9. Работа с интерфейсами
343
После этого обычный пользователь объекта не будет видеть метода Get Enumerator ()
в Garage, хотя конструкция foreach будет все равно получать интерфейс незаметным
образом, когда это необходимо.
Исходный код. Проект CustomEnumerator доступен в подкаталоге Chapter 9.
Создание методов итератора с помощью ключевого слова y i e l d
Раньше, когда требовалось создать специальную коллекцию (вроде Garage), способ­
ную поддерживать перечисление элементов посредством конструкции foreach, реали­
зация интерфейса IEnumerable (и возможно IEnumerator) была единственным доступ­
ным вариантом. Потом, однако, появился альтернативный способ для создания типов,
способных работать с циклами foreach, который предусматривает использование так
называемых итераторов (iterator).
Попросту говоря, итератором называется такой член, который указывает, каким об­
разом должны возвращаться внутренние элементы контейнера при обработке в цикле
foreach. Хотя метод итератора должен все равно носить имя GetEnumerator (), а его
возвращаемое значение относиться к типу IEnumerator, необходимость в реализации
каких-либо ожидаемых интерфейсов в специальном классе при таком подходе отпадает.
Для примера давайте создадим новый проект типа C o n s o le A p p lic a tio n по имени
CustomEnumeratorWithYield и вставим в него типы Car, Radio и Garage из предыду­
щего примера (при желании переименовав пространство имен в соответствии с теку­
щим проектом), после чего модифицируем тип Garage показанным ниже образом:
public class Garage
{
private C ar[] carArray = new Car[4];
// Метод итератора.
public IEnumerator GetEnumerator ()
{
foreach (Car c in carArray)
{
yield return c;
}
Обратите внимание, что в данной реализации GetEnumerator () проход по подэле­
ментам осуществляется с использованием внутренней логики foreach, а каждый объ­
ект Саг возвращается вызывающему коду с применением синтаксиса yield return.
Ключевое слово yield служит для указания значения или значений, которые должны
возвращаться конструкции foreach в вызывающем коде. При достижении оператора
yield return производится сохранение текущего местоположении в контейнере, и при
следующем вызове итератора выполнение начинается уже с этого места (детали будут
описаны позже).
Использовать ключевое слово foreach в методах итераторов для возврата содержи­
мого не требуется. Метод итератора можно также определять следующим образом:
public IEnumerator GetEnumerator ()
{
yield
yield
yield
yield
}
return
return
return
return
carArray[0];
carArray[1];
carArray[2];
carArray[3];
344
Часть III. Дополнительные конструкции программирования на C#
В этой реализации важно обратить внимание, что метод GetEnum erator () явным
образом возвращает вызывающему коду новое значение после каждого прогона. В дан­
ном примере подобный подход не особо удобен, поскольку в случае добавления больше­
го числа объектов в переменную экземпляра с а гА гга у метод GetEnumerator () вышел
бы из-под контроля. Тем не менее, такой синтаксис все-таки может быть довольно по­
лезным, когда необходимо возвращать из метода локальные данные для последующей
обработки внутри fo re a c h .
Создание именованного итератора
Еще интересен тот факт, что ключевое слово y i e l d формально может применяться
внутри любого метода, как бы ни выглядело его имя. Такие методы (называемые име­
нованными итераторами) уникальны тем, что могут принимать любое количество ар­
гументов. При создании именованного итератора необходимо очень хорошо понимать,
что метод будет возвращать интерфейс I Enumerable, а не ожидаемый совместимый с
IEnum erator тип. Для примера добавим к типу Garage следующий метод:
public IEnumerable GetTheCars(bool ReturnRevesed)
{
// Возврат элементов в обратном порядке.
if (ReturnRevesed)
{
for (int i = carArray.Length; 1 != 0; 1 — )
{
yield return carArray[l-l];
}
}
else
{
// Возврат элементов в том порядке,
// в котором они идут в массиве.
foreach (Car с in carArray)
{
yield return с;
}
Обратите внимание, что добавленный новый метод позволяет вызывающему коду
получать подэлементы как в прямом, так и в обратном порядке, если во входном пара­
метре передается значение tru e . Теперь с ним можно взаимодействовать следующим
образом:
static void Main(string [] args)
{
Console .WnteLine ("***** Fun with the Yield Keyword *****\n") ;
Garage carLot = new Garage();
// Получение элементов с помощью GetEnumerator() .
foreach (Car c in carLot)
{
Console.WriteLine("{0} is going {1} MPH",
c.PetName, c .CurrentSpeed);
Console.WriteLine ();
// Получение элементов (в обратном порядке)
/ / с помощью именованного итератора.
Глава 9. Работа с интерфейсами
345
foreach (Car с in carLot.GetTheCars(true))
{
Console .WnteLine ("{ 0 } is going {1} MPH", c.PetName, c .CurrentSpeed) ;
}
Console.ReadLine();
}
Нельзя не согласиться с тем, что именованные итераторы представляют собой очень
полезные конструкции, поскольку позволяют определять в единственном специальном
контейнере сразу несколько способов для запрашивания возвращаемого набора.
Внутреннее представление метода итератора
Столкнувшись с методом итератора, компилятор C# динамически генерирует внут­
ри соответствующего типа (в данном случае Garage) определение вложенного класса.
В этом сгенерированном вложенном классе, в свою очередь, автоматически реализуют­
ся такие члены, как GetEnumerator () ,MoveNext () и Current (но, как ни странно, не
метод Reset ( ) , и при попытке его вызвать возникает исключение времени выполнения).
Если загрузить текущее приложение в утилиту ildasm.exe, можно обнаружить в нем
два вложенных типа, в каждом из которых будет содержаться логика, необходимая для
конкретного метода итератора. На рис. 9.8 видно, что эти сгенерированные автомати­
чески компилятором типы имеют имена <GetEnumerator>d__0 и <GetTheCars>d__6.
/ /
Н Л М у Books'уС * B o c k \C * and th e NET P latfo rm 5th e d First D ra ft\C h a ~
Eile
у lew
H elp
H:\My Books\C# Book\C# and the NET Platform 5th ed\Frst Draft\Chapter _09\Code\CustomEnur
► MANIFEST
W CustomEnumeratorWithYield
*
0 £
CustomEnumeratcr WthYierfd.C a
CustomEnumeratorWithYield. Garage
► .class public auto ansi beforefleldinit
► implements [mscorlb]System Colections IEnumerable
* M .ДЖ М Е
it £
<GetTheCars>d__6
. ■ car Array : private class CustomEnumeratorWithYield. Car[]
■
ctor : void()
■ GetEnumerator class [msccrlib]5ystem. Colections. IEnumeratcr()
■ GetTheCars class [mscorib]5ystem. Colections IEnumerabte(bool)
* B : CustomEnumeratorWithYield.Program
* B : CustomEnumeratorWithYield.Radio
assembly CustomEnumerato'WithYield
i
Рис. 9.8. Методы итераторов внутренне реализуются с помощью
автоматически сгенерированного вложенного класса
Воспользовавш ись ути ли той ildasm.exe для просмотра реализации метода
GetEnumerator () в типе Garage, можно обнаружить, что он был реализован так, чтобы
в нем “за кулисами” использовался тип <GetEnumerator>d__0 (в методе GetTheCars ()
аналогичным образом используется вложенный тип <GetTheCars>d__6):
.method public hidebysig instance class
[mscorlib]System.Collections.IEnumerator
GetEnumerator() cil managed
{
newobj instance void
CustomEnumeratorWithYield.Garage/'<GetEnumerator>d__0'::.ctor(int 32)
} // end of method Garage::GetEnumerator
346
Часть III. Дополнительные конструкции программирования на C#
В завершение темы построения перечислимых объектов запомните, что для того,
чтобы специальные типы могли работать с ключевым словом foreach, в контейнере
должен обязательно присутствовать метод по имени GetEnumeratorO , который уже
был формально определен типом интерфейса IEnumerable. Реализация данного метода
обычно осуществляется за счет делегирования внутреннему члену, который отвечает за
хранение подобъектов; однако можно также использовать синтаксис yield return и
предоставлять с его помощью множество методов “именованных итераторов”.
Исходный код. Проект CustomEnumeratorWithYield доступен в подкаталоге Chapter 9.
Создание клонируемых объектов ( ic io n e a b le )
Как уже рассказывалось в главе 6, в System.Ob j ect имеется член по имени
MemberwiseClone ( ) . Он представляет собой метод и позволяет получить поверхност­
ную копию (shallow сору) текущего объекта. Пользователи объекта не могут вызывать
этот метод напрямую, поскольку он является защищенным, но сам объект вполне мо­
жет это делать во время так называемого процесса клонирования. Для примера давайте
создадим новый проект типа Console Application по имени cloneablePoint и добавим
в него класс Point, представляющий точку.
// Класс Point.
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point (int xPos, int yPos) { X = xPos; Y = yPos;}
public Point () {}
// Переопределение O bject.ToStringO .
public override string ToStringO
{ return string.Format("X = {0}; Y = {1}", X, Y ); }
}
Как уже должно быть известно из материала о ссылочных типах и типах значения
(см. главу 4), в случае присваивания одной переменной ссылочного типа другой полу­
чается две ссылки, указывающие на один и тот же объект в памяти. Следовательно,
показанная ниже операция присваивания будет приводить к получению двух ссылок,
указывающих на один и тот же объект Point в куче, при этом внесение изменений с
использованием любой из этих ссылок будет оказывать воздействие на тот же самый
объект в куче:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Object Cloning *****\n ");
// Две ссылки на один и тот же объект!
Point pi = new Point(50, 50);
Point р2 = pi;
р2 .X = 0;
Console.WriteLine(pi);
Console.WriteLine(p2);
Console.ReadLine();
}
Глава 9. Работа с интерфейсами
347
Чтобы обеспечить специальный тип способностью возвращать идентичную ко­
пию самого себя вызывающему коду, можно реализовать стандартный интерфейс
IC lo n e a b le . Как уже показывалось в начале настоящей главы, этот интерфейс имеет
единственный метод по имени Clone () :
public interface ICloneable
{
object Clone ();
На заметку! По поводу полезности интерфейса IC lo n e a b le в сообществе .NET ведутся горячие
споры. Проблема связана с тем, что в формальной спецификации никак явно не говорится о
том, что объекты, реализующие данный интерфейс, должны обязательно возвращать деталь­
ную копию (deep сору) объекта (т.е. внутренние ссылочные типы объекта должны приводить к
созданию совершенно новых объектов с идентичным состоянием). Из-за этого с технической
точки зрения возможно, что объекты, реализующие IC lo n e a b le , на самом деле будут воз­
вращать поверхностную копию интерфейса (т.е. внутренние ссылки будут указывать на один
и тот же объект в куче), что вызывает приличную путаницу. В рассматриваемом примере пред­
полагается, что метод Clone () должен быть реализован так, чтобы возвращать полную, де­
тальную копию объекта.
Разумеется, реализация метода Clone () в различных объектах может выглядеть поразному. Однако базовая функциональность обычно остается неизменной и заключает­
ся в копировании переменных экземпляра в новый экземпляр объекта того же типа и в
возврате его пользователю. Для примера изменим класс P o in t следующим образом:
// Класс Point теперь поддерживает возможность клонирования.
public class Point : ICloneable
{
public int X { get; set; }
public int Y { get; set; }
public Point (int xPos, int yPos) { X = xPos; Y = yPos; }
public Point () { }
// Переопределение Obj ect.ToStnng () .
public override string ToStnng ()
{ return string.Format("X = {0}; Y = {1}", X, Y ) ; }
// Возврат копии текущего объекта,
public object Clone ()
{ return new Point(this.X, this.Y); }
}
Теперь можно создавать точные автономные копии типа P o in t так, как показано
ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Object Cloning *****\n");
// Обратите внимание, что C lone() возвращает
// простой тип объекта. Для получения производного
// типа требуется явное приведение.
Point рЗ = new Point (100, 100) ;
Point р4 = (Point)р З .Clone() ;
// Изменение р4.Х (которое не приводит к изменению рЗ.х) .
р 4 .X = 0;
348
Часть III. Дополнительные конструкции программирования на C#
// Вывод объектов на консоль.
Console.WriteLine (рЗ);
Console.WriteLine (р 4);
Console.ReadLine ();
}
Хотя текущая реализация Point отвечает всем требованиям, ее все равно можно
немного улучшить. В частности, поскольку Point не содержит никаких внутренних пе­
ременных ссылочного типа, реализацию метода Clone () можно упростить следующим
образом:
p u b lic object C lone()
{
// Копируем каждое поле Point почленно.
return this.MemberwiseClone ();
}
Следует, однако, иметь в виду, что если бы в Point все-таки содержались внутрен­
ние переменные ссылочного типа, метод MemberwiseClone () копировал бы ссылки на
эти объекты (т.е. создавал бы поверхностную копию). Тогда для поддержки построения
детальной копии потребовалось бы создавать во время процесса клонирования новый
экземпляр каждой из переменных ссылочного типа. Давайте рассмотрим соответствую­
щий пример.
Более сложный пример клонирования
Теперь предположим, что в классе Point содержится переменная экземпляра ссы­
лочного типа по имени PointDescription, предоставляющая удобное для восприятия
имя вершины, а также ее идентификационный номер в виде System.Guid (глобаль­
но уникальный идентификатор GUID представляет собой статистически уникальное
128-битное число). Соответствующая реализация показана ниже.
// Этот класс описывает точку.
public class PointDescription
{
public string PetName {get; set;}
public Guid PointID {get; set;}
public PointDescription ()
{
PetName = "No-name";
PointID = Guid.NewGuid ();
'
Как здесь видно, для начала был модифицирован сам класс Point, чтобы его метод
ToString () принимал во внимание подобные новые фрагменты данных о состоянии.
Кроме того, был определен и создан ссылочный тип PointDescription. Чтобы позво­
лить “внешнему миру” указывать желаемое дружественное имя (PetName) для Point, не­
обходимо также изменить аргументы, передаваемые перегруженному конструктору.
public class Point : ICloneable
{
public int X { get; set; }
public int Y { get; set; }
public PointDescription desc = new PointDescription();
public Point (int xPos, int yPos, string petName)
{
X = xPos; Y = yPos;
desc.PetName = petName;
}
Глава 9. Работа с интерфейсами
349
public Point(int xPos, int yPos)
{
X = xPos; Y = yPos;
}
public Point () { }
// Переопределение Object.ToStnng () .
public override string ToStnng ()
{
return string.Format ("X = {0}; Y = {1}; Name = {2};\nID = {3}\n",
X, Y, desc.PetName, desc.PointID);
}
// Возврат копии текущего объекта,
public object Clone ()
{ return this.MemberwiseClone (); }
}
Обратите внимание, что метод Clone () пока еще не обновлялся. Следовательно, в
текущей реализации при запросе клонирования пользователем объекта будет созда­
ваться поверхностная (почленная) копия. Чтобы удостовериться в этом, изменим метод
Main () следующим образом:
static void Main(string[] args)
{
Console.WriteLine (''***** Fun with Object Cloning *****\n");
Console.WriteLine("Cloned p3 and stored new Point in p4") ;
Point p3 = new Point(100, 100, "Jane") ;
Point p4 = (Point)p3.Clone () ;
//До изменения.
Console.WriteLine("Before modification:");
Console.WriteLine("p3: {0}", p3) ;
Console.WriteLine ("p4: {0}", p4);
p 4 .desc.PetName = "My new Point;
p 4 .X = 9;
// После изменения.
Console.WriteLine("\nChanged p 4 .desc.petName and p4.X");
Console.WriteLine("After modification:");
Console.WriteLine("p3: {0}", p3) ;
Console.WriteLine ("p4: {0}", p4);
Console.ReadLine();
}
sa
Ниже показано, как будет выглядеть вывод в таком случае. Обратите внимание, что
хотя типы значения на самом деле изменились, у внутренних ссылочных типов остались
же самые значения, поскольку они “указывают” на одинаковые объекты в памяти,
частности, дружественное имя у обоих объектов сейчас выглядит как Му new Point.).
***** Fun with Object Cloning *****
Cloned p3 and stored new Point in p4
Before modification:
рЗ: X = 100; Y = 100; Name = Jane;
ID = 133d66a7-0 837-4bd7-95c6-b22ab0 434 50 9
p4 : X = 100; Y = 100; Name = Jane;
ID = 133d66a7-0 837-4bd7-95c6-b22ab0 434 50 9
Changed p 4 .desc.petName and p4.X
After modification:
рЗ: X = 100; Y = 100; Name = My new Point;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509°
p4: X = 9; Y = 100; Name = My new Point;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509
350
Часть III. Дополнительные конструкции программирования на C#
Для того чтобы метод Clone () создавал полную детальную копию внутренних ссы­
лочных типов, необходимо настроить возвращаемый методом Memberwise Сlon e () объ­
ект, чтобы он принимал во внимание имя текущего объекта P o in t (тип System. Guid на
самом деле представляет собой структуру, поэтому в действительности числовые дан­
ные будут копироваться). Ниже показан один из возможных вариантов реализации.
// Теперь необходимо подстроить код таким образом, чтобы
// в нем принимался во внимание член P ointD escription.
public object Clone()
{
// Сначала получаем поверхностную копию.
Point newPoint = (Point)this.MemberwiseClone ();
// Теперь заполняем пробелы.
PointDescription currentDesc = new PointDescription();
currentDesc.PetName = this.desc.PetName;
newPoint.desc = currentDesc;
return newPoint;
Если теперь запустить приложение и посмотреть на его вывод (который показан
ниже), то будет видно, что возвращаемый методом Clone () объект Point сейчас дейст­
вительно начал копировать свои внутренние переменные экземпляра ссылочного типа
(обратите внимание, что дружественные имена у рЗ и р4 теперь стали уникальными).
***** Fun with Object Cloning *****
Cloned рЗ and stored new Point in p4
Before modification;
рЗ: X = 10 0; Y = 100; Name = Jane;
ID = 5 If 64 f25-4b0e-4 7ac-ba35-37d2634 964 0 6
p4: X = 100; Y = 100; Name = Jane;
ID = 0d377 6b3-bl5 9-4 90d-b022-7 f3f 607 88e8a
Changed p 4 .desc.petName and p4.X
After modification:
р З : X = 100; Y = 100; Name = Jane;
ID = 51f64f25-4b0e-47ac-ba35-37d2 634 9640 6
p4: X = 9; Y = 100; Name = My new Point;
ID = 0d377 6b3-bl5 9-4 90d-b022-7 f3f 607 88e8a
Подведем итог по процессу клонирования. При наличии класса или структуры, в
которой не содержится ничего кроме типов значения, достаточно реализовать метод
Clone () с использованием MemberwiseClone ( ) . Однако если есть специальный тип,
поддерживающий другие ссылочные типы, необходимо создать новый объект, прини­
мающий во внимание каждую из переменных экземпляра ссылочного типа.
Исходный код. Проект CloneablePoint доступен в подкаталоге Chapter 9.
Создание сравнимых объектов (iComparable)
Интерфейс System. IComparable обеспечивает поведение, которое позволяет сорти­
ровать объект на основе какого-то указанного ключа. Формально его определение вы­
глядит так:
// Этот интерфейс позволяет объекту указывать
// его отношения с другими подобными объектами,
public interface IComparable
{
int CompareTo(object o) ;
Глава 9. Работа с интерфейсами
351
На заметку! В обобщенной версии этого интерфейса (1СошрагаЫе<т>) предлагается более
безопасный в отношении типов способ для обработки сравнений объектов. Обобщения будут
более подробно рассматриваться в главе 10.
Д ля примера создадим новый проект типа C o n s o le A p p lic a tio n по имени
ComparableCar и вставим в него следующую обновленную версию класса Саг (обратите
внимание, что здесь просто добавлено новое свойство для представления уникального
идентификатора каждого автомобиля и модифицированный конструктор):
public class Car
public int CarlD {get; set;}
public Car(string name, int currSp, int id)
{
CurrentSpeed = currSp;
PetName = name;
CarID = id;
}
Теперь создадим массив объектов Саг, как показано ниже:
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Object Sorting *****\n");
// Создание массива объектов Car.
Car[] myAutos = new Car [5];
myAutos[0] = new Car("Rusty", 80, 1) ;
myAutos [1] = new Car ("Mary", 40, 234);
myAutos[2] = new Ca r ("Viper", 40, 34);
myAutos [3] = new Car ("Mel", 40, 4);
myAutos[4] = new Ca r ("Chucky", 40, 5);
Console.ReadLine();
}
В классе System. A rray определен статический метод S ort () . При вызове этого ме­
тода на массиве внутренних типов (in t, sh ort, s t r in g и т.д.) элементы массива могут
сортироваться в числовом или алфавитном порядке, поскольку эти внутренние типы
данных реализуют интерфейс I Comparable. Однако что будет происходить в случае пе­
редачи методу S ort () массива типов Саг, как показано ниже?
// Будет ли выполняться сортировка автомобилей?
Array.Sort(myAutos) ;
В случае выполнения этого тестового кода в исполняющей среде будет возникать
исключение, потому что в классе Саг необходимый интерфейс не поддерживается.
При создании специальных типов для обеспечения возможности сортировки
массивов, которые содержат элементы этих типов, можно реализовать интерфейс
IComparable. При реализации деталей CompareTo () решение о том, что должно брать­
ся за основу в операции упорядочивания, необходимо принимать самостоятельно. Для
рассматриваемого типа Саг с логической точки зрения наиболее подходящим на эту
роль “кандидатом” является внутренняя переменная CarlD:
// Упорядочивание элементов при итерации Саг
// может производиться на основе CarlD.
public class Car : IComparable
{
352
Часть III. Дополнительные конструкции программирования на C#
// Реализация IComparable.
int IComparable.CompareTo (object obj)
{
Car temp = obj as Car;
if (temp != null)
{
if (this.CarlD > temp.CarlD)
return 1;
if (this.CarlD < temp.CarlD)
return -1;
else
return 0;
}
else
throw new ArgumentException("Parameter is not a Car!");
// Параметр не является объектом типа Саг!
Как здесь показано, логика CompareTo () состоит в сравнении входного объекта
с текущим экземпляром по конкретному элементу данных. Возвращаемое значение
CompareTo () служит для выяснения того, является данный объект меньше, больше или
равным объекту, с которым он сравнивается (табл. 9.1).
Таблица 9.1. Значения, которые может возвращать CompareTo ()
Возвращаемое значение
Описание
Любое число меньше нуля
Обозначает, что данный экземпляр находится перед указанным
объектом в порядке сортировки
Нуль
Обозначает,что данный экземпляр равен указанному объекту
Любое число больше нуля
Обозначает, что данный экземпляр находится после указанного
объекта в порядке сортировки
Предыдущую реализацию CompareTo () можно упростить, благодаря тому, что в C#
тип данных m t (который представляет собой сокращенный вариант обозначения типа
System. Int32 в CLR) реализует интерфейс IComparable. Реализовать CompareTo () в
Саг можно следующим образом:
int IComparable.CompareTo (object obj)
Car temp = obj as Car;
if (temp != null)
return this.CarlD.CompareTo(temp.CarID);
else
throw new ArgumentException("Parameter is not a Car!");
// Параметр не является объектом типа Саг'
}
Далее в обоих случаях, чтобы тип Саг понимал, каким образом сравнивать себя с
подобными объектами, можно написать следующий код:
// Использование интерфейса IComparable.
static void Main (string [] args)
{
// Создание массива объектов Car.
Глава 9. Работа с интерфейсами
353
// Отображение текущего массива.
Console.WnteLine ("Here is the unordered set of cars:") ;
foreach(Car c in myAutos)
Console.WriteLine("{0} {1}", c.CarlD, c.PetName);
// Сортировка массива с применением интерфейса ХСохорагаЫе.
Array.Sort(myAutos);
// Отображение отсортированного массива.
Console.WnteLine ("Here is the ordered set of cars:") ;
foreach(Car c in myAutos)
Console.WriteLine("{0} {1}", c.CarlD, c.PetName);
Console.ReadLine();
}
Ниже показан вывод после выполнения приведенного выше метода Main () :
***** Fun with Object Sorting *****
Here is the unordered set of cars:
1 Rusty
234 Mary
34 Viper
4 Mel
5 Chucky
Hete is the ordered set of cars:
1 Rusty
4 Mel
5 Chucky
34 Viper
234 Mary
Указание множества критериев для сортировки ( iC o m p a r e r )
В предыдущей версии класса Саг в качестве основы для порядка сортировки ис­
пользовался идентификатор автомобиля (ca rlD ). В другой версии для этого могло бы
применяться дружественное название автомобиля (для перечисления автомобилей в а л­
фавитном порядке). А что если возникнет желание создать класс Саг, способный про­
изводить сортировку и по идентификатору, и по дружественному названию? Для этого
должен использоваться другой стандартный интерфейс по имени IComparer, который
поставляется в пространстве имен System. C o lle c t io n s и определение которого выгля­
дит следующим образом:
// Общий способ для сравнения двух объектов,
interface IComparer
{
int Compare(object ol, object o2) ;
На заметку! В обобщенной версии этого интерфейса (IC om p a rere< T > ) предлагается более
безопасный в отношении типов способ для обработки сравнений между объектами. Обобщения
более подробно рассматриваются в главе 10.
В отличие от ICom parable, интерфейс ICom parer обычно реализуется не в самом
подлежащем сортировке типе (Саг в рассматриваемом случае), а в наборе соответст­
вующих вспомогательных классов, по одному для каждого порядка сортировки (по
дружественному названию, идентификатору и т.д.). В настоящее время типу Саг (ав­
томобиль) уже “известно”, как ему следует сравнивать себя с другими автомобилями по
354
Часть III. Дополнительные конструкции программирования на C#
внутреннему идентификатору. Следовательно, чтобы позволить пользователю объекта
производить сортировку массива объектов Саг еще и по значению petName, понадо­
бится создать дополнительный вспомогательный класс, реализующий IComparer. Ниже
показан весь необходимый для этого код (перед его использованием важно не забыть
импортировать в файл кода пространство имен S y s te m .C o lle c tio n s ).
// Этот вспомогательный класс предназначен для
// обеспечения возможности сортировки массива
// объектов Саг по дружественному названию.
public class PetNameComparer : IComparer
{
// Проверка дружественного названия каждого объекта.
int IComparer.Compare (object ol, object o2)
{
Car tl = ol as Car;
Car t2 = o2 as Car;
if(tl != null && t2 != null)
return String.Compare (tl.PetName, t2 .PetName);
else
throw new ArgumentException("Parameter is not a Car!");
// Параметр не является объектом типа Саг1
}
}
Теперь можно использовать этот вспомогательный класс в коде. В S y stem .A rra y
имеется набор перегруженных версий метода S o rt ( ) , в одной из которых принимается
в качестве параметра объект, реализующий интерфейс IComparer.
static void Main(string[] args)
{
// Теперь выполнение сортировки по дружественному названию.
Array.Sort(myAutos, new PetNameComparer());
// Вывод отсортированного массива в окне консоли.
Console.WnteLine ("Ordering by pet name:");
foreach(Car c in myAutos)
Console .WnteLine (" {0 } {!}", CarlD, c. PetName);
Использование специальных свойств
и специальных типов для сортировки
Следует отметить, что можно также использовать специальное статическое свойство
для оказания пользователю объекта помощи с сортировкой типов Саг по какому-то кон­
кретному элементу данных. Для примера добавим в класс Саг статическое доступное
только для чтения свойство по имени SortByPetName, которое возвращает экземпляр
объекта, реализующего интерфейс IComparer (в этом случае PetNameComparer):
// Обеспечение поддержки для специального свойства,
// способного возвращать правильный интерфейс IComparer.
public class Car : IComparable
{
// Свойство, возвращающее компаратор SortByPetName.
public static IComparer SortByPetName
{ get { return (IComparer)new PetNameComparer() ; } }
}
Глава 9. Работа с интерфейсами
355
Теперь в коде пользователя объекта сортировка по дружественному названию может
выполняться с помощью строго ассоциированного свойства, а не автономного класса
PetNameComparer:
// Сортировка по дружественному названию теперь немного проще.
Array.Sort(myAutos, C ar. SortByPetName);
Исходный код. Проект ComparableCar доступен в подкаталоге Chapter 9.
К этому моменту должны быть понятны не только способы определения и реали­
зации собственных интерфейсов, но и то, какую пользу они могут приносить. Следует
отметить, что интерфейсы встречаются в каждом крупном пространстве имен .NET, и в
остальной части книги придется неоднократно иметь дело с различными стандартны­
ми интерфейсами.
Резюме
Интерфейс может быть определен как именованная коллекция абстрактных членов.
Из-за того, что никаких касающихся реализации деталей в интерфейсе не предоставля­
ется, интерфейс часто рассматривается как поведение, которое может поддерживаться
тем или иным типом. В случае, когда один и тот же интерфейс реализуют два или более
класса, каждый из типов может обрабатываться одинаково (что называется обеспече­
нием полиморфизма на основе интерфейса), даже если эти типы расположены в разных
иерархиях классов.
Для определения новых интерфейсов в C# предусмотрено ключевое слово interface.
Как было показано в этой главе, в любом типе может обеспечиваться поддержка для лю ­
бого количества интерфейсов за счет предоставления соответствующего разделенного
запятыми списка их имен. Более того, также допускается создавать интерфейсы, унас­
ледованные от нескольких базовых интерфейсов.
Помимо возможности создания специальных интерфейсов, в библиотеках .NET пред­
лагается набор стандартных (поставляемых вместе с платформой) интерфейсов. Как
было показано в этой главе, можно создавать специальные типы, реализующие пре­
допределенные интерфейсы, и получать доступ к желаемым возможностям, таким как
клонирование, сортировка и перечисление.
ГЛАВА
10
Обобщения
амым элементарным контейнером на платформе .NET является тип System .
A rray. Как было показано в главе 4, массивы C# позволяют определять наборы
типизированных элементов (включая массив объектов типа S y stem .O b ject, по сути
представляющий собой массив любых типов) с фиксированным верхним пределом. Хотя
базовые массивы могут быть удобны для управления небольшими объемами известных
данных, бывает ташке немало случаев, когда требуются более гибкие структуры дан­
ных, такие как динамически растущие и сокращающиеся контейнеры или контейне­
ры, которые хранят только элементы, отвечающие определенному критерию (например,
элементы, унаследованные от заданного базового класса, реализующие определенный
интерфейс или что-то подобное).
После появления первого выпуска платформы .NET программисты часто исполь­
зовали пространство имен S y s te m .C o lle c tio n s для получения более гибкого способа
управления данными в приложениях. Однако, начиная с версии .NET 2.0, язык про­
граммирования C# был расширен поддержкой средства, которое называется обобщени­
ем (generic). Вместе с ним библиотеки базовых классов пополнились совершенно новым
пространством имен, связанным с коллекциями — S y s te m .C o lle c tio n s .G en eric.
Как будет показано в настоящей главе, обобщенные контейнеры во многих отноше­
ниях превосходят свои необобщенные аналоги, обеспечивая высочайшую безопасность
типов и выигрыш в производительности. После общего знакомства с обобщениями бу­
дут описаны часто используемые классы и интерфейсы из пространства имен System.
C o lle c t io n s .G e n e r ic . В оставшейся части этой главы будет показано, как строить соб­
ственные обобщенные типы. Вы также узнаете о роли ограничений (constraint), которые
позволяют строить контейнеры, исключительно безопасные в отношении типов.
С
На заметку! Можно также создавать обобщенные типы делегатов; об этом пойдет речь в следую­
щей главе.
Проблемы, связанные
с необобщенными коллекциями
С момента появления платформы .NET программисты часто использовали простран­
ство имен S y s te m .C o lle c ito n s из сборки m s c o r lib .d ll. Здесь разработчики платфор­
мы предоставили набор классов, позволявших управлять и организовывать большие
объемы данных. В табл. 10.1 документированы некоторые наиболее часто используе­
мые классы коллекций, а также основные интерфейсы, которые они реализуют.
Глава 10. Обобщения
357
Таблица 10.1. Часто используемые классы из S y s t e m . C o l l e c t i o n s
Основные реализуемые
интерфейсы
Класс
Назначение
ArrayList
Представляет коллекцию динамически из­
меняемого размера, содержащую объекты в
определенном порядке
IList, ICollection,
IEnumerable и ICloneable
Hashtable
Представляет коллекцию пар “ключ/значение” ,
организованных на основе хеш-кода ключа
IDictionary, ICollection,
IEnumerable и ICloneable
Queue
Представляет стандартную очередь, рабо­
тающую по алгоритму FIFO (“первый во­
шел — первый вышел” )
и ICloneable
SortedList
Представляет коллекцию пар “ключ/значение” , отсортированных по ключу и доступных
по ключу и по индексу
Stack
Представляет стек LIFO (“последний во­
шел — первый вышел” ), поддерживающий
функциональность заталкивания (push) и вы­
талкивания (pop), а также считывания (реек)
ICollection, IEnumerable
IDictionary, Icollection,
IEnumerable и ICloneable
ICollection, IEnumerable
и ICloneable
Интерфейсы, реализованные этими базовыми классами коллекций, представля­
ют огромное “окно” в их общую функциональность. В табл. 10.2 представлено опи­
сание общей природы этих основных интерфейсов, часть из которых поверхностно
рассматривалась в главе 9.
Таблица 10.2. Основные интерфейсы, поддерживаемые классами S y s t e m .C o lle c t io n s
Интерфейс
Назначение
ICollection
Определяет общие характеристики (т.е. размер, перечисление и безопас­
ность к потокам) всех необобщенных типов коллекций
ICloneable
Позволяет реализующему объекту возвращать копию самого себя вызываю­
щему коду
IDictionary
Позволяет объекту необобщенной коллекции представлять свое содержимое
в виде пар “имя/значение”
IEnumerable
Возвращает объект, реализующий интерфейс IEnumerator
(см. следующую строку в таблице)
IEnumerator
Позволяет итерацию в стиле foreach по элементам коллекции
IList
Обеспечивает поведение добавления, удаления и индексирования элементов
в списке объектов
В дополнение к этим базовым классам (и интерфейсам) добавляю тся н еск оль­
ко специализированных типов коллекций, таких как BitVector32, ListDictionary,
StringDictionary и StringCollection, определенных в пространстве имен System.
Collections.Specialized из сборки System.dll. Это пространство имен также со­
держит множество дополнительных интерфейсов и абстрактных классов, которые
можно использовать в качестве отправной точки при создании специальных классов
коллекций.
358
Часть III. Дополнительные конструкции программирования на C#
Хотя за последние годы с применением этих “классических” классов коллекций
(и интерфейсов) было построено немало успешных приложений .NET, опыт показал, что
применение этих типов может быть сопряжено с множеством проблем.
Первая проблема состоит в том, что использование классов коллекций System.
Collections и System.Collections.Specialized приводит к созданию низкопроизво­
дительного кода, особенно если осуществляются манипуляции со структурами данных
(т.е. типами значения). Как вскоре будет показано, при сохранении структур в класси­
ческих классах коллекций среде CLR приходится выполнять массу операций перемеще­
ния данных в памяти, что может значительно снизить скорость выполнения.
Вторая проблема связана с тем, что эти классические классы коллекций не явля­
ются безопасными к типам, так как они (по большей части) были созданы для работы
с System.Object и потому могут содержать в себе все что угодно. Если разработчику
.NET требовалось создать безопасную в отношении типов коллекцию (т.е. контейнер,
который может содержать только объекты, реализующие определенный интерфейс), то
единственным реальным вариантом было создание совершенно нового класса коллек­
ции собственноручно. Это не слишком трудоемкая задача, но довольно утомительная.
Учитывая эти (и другие) проблемы, разработчики .NET 2.0 добавили новый набор
классов коллекций, собранных в пространстве ийен System.Collections .Generic.
В любом новом проекте, который создается с помощью платформы .NET 2.0 и последую­
щих версий, предпочтение должно отдаваться соответствующим обобщенным классам
перед унаследованными необобщенными.
На заметку! Следует повториться: любое приложение, которое строится на платформе версии
.NET 2.0 и выше, должно игнорировать классы из пространства имен System.Collect ions,
а использовать вместо них классы из пространства имен System.Collections.Generic.
Прежде чем будет показано, как использовать обобщения в своих программах, стоит
глубже рассмотреть недостатки необобщенных коллекций; это поможет лучше понять
проблемы, которые был призван решить механизм обобщений. Давайте создадим новое
консольное приложение по имени IssuesWithNongenericCollections и затем импор­
тируем пространство имен System.Collections в начале кода С#:
using System.Collections;
Проблема производительности
Как уже должно быть известно из главы 4, платформа .NETT поддерживает две обшир­
ные категории данных: типы значения и ссылочные типы. Поскольку в .NET определе­
ны две основных категории типов, однажды может возникнуть необходимость предста­
вить переменную одной категории в виде переменной другой категории. Для этого в C#
предлагается простой механизм, называемый упаковкой (boxing), который служит для
сохранения данных типа значения в ссылочной переменной. Предположим, что в мето­
де по имени SimpleBoxUnboxOperationO создана локальная переменная типа int:
static void SimpleBoxUnboxOperationO
{
// Создать переменную ValueType (int).
int mylnt = 25;
}
Если далее в приложении понадобится представить этот тип значения в виде ссы­
лочного типа, значение следует упаковать, как показано ниже:
private static void SimpleBoxUnboxOperationO
{
Глава 10. Обобщения
359
// Создать переменную ValueType (int).
int mylnt = 25;
// Упаковать int в ссылку на object,
object boxedlnt = mylnt;
}
Упаковку можно определить формально как процесс явного присваивания типа зна­
чения переменной System.Object. При упаковке значения CLR-среда размещает в куче
новый объект и копирует значение типа значения (в данном случае — 25) в этот экзем­
пляр. В качестве результата возвращается ссылка на вновь размещенный в куче объ­
ект. При таком подходе не нужно использовать набор классов-оболочек для временной
трактовки данных стека как объектов, размещенных в куче.
Противоположная операция также возможна, и она называется распаковкой
(unboxing). Распаковка — это процесс преобразования значения, хранящегося в объект­
ной ссылке, обратно в соответствующий тип значения в стеке. Синтаксически опера­
ция распаковки выглядит как нормальная операция приведения, однако ее семантика
несколько отличается. Среда CLR начинает с проверки того, что полученный тип дан­
ных эквивалентен упакованному типу; и если это так, копирует значение обратно в на­
ходящуюся в стеке переменную. Например, следующие операции распаковки работают
успешно при условии, что типом boxedlnt в действительности является int:
private static void SimpleBoxUnboxOperation()
{
// Создать переменную ValueType (int).
int mylnt = 25;
// Упаковать int в ссылку на object,
object boxedlnt = mylnt;
// Распаковать ссылку обратно в int.
int unboxedlnt = (int)boxedlnt;
}
Когда компилятор C# встречает синтаксис упаковки/распаковки, он генерирует CILкод, содержащий коды операций box/unbox. Заглянув в сборку с помощью утилиты
ildasm.exe, можно найти там следующий CIL-код:
.method private hidebysig static void SimpleBoxUnboxOperation () cil managed
// Code size 19 (0x13)
.maxstack 1
.locals init ([0] int32 mylnt, [1] object boxedlnt,
IL_0000
nop
IL_0001
ldc.i4.s 25
stloc.0
IL_0003
IL_0004
ldloc.O
box [m scorlib] System. Int32
IL_0005
IL_000a
stloc.1
IL_000b
ldloc.l
unbox.any [mscorlib]System.Int32
IL_000c
IL_0011
stloc.2
ret
IL 0012
} // end of method Program::SimpleBoxUnboxOperation
[2] int32 unboxedlnt)
Помните, что в отличие от обычного приведения распаковка должна производиться
только в соответствующий тип данных. Попытка распаковать порцию данных в некор­
ректную переменную приводит к генерации исключения InvalidCastException. Для
полной безопасности следовало бы поместить каждую операцию распаковки в конст­
рукцию try / ca tc h , однако делать это для абсолютно каждой операции распаковки в
360
Часть III. Дополнительные конструкции программирования на C#
приложении может оказаться довольно трудоемкой задачей. Взгляните на следующий
измененный код, который выдаст ошибку, поскольку предпринята попытка распако­
вать упакованный int в long:
private static void SimpleBoxUnboxOperation ()
{
// Создать переменную ValueType (int).
int mylnt = 25;
// Упаковать int в ссыпку на object,
object boxedlnt = mylnt;
// Распаковать в неверный тип данных, чтобы инициировать
// исключение времени выполнения,
try
{
long unboxedlnt = (long)boxedlnt;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
На первый взгляд упаковка/распаковка может показаться довольно несуществен­
ным средством языка, представляющим скорее академический интерес, чем практиче­
скую ценность. На самом деле процесс упаковки/распаковки очень полезен, поскольку
позволяет предположить, что все можно трактовать как System.Object, причем CLR
берет на себя все заботы о деталях, связанных с организацией памяти.
Давайте посмотрим на практическое применение этих приемов. Предположим, что
создан необобщенный класс System.Collections.ArrayList для хранения множества
числовых (расположенных в стеке) данных. Члены ArrayList прототипированы для ра­
боты с данными System.Object. Теперь рассмотрим методы Add(), Insert (), Remove(),
а также индексатор класса:
public class ArrayList : object,
IList, ICollection, IEnumerable, ICloneable
public
public
public
public
virtual
virtual
virtual
virtual
int A d d (o bject value);
void Insert(int index, o bject value);
void Remove(o bject obj);
object this[int index] {get; set; }
}
Класс A r r a y L is t ориентирован на работу с экземплярами o b je c t , которые пред­
ставляют данные, расположенные в куче, поэтому может показаться странным, что
следующий код компилируется и выполняется без ошибок:
static void WorkWithArrayList ()
{
// Типы значений упаковываются автоматически
// при передаче методу, запросившему объект.
ArrayList mylnts = new ArrayList ();
mylnts.Add(10);
mylnts.A d d (20);
mylnts.Add(35);
Console.ReadLine();
}
Глава 10. Обобщения
361
Несмотря на непосредственную передачу числовых данных в методы, которые при­
нимают тип object, исполняющая среда автоматически упаковывает их в данные, распол оженные в стеке.
При последующем извлечении элемента из ArrayList с использованием индексато­
ра типа потребуется распаковать операцией приведения объект, находящийся в куче, в
целочисленное значение, расположенное в стеке. Помните, что индексатор ArrayList
возвращает System.Object, а не System.Int32:
static void WorkWithArrayList ()
{
// Типы значений автоматически упаковываются, когда
// передаются члену, принимающему объект.
ArrayList mylnts = new ArrayList();
mylnts.Add(10);
mylnts.Add(20);
mylnts.Add(35);
// Распаковка происходит, когда объект преобразуется
// обратно в расположенные в стеке данные.
int i = (int)mylnts[0];
// Теперь значение вновь упаковывается,
// так как W riteL in e() требует объектных типов1
Console.WnteLine ("Value of your int: {0}", l) ;
Console.ReadLine();
}
Обратите внимание, что расположенные в стеке значения System.Int32 упаковы­
ваются перед вызовом ArrayList .Add (), чтобы их можно было передать в требуемом
виде System.Object. Кроме того, объекты System.Object распаковываются обрат­
но в System.Int32 после их извлечения из ArrayList с использованием индексатора
типа только для того, чтобы вновь быть упакованными для передачи в метод Console.
WriteLineO, поскольку этот метод оперирует переменными System.Object.
Хотя упаковка и распаковка очень удобна с точки зрения программиста, этот уп­
рощенный подход к передаче данных между стеком и кучей влечет за собой проблемы
производительности (это касается как скорости выполнения, так и размера кода), а так­
же недостаток безопасности в отношении типов. Чтобы понять, в чем состоят проблемы
с производительностью, взгляните на перечень действий, которые должны быть выпол­
нены при упаковке и распаковке простого целого числа.
1. Новый объект должен быть размещен в управляемой куче.
2. Значение данных, находящихся в стеке, должно быть передано в выделенное ме­
сто в памяти.
3. При распаковке значение, которое хранится в объекте, находящемся в куче, долж­
но быть передано обратно в стек.
4. Неиспользуемый больше объект в куче будет (в конечном) итоге удален сборщи­
ком мусора.
Хотя существующий метод Main() не является основным узким местом в смысле
производительности, вы определенно это почувствуете, если ArrayList будет содер­
жать тысячи целочисленных значений, к которым программа обращается на регуляр­
ной основе. В идеальном случае хотелось бы манипулировать расположенными в стеке
данными внутри контейнера, не имея проблем с производительностью. Было бы хо­
рошо иметь возможность извлекать данные из контейнера, обходясь без конструкций
try/catch (именно это обеспечивают обобщения).
362
Часть III. Дополнительные конструкции программирования на C#
Проблемы с безопасностью типов
Проблема безопасности типов уже затрагивалась, когда речь шла об операции рас­
паковки. Вспомните, что данные должны быть распакованы в тот же тип, который
был для них объявлен перед упаковкой. Однако есть и другой аспект безопасности
типов, который следует иметь в виду в мире без обобщений: тот факт, что классы из
System. Col lections могут хранить все что угодно, поскольку их члены прототипированы для работы с System.Object. Например, в следующем методе контейнер ArrayList
хранит произвольные фрагменты несвязанных данных:
static void ArrayListOfRandomObjects ()
{
// A rra y L ist может хранить все что угодно.
ArrayList allMyObject = new ArrayList ();
allMyObjects.Add(true) ;
allMyObjects.Add(new OperatingSystem(PlatformID.MacOSX, new Version(10, 0)));
allMyObjects.A d d (66) ;
allMyObjects.Add(3.14) ;
}
В некоторых случаях действительно необходим исключительно гибкий контейнер,
который может хранить буквально все. Однако в большинстве ситуаций понадобится
безопасный в отношении типов контейнер, который может оперировать только опре­
деленным типом данных, например, контейнер, который хранит только подключения к
базе данных, битовые образы или объекты, совместимые с IPointy.
До появления обобщений единственным способом решения этой проблемы было соз­
дание вручную строго типизированных коллекций. Предположим, что создана специ­
альная коллекция, которая может содержать только объекты типа Person:
public class Person
{
public int Age {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public Person () { }
public Person(string firstName, string lastName, int age)
{
Age = age;
FirstName = firstName;
LastName = lastName;
}
public override string ToStringO
{
return string.Format("Name: {0} {1}, Age: {2}",
FirstName, LastName, Age) ;
}
}
Чтобы построить коллекцию только элементов Person, можно определить перемен­
ную-член System.Collection.ArrayList внутри класса, именуемого PeopleCollection,
и сконфигурировать все члены для работы со строго типизированными объектами
Person вместо объектов типа System.Object. Ниже приведен простой пример (ре­
альная коллекция производственного уровня должна включать множество допол­
нительных членов и расширять абстрактный базовый класс из пространства имен
System. Col lections):
Глава 10. Обобщения
363
public class PeopleCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList ();
// Приведение для вызыважяцего кода.
public Person GetPerson (int pos)
{ return (Person)arPeople[pos]; }
// Вставка только объектов Person.
public void AddPerson (Person p)
{ arPeople.Add(p); }
public void ClearPeople ()
{ arPeople.Clear(); }
public int Count
{ get { return arPeople.Count; } }
// Поддержка перечисления с помощью foreach.
IEnumerator IEnumerable.GetEnumerator ()
{ return arPeople.GetEnumerator(); }
}
Обратите внимание, что класс PeopleCollection реализует интерфейс IEnumerable,
который делает возможной итерацию в стиле foreach по всем содержащимся в коллек­
ции элементам. Кроме того, методы GetPerson () и AddPerson () прототипированы на
работу только с объектами Person, а не битовыми образами, строками, подключениями
к базе данных или другими элементами. За счет создания таких классов обеспечивается
безопасность типов; при этом компилятору C# позволяется определять любую попытку
вставки элемента неподходящего типа:
static void UsePers-onCollection ()
{
Console.WriteLine (''***** Custom Person Collection *****\n");
PersonCollection myPeople = new PersonCollection ();
myPeople.AddPerson(new Person("Homer", "Simpson", 40));
myPeople.AddPerson(new Person("Marge", "Simpson", 38));
myPeople.AddPerson(new Person("Lisa", "Simpson", 9));
myPeople.AddPerson(new Person("Bart", "Simpson", 7));
myPeople.AddPerson(new Person("Maggie", "Simpson", 2));
// Это вызовет ошибку при компиляции!
// myPeople.AddPerson(new Car ());
foreach (Person p in myPeople)
Console.WriteLine(p);
}
Хотя подобные специальные коллекции гарантируют безопасность типов, такой
подход все же обязывает создавать (почти идентичные) специальные коллекции для ка­
ждого уникального типа данных, который планируется хранить. Таким образом, если
нужна специальная коллекция, которая будет способна оперировать только классами,
унаследованными от базового класса Саг, понадобится построить очень похожий класс
коллекции:
public class CarCollection : IEnumerable
{
private ArrayList arCars = new ArrayList ();
// Приведение для вызыважяцего кода.
public Car GetCar(int pos)
{ return (Car) arCars[pos]; }
// Вставка только объектов Car.
public void AddCar(Car c)
{ arCars.Add(c); }
364
Часть III. Дополнительные конструкции программирования на C#
public void ClearCars ()
{ arCars.Clear (); }
public int Count
{ get { return arCars.Count; } }
// Поддержка перечисления с помощью foreach.
IEnumerator IEnumerable.GetEnumerator()
{ return arCars.GetEnumerator (); }
}
Однако эти специальные контейнеры мало помогают в решении проблем упаковки/
распаковки. Даже если создать специальную коллекцию по имени In t C o lle c t io n , пред­
назначенную для работы только с элементами S y stem .In t32, все равно придется выде­
лить некоторый тип объекта для хранения данных (т.е. S ystem .A rray и A rra y L is t):
public class IntCollection : IEnumerable
{
private ArrayList arlnts = new ArrayList () ;
// Распаковать для вызывающего кода.
public int Getlnt(int pos)
{ return (int)arlnts [pos]; }
// Операция упаковки!
public void Addlnt(int i)
{ arlnts.Add(i); }
public void ClearlntsO
{ arlnts.Clear(); }
public int Count
{ get { return arInts.Count; } }
IEnumerator IEnumerable.GetEnumerator()
{ return arInts.GetEnumerator (); }
}
Независимо от того, какой тип выбран для хранения целых чисел, дилеммы упаков­
ки нельзя избежать, применяя необобщенные контейнеры.
В случае использования классов обобщенных коллекций исчезают все описанные
выше проблемы, включая затраты на упаковку/распаковку и недостаток безопасно­
сти типов. Кроме того, необходимость в построении собственного специального класса
обобщенной коллекции возникает редко. Вместо построения специальных коллекций,
которые могут хранить людей, автомобили и целые числа, можно обратиться к обоб­
щенному классу коллекции и указать тип хранимых элементов. В показанном ниже
методе класс L i s t o (из пространства имен S y s te m .C o lle c tio n .G e n e ric ) используется
для хранения различных типов данных в строго типизированной манере (пока не обра­
щайте внимания на детали синтаксиса обобщений):
static void UseGenericList ()
{
Console.WriteLine ("***** Fun with Generics *****\n");
// Этот L i s t O может хранить только объекты Person.
List<Person> morePeople = new List<Person> ();
morePeople.Add(new Person ("Frank", "Black", 50));
Console.WriteLine(morePeople [0]);
// Этот L i s t o может хранить только целые числа.
List<int> morelnts = new List<int>();
morelnts.A d d (10);
morelnts.A d d (2);
int sum = morelnts [0] + morelnts[1];
// Ошибка компиляции! Объект Person не может быть добавлен к списку целых!
// morelnts.Add(new Person ());
}
Глава 10. Обобщения
365
Первый объект Lis t o может хранить только объекты Person. Поэтому выполнять
приведение при извлечении элементов из контейнера не требуется, что делает этот под­
ход более безопасным в отношении типов. Второй Lis t o может хранить только целые,
и все они размещены в стеке; другими словами, здесь не происходит никакой скрытой
упаковки/распаковки, как это имеет место в необобщенном ArrayList.
Ниже приведен краткий список преимуществ обобщенных контейнеров перед их не­
обобщенными аналогами.
• Обобщения обеспечивают более высокую производительность, поскольку не стра­
дают от проблем упаковки/распаковки.
• Обобщения более безопасны в отношении типов, так как могут содержать только
объекты указанного типа.
• Обобщения значительно сокращают потребность в специальных типах кол­
лекций, потому что библиотека базовых классов предлагает несколько готовых
контейнеров.
Исходный код. Проект IssuesWithNonGenericCollections доступен в подкаталоге
Chapter 10.
Роль параметров обобщенных типов
Обобщенные классы, интерфейсы, структуры и делегаты буквально разбросаны по
всей базовой библиотеке классов .NET, и они могут быть частью любого пространства
имен .NET.
На заметку! Обобщенными могут быть только классы, структуры, интерфейсы и делегаты, но не
перечисления.
Отличить обобщенный элемент в документации .NET Framework 4.0 SDK или брау­
зере объектов Visual Studio 2010 от других элементов очень легко по наличию пары
угловых скобок с буквой или другой лексемой. На рис. 10.1 показано множество обоб­
щенных элементов в пространстве имен System.Collections .Generic, включая выде­
ленный класс List<T>.
Рис. 10.1. Обобщенные элементы, поддерживающие параметры типа
366
Часть III. Дополнительные конструкции программирования на C#
Формально эти лексемы можно называть п а р а м е т р а м и т ипа, однако в более дру­
жественных к пользователю терминах их можно считать просто м е с т а м и п одст а н ов ­
ки (placeholder). Конструкцию <Т> можно воспринимать как т и п а Т. Таким образом,
IEnumerable<T> можно читать как I E n u m e r a b l e т и п а Т, или, говоря иначе, п еречи с­
л е н и е т и п а Т.
На заметку! Имя параметра типа (места подстановки) не важно, и это — дело вкуса того, кто созда­
ет обобщенный элемент. Тем не менее, обычно для представления типов используется т, ТКеу
или к — для представления ключей, а также TValue или V — для представления значений.
При создании обобщенного объекта, реализации обобщенного интерфейса или вы­
зове обобщенного члена должно быть указано значение для параметра типа. Как в
этой главе, так и в остальной части книги будет показано немало примеров. Однако
для начала следует ознакомиться с основами взаимодействия с обобщенными типами
и членами.
Указание параметров типа для обобщенных классов и структур
При создании экземпляра обобщенного класса или структуры параметр типа указы­
вается, когда объявляется переменная и когда вызывается конструктор. В предыдущем
фрагменте кода было показано, что UseGenericList () определяет два объекта Listo:
// Этот L i s t o может хранить только объекты Person.
List<Person> morePeople = new List<Person> ();
Этот фрагмент можно трактовать как L i s t o о б ъ е к т о в Т, г д е Т — т и п P e r s o n , или
более просто — сп исок о б ъ е к т о в персон. Однажды указав параметр типа обобщенно­
го элемента, его нельзя изменить (помните: обобщения предназначены для поддерж­
ки безопасности типов). После указания параметра типа для обобщенного класса или
структуры все вхождения заполнителей заменяются указанным значением.
Просмотрев полное объявление обобщенного класса List<T> в браузере объектов
Visual Studio 2010, можно заметить, что заполнитель Т используется в определении
повсеместно. Ниже приведен частичный листинг (обратите внимание на выделенные
элементы):
// Частичный листинг класса List<T>.
namespace System.Collections.Generic
{
public class List<T> :
IList<T>, ICollection<T>, IEnumerable<T>,
IList, ICollection, IEnumerable
{
public
public
public
public
public
public
public
public
public
public
public
public
void Add(T item);
ReadOnlyCollection<T> AsReadOnly ();
int BinarySearch (T item);
bool Contains(T item);
void CopyTo(T[] array);
int Findlndex(System.Predicate<T> match);
T FindLast(System.Predicate<T> match);
bool Remove(T item);
int RemoveAll(System.Predicate<T> match);
T [] ToArrayO;
bool TrueForAll(System.Predicate<T> match);
T this [int index] { get; set; }
Глава 10. Обобщения
367
Когда создается List<T> с указанием объектов Person, это все равно, как если бы
тип List<T> был определен следующим образом:
namespace System.Collections.Generic
{
public class List<Person> :
IList<Person>, ICollection<Person>, IEnumerable<Person>,
IList, ICollection, IEnumerable
{
public
public
public
public
public
public
public
public
public
public
public
public
void Add(Person item);
ReadOnlyCollection<Person> AsReadOnly();
int BinarySearch(Person item);
bool Contains(Person item);
void CopyTo(Person[] array);
int Findlndex(System.Predicate<Person> match);
Person FindLast(System.Predicate<Person> match);
bool Remove(Person item);
int RemoveAll(System.Predicate<Person> match);
Person [] ToArrayO;
bool TrueForAll(System.Predicate<Person> match);
Person this[int index] { get; set; }
Разумеется, при создании программистом обобщенной переменной List<T> ком­
пилятор на самом деле не создает буквально совершенно новую реализацию класса
List<T>. Вместо этого он обрабатывает только члены обобщенного типа, к которым
действительно производится обращение.
На заметку! В следующей главе рассматриваются обобщенные делегаты, которые также при соз­
дании требуют указания параметра типа.
Указание параметров типа для обобщенных членов
Для необобщенного класса или структуры вполне допустимо поддерживать несколь­
ко обобщенных членов (т.е. методов и свойств). В таких случаях указывать значение
заполнителя нужно также и во время вызова метода. Например, System.Array поддер­
живает несколько обобщенных методов (которые появились в .NET 2.0). В частности,
статический метод Sort() теперь имеет обобщенный конструктор по имени Sort<T>().
Рассмотрим следующий фрагмент кода, в котором Т — это тип int:
int [] mylnts = { 10, 4, 2, 33, 93 };
// Указание заполнителя для обобщенного метода S o r t < > ().
Array.Sort<int>(mylnts);
foreach (int i in mylnts)
{
Console.WriteLine(i);
}
Указание параметров типов для обобщенных интерфейсов
Обобщенные интерфейсы обычно реализуются при построении классов или струк­
тур, которые должны поддерживать различные поведения платформы (т.е. клонирова­
ние, сортировку и перечисление). В главе 9 рассматривалось множество необобщен­
ных интерфейсов, таких как IComparable, IEnumerable, IEnumerator и IComparer.
Вспомните, как определен необобщенный интерфейс IComparable:
368
Часть III. Дополнительные конструкции программирования на C#
public interface IComparable
{
int CompareTo(object obj);
}
В той же главе 9 этот интерфейс был реализован в классе Саг для обеспечения
сортировки в стандартном массиве. Однако код требовал нескольких проверок вре­
мени выполнения и операций приведения, потому что параметром был общий тип
System .O bject:
public class Car : IComparable
{
// Реализация IComparable.
int IComparable.CompareTo (object obj )
{
Car temp = obj as Car;
if (temp '= null)
{
if (this.CarlD > temp.CarlD)
return 1;
if (this.CarlD < temp.CarlD)
return -1;
else
return 0;
}
else
throw new ArgumentException ("Parameter is not a Car!");
}
}
Теперь воспользуемся обобщенным аналогом этого интерфейса:
public interface IComparable<T>
-{
int CompareTo(T obj);
}
В этом случае код реализации будет значительно яснее:
public class Car : IComparable<Car>
{
// Реализация IComparable<T>.
int IComparable<Car>.CompareTo (Car obj)
{
if (this.CarlD > obj.CarlD)
return 1;
if (this.CarlD < obj.CarlD)
return -1;
else
return 0;
}
Здесь уже не нужно проверять, относится ли входной параметр к типу Саг, потому
что он может быть только Саг! В случае передачи несовместимого типа данных возни­
кает ошибка времени компиляции.
Получив начальные сведения о том, как взаимодействовать с обобщенными элементами,
а также ознакомившись с ролью параметров тала (т.е. заполнителей), можно приступать к
изучению классов и интерфейсов из пространства имен S ystem .C ollection s.G en eric.
Глава 10. Обобщения
369
Пространство имен
System.Collections .Generic
Основная часть пространства имен System.Collections.Generic располагается в
сборках mscorlib.dll и system.dll. В начале этой главы кратко упоминались некото­
рые из необобщенных интерфейсов, реализованных необобщенными классами коллек­
ций. Не должно вызывать удивление, что в пространстве имен System.Collections.
Generic определены обобщенные замены для многих из них.
В действительность есть много обобщенных интерфейсов, которые расширяют
свои необобщенные аналоги. Это может показаться странным; однако благодаря этому
реализации новых классов также поддерживают унаследованную функциональность,
имеющуюся у их необобщенных аналогов. Например, IEnumerable<T> расширяет
IEnumerable. В табл. 10.3 документированы основные обобщенные интерфейсы, с ко­
торыми придется иметь дело при работе с обобщенными классами коллекций.
На заметку! Если вы работали с обобщениями до выхода .NET 4.0, то должны быть знакомы с типа­
ми lSet<T> и SortedSet<T>, которые более подробно рассматриваются далее в этой главе.
Таблица 1 0 .3 . Основные интерфейсы, поддерживаемые классами из пространства
имен S y s t e m . C o l l e c t i o n s . G e n e r i c
Интерфейс
S y s t e m . C o lle c t io n s
Назначение
ICollection<T>
Определяет общие характеристики (например, размер, перечисление
и безопасность к потокам) для всех типов обобщенных коллекций
IComparer<T>
Определяет способ сравнения объектов
IDictionary<TKey,
TValue>
Позволяет объекту обобщенной коллекции представлять свое со­
держимое посредством пар "ключ/значение”
IEnumerable<T>
Возвращает интерфейс IEnumerator<T> для заданного объекта
IEnumerator<T>
Позволяет выполнять итерацию в стиле foreach по элементам
коллекции
IList<T>
Обеспечивает поведение добавления, удаления и индексации эле­
ментов в последовательном списке объектов
ISet<T>
Предоставляет базовый интерфейс для абстракции множеств
В пространстве имен System.Collectiobs.Generic также определен набор клас­
сов, реализующих многие из этих основных интерфейсов. В табл. 10.4 описаны часто
используемые классы из этого пространства имен, реализуемые ими интерфейсы и их
базовая функциональность.
Пространство имен System.Collections.Generic также определяет ряд вспомога­
тельных классов и структур, работающих в сочетании со специфическим контейнером.
Например, тип LinkedListNode<T> представляет узел внутри обобщенного контейнера
LinkedList<T>, исключение KeyNotFoundException генерируется при попытке полу­
чить элемент из коллекции с указанием несуществующего ключа, и т.д.
Следует отметить, что mscorlib.dll и System.dll — не единственные сборки,
которые добавляют новые типы в пространство имен System.Collections.Generic.
Например, System.Core.dll добавляет класс HashSet<T>. Детальные сведения о
пространстве имен System.Collections.Generic доступны в документации .NET
Framework 4.0 SDK.
370
Часть III. Дополнительные конструкции программирования на C#
Таблица 10.4. Классы из пространства имен S y s t e m . C o l l e c t i o n s . G e n e r i c
Обобщенный класс
Поддерживаемые основные
интерфейсы
Dictionary<TKey,
TValue>
ICollection<T>,
IDictionary<TKey, TValue>,
Назначение
Представляет обобщен­
ную коллекцию ключей и
значений
IEnumerable<T>
List<T>
ICollect ion<T>,
IEnumerable<T>, IList<T>
LinkedList<T>
ICollection<T>,
Динамически изменяе­
мый последовательный
список элементов
Представляет двуна­
правленный список
IEnumerable<T>
Queue<T>
ICollection (Это не опечатка!
Это интерфейс необобщенной кол­
лекции!), IEnumerable<T>
Обобщенная реализация
очереди — списка, ра­
ботающего по алгоритму
“первые вошел — пер­
вый вышел” (FIFO)
SortedDictionary<TKey,
TValue>
ICollection<T>,
IDictionary<TKey, TValue>,
Обобщенная реализа­
ция сортированного
множества пар “ключ/
значение”
IEnumerable<T>
SortedSet<T>
ICollect ion<T>,
IEnumerable<T>, ISet<T>
Stack<T>
ICollection (Это не опечатка!
Это интерфейс необобщенной кол­
лекции!), IEnumerable<T>
Представляет коллекцию
объектов, поддерживае­
мых в сортированном по­
рядке без дублирования
Обобщенная реализация
стека — списка, рабо­
тающего по алгоритму
“последний вошел —
первый вышел” (LIFO)
В любом случае следующей задачей будет научиться использовать некоторые из этих
обобщенных классов коллекций. Но прежде давайте рассмотрим языковые средства C#
(впервые появившиеся в .NET 3.5), которые упрощают наполнение данными обобщен­
ных (и необобщенных) коллекций.
Синтаксис инициализации коллекций
В главе 4 был представлен синтаксис инициализации объектов, который позволя­
ет устанавливать свойства для новой переменной во время ее конструирования. Тесно
связан с этим синтаксис инициализации коллекций. Это средство языка C# позволя­
ет наполнять множество контейнеров (таких как ArrayList или List<T>) элемента­
ми, используя синтаксис, похожий на тот, что применяется для наполнения базового
массива.
На заметку! Синтаксис инициализации коллекций может применяться только к классам, ко­
торые поддерживают метод A d d (), формализованный интерфейсами ICollection<T>/
ICollection.
Глава 10. Обобщения
371
Рассмотрим следующие примеры:
// Инициализация стандартного массива.
int [] myArrayOf Ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Инициализация обобщенного списка L i s t o элементов int.
List<int> myGenericList = new List<int> { 0, 1, 2 , 3 , 4, 5 ,
// Инициализация ArrayList числовыми данными.
ArrayList myList = new ArrayList { 0, 1, 2 , 3 ,
4,
5, 6,
1,
6,
1,
8, 9 };
8, 9 };
Если контейнер управляет коллекцией классов или структур, можно смешивать
синтаксис инициализации объектов с синтаксисом инициализации коллекций, созда­
вая некоторый функциональный код. Возможно, вы помните класс Point из главы 5,
в котором были определены два свойства X и Y. Чтобы построить обобщенный список
List<T> объектов Р, можно написать такой код:
List<Point> myListOfPoints = new List<Point>
{
new Point { X = 2, Y = 2 },
new Point { X = 3, Y = 3 },
new Point(PointColor.BloodRed){ X =
4,
Y = 4 }
};
foreach (var pt in myListOfPoints)
{
Console.WriteLine (pt);
Преимущество этого синтаксиса в экономии большого объема клавиатурного ввода.
Хотя вложенные фигурные скобки затрудняют чтение, если не позаботиться о форма­
тировании, представьте объем кода, который бы потребовался для наполнения следую­
щего списка List<T> объектов Rectangle, если бы не было синтаксиса инициализации
коллекций (вспомните, как в главе 4 создавался класс Rectangle, который содержал два
свойства, инкапсулирующих объекты Point):
List<Rectangle> myListOfRects = new List<Rectangle>
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 },
BottomRight = new Point { X = 200, Y = 200} },
new Rectangle {TopLeft = new Point { X = 2, Y = 2 },
BottomRight = new Point { X = 100, Y = 100} },
new Rectangle {TopLeft = new Point { X = 5, Y = 5 },
BottomRight = new Point { X = 90, Y = 75}}
foreach (var r in myListOfRects)
{
Console.WriteLine(r);
Работа с классом L i s t < T >
Д ля начала создадим новый проект к он сольн ого п р и лож ен и я по им ени
FunWithGenericCollections. Проекты этого типа автоматически ссылаются на сбор­
ки mscorlib.dll и System.dll, что обеспечивает доступ к большинству обобщенных
классов коллекций. Обратите внимание, что в первоначальном файле кода C# уже им­
портируется пространство имен System.Collections.Generic.
Первый обобщенный класс, который мы рассмотрим — это List<T>, который
уже использовался ранее в этой главе. Из всех классов пространства имен System.
Col lections. Generic класс List<T> будет применяться наиболее часто, потому что
он позволяет динамически изменять размер своего содержимого. Чтобы проиллюстри­
372
Часть III. Дополнительные конструкции программирования на C#
ровать основы этого типа, добавьте в класс Program метод UseGenericList(), в кото­
ром List<T> используется для манипуляций множеством объектов Person; вы должны
помнить, что в классе Person определены три свойства (Age, FirstName и LastName) и
специальная реализация метода ToStringO.
private static void UseGenericList ()
{
// Создать список объектов Person и заполнить его с помощью
// синтаксиса инициализации объектов/коллекций.
List<Person> people = new List<Person> ()
{
new
new
new
new
Person
Person
Person
Person
{FirstName=
{FirstName=
{FirstName=
{FirstName=
"Homer", LastName="Simpson", Age=47},
"Marge", LastName="Simpson", Age=45},
"Lisa", LastName="Simpson", Age=9},
"Bart", LastName="Simpson", Age=8}
};
// Вывести на консоль количество элементов в списке.
Console .WnteLine ("Items in list: {0}", people.Count);
// Перечислить список,
foreach (Person p in people)
Console .WnteLine (p) ;
// Вставить новую персону.
Console .WnteLine (" \n->Inserting new person.") ;
people.Insert(2, new Person { FirstName = "Maggie", LastName = "Simpson", Age = 2 });
Console .WnteLine ("Items in list: {0}", people.Count);
// Скопировать данные в новый массив.
Person[] arrayOfPeople = people.ToArray() ;
for (int i = 0 ; i < arrayOfPeople.Length; i++)
{
Console .WnteLine ("First Names: {0}", arrayOfPeople[l].FirstName);
Здесь вы используете синтаксис инициализации для наполнения вашего L ist< T >
объектами, как сокращенную нотацию вызовов A dd() п раз. Выведя количество эле­
ментов в коллекции (а также пройдясь по каждому элементу), вы вызываете In s e r t ().
Как можно видеть, I n s e r t () позволяет вам вставить новый элемент в L ist< T > по ука­
занному индексу.
И наконец, обратите внимание на вызов метода T o A r r a y (), который возвращает
массив объектов Person, основанный на содержимом исходного List<T>. Затем вы вы­
полняете проход по всем элементам этого массива, используя синтаксис индекса масси­
ва. Если вы вызовете этот метод из M ain(), то получите следующий вывод:
***** Fun with Generic Collections *****
Items
Name:
Name:
Name:
Name:
in list: 4
Homer Simpson, Age: 47
Marge Simpson, Age: 45
Lisa Simpson, Age: 9
Bart Simpson, Age: 8
->Inserting new person.
Items in list: 5
First Names: Homer
First Names: Marge
First Names: Maggie
First Names: Lisa
First Names: Bart
Глава 10. Обобщения
373
В классе List<T> определено множество дополнительных членов, представляю­
щих интерес, поэтому за дополнительной информацией обращайтесь в документацию
.NET Framework 4.0 SDK. Теперь рассмотрим еще несколько обобщенных коллекций:
Stack<T>, Queue<T> и SortedSet<T>. Это даст более полное понимание базовых вари­
антов хранения данных в приложении.
Работа с классом S t a c k < T >
Класс Stack<T> представляет коллекцию элементов, работающую по алгоритму “по­
следний вошел — первый вышел” (LIFO). Как и можно было ожидать, в Stack<T> опре­
делены члены Push () и Рор(), предназначенные для вставки и удаления элементов в
стеке. Приведенный ниже метод создает коллекцию Stack<T> объектов Person:
static void UseGenericStack()
{
Stack<Person> stackOfPeople = new Stack<Person>() ;
stackOfPeople.Push(new Person
{ FirstName = "Homer", LastName = "Simpson", Age = 47 });
stackOfPeople.Push(new Person
{ FirstName = "Marge", LastName = "Simpson", Age = 4 5 });
stackOfPeople.Push(new Person
{ FirstName = "Lisa", LastName = "Simpson", Age = 9 });
// Просмотреть верхний элемент, вытолкнуть его и просмотреть снова.
Console.WriteLine("First person is: {0}", stackOfPeople.Peek());
Console .WnteLine ("Popped of f {0}", stackOf People .Pop ()) ;
Console.WriteLine ("\nFirst person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop ()) ;
Console.WriteLine("\nFirst person item is: {0}", stackOfPeople.Peek ()) ;
Console.WriteLine ("Popped off {0}", stackOfPeople.Pop ());
try
{
Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek());
Console.WriteLine("Popped off {0}", stackOfPeople.Pop ());
}
catch (InvalidOperationException ex)
{
Console.WriteLine("\nError! {0}", ex.Message); // стек пуст
}
}
В коде строится стек, содержащий информацию о трех людях, добавленных по по­
рядку имен: Homer, Marge и Lisa. Заглядывая (peek) в стек, вы всегда видите объект, на­
ходящийся на его вершине; поэтому первый вызов Реек() вернет третий объект Person.
После серии вызовов Рор() и Реек() стек, наконец, опустошается, после чего вызовы
Реек () и Рор() приводят к генерации системного исключения. Вывод этого примера
показан ниже:
***** Fun with Generic Collections *****
First person is: Name: Lisa Simpson, Age: 9
Popped off Name: Lisa Simpson, Age: 9
First person is: Name: Marge Simpson, Age: 45
Popped off Name: Marge Simpson, Age: 45
First person item is: Name: Homer Simpson, Age: 47
Popped off Name: Homer Simpson, Age: 47
Error1 Stack empty.
374
Часть III. Дополнительные конструкции программирования на C#
Работа с классом Q u e u e < T >
Очереди — это контейнеры, гарантирующие доступ к элементам в стиле “первый
вошел — первый вышел” (FIFO). К сожалению, людям приходится сталкиваться с очере­
дями каждый день: очереди в банк, очереди в кинотеатр, очереди в кафе. Когда нужно
смоделировать сценарий, в котором элементы обрабатываются в режиме FIFO, класс
Queue<T> подходит наилучшим образом. В дополнение к функциональности, предос­
тавляемой поддерживаемыми интерфейсами, Queue определяет основные члены, которые перечислены в табл. 10.5.
Таблица 10.5. Члены типа Q u eu e<T>
Член
Назначение
Dequeue ()
Удаляет и возвращает объект из начала Queue<T>
Enqueue ()
Добавляет объект в конец Queue<T>
Peek()
Возвращает объект из начала Queue<T>, не удаляя его
Теперь давайте посмотрим на эти методы в работе. Можно снова вернуться к
классу Person и построить объект Queue<Т>, эмулирующий очередь людей, которые
ожидают заказа кофе. Для начала представим, что имеется следующий статический
метод:
static void GetCoffee(Person p)
{
Console.WriteLine("{0} got coffee!", p .FirstName) ;
}
Кроме того, есть также дополнительный вспомогательный метод, который вызывает
GetCof fee () внутренне:
static void UseGenencQueue ()
{
// Создать очередь из трех человек.
Queue<Person> peopleQ = nev; Queue<Person> ();
peopleQ.Enqueue (new Person {FirstIIame= "Homer",
LastName="Simpson", Age=47});
peopleQ.Enqueue(new Person {FirstName= "Marge",
LastName="Simpson", Age=45});
peopleQ.Enqueue(new Person {FirstName= "Lisa",
LastName="Simpson", Age=9});
// Кто первый в очереди?
Console.WriteLine ("{0} is first in line!", peopleQ.Peek().FirstName);
// Удалить всех из очереди.
GetCoffee(peopleQ.Dequeue ());
GetCof fee(peopleQ.Dequeue());
GetCof fee(peopleQ.Dequeue());
.
// Попробовать извлечь кого-то из очереди снова?
try
{
GetCoffee(peopleQ.Dequeue());
}
catch(InvalidOperationException e)
{
Console.WriteLine ("Error 1 {0}", e.Message); // очередь пуста
}
Глава 10. Обобщения
375
Здесь вы вставляете три элемента в класс Queue<T>, используя метод Enqueue ().
Вызов Реек () позволяет вам просматривать (но не удалять) первый элемент, находя­
щийся в данный момент в Queue. Наконец, вызов Dequeue () удаляет элемент из очере­
ди и посылает его вспомогательной функции GetCoffeeO для обработки. Заметьте, что
если вы пытаетесь удалять элементы из пустой очереди, генерируется исключение вре­
мени выполнения. Приведем вывод, который вы получаете при вызове этого метода:
***** Fun with Generic Collections *****
Homer is first in line1
Homer got coffee!
Marge got coffee!
Lisa got coffee!
Error! Queue empty.
Работа с классом S o r t e d S e t < T >
Последний из классов обобщенных коллекций, который мы рассмотрим здесь, поя­
вился в версии .NET 4.0. Класс SortedSet<T> удобен тем, что при вставке или удале­
нии элементов он автоматически обеспечивает сортировку элементов в наборе. Класс
SortedSet<T> понадобится информировать о том, как должны сортироваться объек­
ты, за счет передачи его конструктору аргумента — объекта, реализующего интерфейс
IComparer<T>.
Начнем с создания нового класса по имени SortPeopleByAge, реализующего
IComparer<T>, где Т — тип Person. Вспомните, что этот интерфейс определяет единст­
венный метод по имени Compare (), в котором можно запрограммировать логику срав­
нения элементов. Ниже приведена простая реализация этого класса:
class SortPeopleByAge : IComparer<Person>
{
public int Compare(Person firstPerson, Person secondPerson)
{
if (firstPerson.Age > secondPerson.Age)
return 1;
if (firstPerson.Age < secondPerson.Age)
return -1;
else
return 0;
}
}
Теперь добавим в класс Program следующий новый метод, который должен будет
вызван в Main():
private static void UseSortedSet ()
{
// Создать несколько людей разного возраста.
SortedSet<Person> setOfPeople = new SortedSet<Person>(new SortPeopleByAge())
{
new
new
new
new
Person
Person
Person
Person
{FirstName=
{FirstName=
{FirstName=
{FirstName=
"Homer", LastName="Simpson", Age=47},
"Marge", LastName="Simpson", Age=45},
"Lisa", LastName="Simpson", Age=9},
"Bart", LastName="Simpson", Age=8}
};
// Обратите внимание, что элементы отсортированы по возрасту,
foreach (Person р in setOfPeople)
{
Console.WriteLine(р);
376
Часть III. Дополнительные конструкции программирования на C#
Console.WnteLine () ;
// Добавить еще несколько людей разного возраста.
setOfPeople.Add(new Person { FirstName = "Saku", LastName = "Jones", Age = 1 });
setOfPeople.Add(new Person { FirstName = "Mikko", LastName = "Jones", Age = 32 });
// Элементы по-прежнему отсортированы по возрасту,
foreach (Person р in setOfPeople)
{
Console .WnteLine (p) ;
После запуска приложения видно, что список объектов будет всегда упорядочен
по значению свойства Аде, независимо от порядка вставки и удаления объектов в
коллекцию:
★ ★ ★ ★ ★ Fun with Generic Collections
Name :
Name:
Name :
Name :
Bart Simpson, Age: 8
Lisa Simpson, Age: 9
Marge Simpson, Age: 45
Homer Simpson, Age: 47
Name :
Name :
Name :
Name :
Name :
Name :
Saku Jones, Age: 1
Bart Simpson, Age: 8
Lisa Simpson, Age: 9
Mikko Jones, Age: 32
Marge Simpson, Age: 45
Homer Simpson, Age: 47
Великолепно! Теперь вы должны почувствовать себя увереннее, причем не только в
отношении преимуществ обобщенного программирования вообще, но также в исполь­
зовании обобщенных типов из библиотеки базовых классов .NET. В завершении этой
главы будет также показано, как и для чего строить собственные обобщенные типы и
обобщенные методы.
Исходный код. Проект F u n W ith G e n e ric C o lle c tio n s доступен в подкаталоге Chapter 10.
Создание специальных обобщенных методов
Хотя большинство разработчиков обычно используют существующие обобщенные
типы из библиотек базовых классов, можно также строить собственные обобщенные
методы и специальные обобщенные типы. Чтобы понять, как включать обобщения в
собственные проекты, начнем с построения обобщенного метода обмена, предваритель­
но создав новое консольное приложение по имени G enericM ethods.
Построение специальных обобщенных методов представляет собой более развитую
версию традиционной перегрузки методов. В главе 2 было показано, что перегрузка —
это определение нескольких версий одного метода, отличающихся друг от друга коли­
чеством или типами параметров.
Хотя перегрузка — полезное средство объектно-ориентированного языка, при этом
возникает проблема, вызванная появлением огромного числа методов, которые по су­
ществу делают одно и то же. Например, предположим, что требуется создать методы,
которые позволяют менять местами два фрагмента данных. Можно начать с написания
простого метода для обмена двух целочисленных значений:
Глава 10. Обобщения
377
// Обмен двух значений int.
static void Swap(ref int a, ref int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
Пока все хорошо. А теперь представим, что нужно поменять местами два объекта
Person; для этого понадобится новая версия метода Swap():
// Обмен двух объектов Person.
static void Swap(ref Person a, ref Person b)
{
Person temp;
temp = a;
a = b;
b = temp;
}
Уже должно стать ясно, куда это ведет. Если также потребуется поменять местами два
значения с плавающей точкой, две битовые карты, два объекта автомобилей, придется
писать дополнительные методы, что в конечном итоге превратится в кошмар при сопро­
вождении. Правда, можно построить один (необобщенный) метод, оперирующий пара­
метрами типа object, но тогда возникнут проблемы, которые были описаны ранее в этой
главе, т.е. упаковка, распаковка, недостаток безопасности типов, явное п р и в ед ете и т.п.
Всякий раз, когда имеется группа перегруженных методов, отличающихся только
входными аргументами — это явный признак того, что за счет применения обобщений
удастся облегчить себе жизнь. Рассмотрим следующий обобщенный метод Swap<T>, ко­
торый может менять местами два значения Т:
// Этот метод обменивает между собой значения двух
// элементов типа, переданного в параметре <Т>.
static void Swap<T>(ref Т a, ref Т b)
{
Console.WnteLine ("You sent the Swap () method a {0}", typeof(T));
T temp;
temp = a;
a = b;
b = temp;
}
Обратите внимание, что обобщенный метод определен за счет спецификации пара­
метра типа после имени метода и перед списком параметров. Здесь устанавливается,
что метод Swap () может оперировать любыми двумя параметрами типа <Т>. Чтобы не­
много прояснить картину, имя подставляемого типа выводится на консоль с использо­
ванием операции typeof (). Теперь рассмотрим следующий метод М аш (), обмениваю­
щий значениями целочисленные и строковые переменные:
static void Main(string[] args)
{
Console.WnteLine (''***** Fun with Custom Generic Methods *****\n");
// Обмен двух значений int.
int a = 10, b = 90;
Console. WnteLine ("Before swap: {0}, {1}", a, b) ;
Swap<int>(ref a, ref b);
Console.WnteLine ("After swap: {0}, {1}", a, b) ;
Console.WnteLine () ;
378
Часть III. Дополнительные конструкции программирования на C#
// Обмен двух строк.
string si = "Hello", s2 = "There";
Console.WriteLine("Before swap: {0} {1}!", si, s2) ;
Swap<stnng> (ref si, ref s2);
Console.WriteLine("After swap: {0} {1}!", si, s2);
Console.ReadLine();
Ниже показан вывод этого примера:
***** Fun with Custom Generic Methods *****
Before swap: 10, 90
You sent the SwapO method a System. Int32
After swap: 90, 10
Before swap: Hello Therel
You sent the SwapO method a System.String
After swap: There Hello 1
Основное преимущество этого подхода в том, что нужно будет сопровождать только
одну версию Swap<T>(), хотя она может оперировать любыми двумя элементами опре­
деленного типа, причем в безопасной к типам манере. Еще лучше то, что находящиеся
в стеке элементы остаются в стеке, а расположенные в куче — соответственно, в куче.
Выведение параметра типа
При вызове таких обобщенных методов, как Swap<T>, можно опускать параметр тип,
если (и только если) обобщенный метод требует аргументов, поскольку компилятор мо­
жет вывести параметр типа из параметров членов. Например, добавив к Main() следую­
щий код, можно обменивать значения System.Boole ап:
// Компилятор самостоятельно выведет тип System.Boolean.
bool Ы = true, Ь2 = false;
Console .WriteLine ("Before swap: {0}, {1}", bl, b2) ;
Swap (ref bl, ref b2);
Console.WriteLine("After swap: {0}, {1}", bl, b2) ;
Несмотря на то что компилятор может определить параметр типа на основе типа
данных, использованного в объявлении Ы и Ь2, стоит выработать привычку всегда ука­
зывать параметр типа явно:
Swap<bool> (ref bl, ref Ь2);
Это даст понять неопытным программистам, что данный метод на самом деле яв­
ляется обобщенным. Более того, выведение типов параметров работает только в том
случае, если обобщенный метод имеет, по крайней мере, один параметр. Например,
предположим, что в классе Program определен следующий обобщенный метод:
static void DisplayBaseClass<T> ()
{
// BaseType — это метод, используемый в рефлексии;
// он будет рассматриваться в главе 15.
Console.WriteLine ("Base class of {0} is: {1}.",
typeof (T), typeof(T).BaseType);
}
При его вызове потребуется указать параметр типа:
static void Main(string[] args)
{
// Необходимо указать параметр типа если метод не принимает параметров.
Глава 10. Обобщения
379
DisplayBaseClass<int>();
DisplayBaseClass<stnng> () ;
// Ошибка на этапе компиляции! Нет параметров?
// Значит, необходимо указать тип для подстановки!
// DisplayBaseClass ();
Console.ReadLine();
}
В настоящее время обобщенные методы Swap<T> и DisplayBaseClass<T> определе­
ны в классе Program приложения. Конечно, как и любой другой метод, если вы захоти­
те определить эти члены в отдельном классе (MyGenericMethods), то можно поступить
так:
public static class MyGenericMethods
{
public static void Swap<T>(ref T a, ref T b)
{
Console.WnteLine ("You sent the SwapO method a {0}",
typeof(T) );
T temp;
temp = a;
a = b;
b = temp;
}
public static void DisplayBaseClass<T> ()
{
Console.WnteLine ("Base class of {0} is: {1}.",
typeof(T), typeof(T).BaseType);
}
Статические методы Swap<T> и DisplayBaseClass<T> находятся в контексте нового
статического типа класса, поэтому потребуется указать имя типа при вызове каждого
члена, например:
MyGenericMethods.Swap<int>(ref a, ref b) ;
Разумеется, методы не обязательно должны быть статическими. Если бы Swap<T> и
DisplayBaseClass<T> были методами уровня экземпляра (определенными в нестатиче­
ском классе), нужно было бы просто создать экземпляр MyGenericMethods и вызывать
их с использованием объектной переменной:
MyGenericMethods с = new MyGenericMethods();
с .Swap<int>(ref a, ref b) ;
Исходный код. Проект CustomGenericMethods доступен в подкаталоге Chapter 10.
Создание специальных обобщенных
структур и классов
Теперь, когда известно, как определяются и вызываются обобщенные методы, да­
вайте посмотрим, как конструировать обобщенную структуру (процесс построения
обобщенного класса идентичен) в новом проекте консольного приложения по имени
GenericPoint. Предположим, что строится обобщенная структура Point, которая под­
держивает единственный параметр типа, определяющий внутреннее представление ко­
ординат (х, у). Вызывающий код должен иметь возможность создавать типы Point<T>
следующим образом:
380
Часть III. Дополнительные конструкции программирования на C#
// Точка с координатами in t.
Point<int> р = new Point<int> (10, 10);
// Точка с координатами double.
Point<double> р2 = new Point<double>(5.4, 3.3);
А вот полное определение Point<T> с последующим анализом:
// Обобщенная структура Point.
public struct Point<T>
{
// Обобщенные данные состояния.
private Т xPos;
private Т yPos;
// Обобщенный конструктор.
public Point(Т xVal, Т yVal)
{
xPos = xVal;
yPos = yVal;
// Обобщенные свойства.
public T X
{
get { return xPos; }
set { xPos = value; }
}
public T Y
{
get { return yPos; }
set { yPos = value; }
}
public override string ToStringO
{
return string.Format (" [{0 }, {1}]", xPos, yPos);
}
// Сбросить поля в значения по умолчанию для заданного параметра типа.
public void ResetPoint ()
{
xPos = default (Т);
yPos = default(T);
Ключевое слово d e f a u l t в обобщенном коде
Как видите, Point<T> использует параметр типа в определении данных полей, ар­
гументов конструктора и определении свойств. Обратите внимание, что в дополнение
к переопределению T o S trin g O , в Point<T> определен метод по имени R e s e tP o in t(), в
котором используется некоторый новый синтаксис:
// Ключевое слово d e fa u lt перегружено в С#.
// При использовании с обобщениями оно представляет
// значение по умолчанию для параметра типа.
public void ResetPoint ()
{
X = d e fa u lt (Т );
Y = d e fa u lt (Т );
}
Глава 10. Обобщения
381
С появлением обобщений ключевое слово d e fa u lt обрело второй смысл. В дополне­
ние к использованию с конструкцией sw itch оно теперь может применяться для уста­
новки значения по умолчанию для параметра типа. Это очень удобно, учитывая, что
обобщенный тип не знает заранее, что будет подставлено вместо заполнителя в угловых
скобках, и потому не может безопасно строить предположений о значениях по умолча­
нию. Умолчания для параметров типа следующие:
• значение по умолчанию числовых величин равно 0;
• ссылочные типы имеют значение по умолчанию n u ll;
• поля структур устанавливаются в 0 (для типов значений) или в n u ll (для ссылоч­
ных типов).
Для Point<T> можно было установить значение X и Y в 0 напрямую, исходя из пред­
положения, что вызывающий код будет применять только числовые значения. Однако
за счет использования синтаксиса d e fa u lt (Т) повышается общая гибкость обобщен­
ного типа. В любом случае теперь можно использовать методы Poin t<T> следующим
образом:
static void Main(string [] args)
{
Console. WnteLine ("***** Fun with Generic Structures *****\n");
// Объект Point, в котором используются in t.
Point<int> p = new Point<int>(10, 10);
Console .WnteLine ("p.ToStnng () = {0 }", p .ToString () ) ;
p .ResetPoint();
Console.WriteLine("p.ToString()={0}", p .ToString());
Console.WnteLine () ;
// Объект Point, в котором используются double.
Point<double> p2 = new Point<double>(5.4, 3.3);
Console.WriteLine ("p2.ToString()={0}", p 2 .ToString());
p 2 .ResetPoint();
Console.WnteLine ("p2 .ToString () = {0 }", p2 .ToString () ) ;
Console.ReadLine ();
}
Ниже показан вывод этого примера:
***** Fun with Generic Structures *****
p .ToString()=[10, 10]
p .ToString () = [0, 0]
p2 .ToString() = [5.4, 3.3]
p2 .ToString () = [0, 0]
Исходный код. Проект G e n e ric P o in t доступен в подкаталоге Chapter 10.
Обобщенные базовые классы
Обобщенные классы могут служить базовыми для других классов, и потому опре­
делять любое количество виртуальных или абстрактных методов. Однако типы-нас­
ледники должны подчиняться нескольким правилам, чтобы гарантировать сохранение
природы обобщенной абстракции. Во-первых, если необобщенный класс расширяет
обобщенный класс, то класс-наследник должен указывать параметр типа:
382
Часть III. Дополнительные конструкции программирования на C#
// Предположим, что создан специальный класс обобщенного списка.
public class MyList<T>
{
private List<T> listOfData = new List<T>();
}
// Heобобщенные типы должны указывать параметр типа
// при наследовании от обобщенного базового класса.
public class MyStnngList : MyList<string>
{}
Во-вторых, если обобщенный базовый класс определяет обобщенные или абстракт­
ные методы, то тип-наследник должен переопределять обобщенные методы, используя
указанный параметр типа:
// Обобщенный класс с виртуальным методом.
public class MyList<T>
{
private List<T> listOfData = new List<T>();
public virtual void PnntList(T data) { }
}
public class MyStnngList : MyList<string>
{
// Должен подставлять параметр типа, использованный
/ / в родительском классе, в унаследованные методы.
public override void PnntList (string data) { }
}
В-третьих, если тип-наследник также является обобщенным, дочерний класс может
(дополнительно) повторно использовать тот же заполнитель типа в своем определении.
Однако имейте в виду, что любые ограничения, наложенные на базовый класс, должны
соблюдаться в типе-наследнике. Например:
// Обратите внимание на ограничение — конструктор по умолчанию.
public class MyList<T> where T : new()
{
private List<T> listOfData = new List<T>();
public virtual void PrintList(T data) { }
}
// Тип-наследник должен соблюдать ограничения.
public class MyReadOnlyList<T> : MyList<T> where T : new ()
{
public override void PnntList (T data) { }
}
При решении повседневных программистских задач вряд ли часто придется созда­
вать иерархии специальных обобщенных классов. Тем не менее, это вполне возможно
(до тех пор, пока вы придерживаетесь правил).
Ограничение параметров типа
Как показано в этой главе, любой обобщенный элемент имеет, по крайней мере, один
параметр типа, который должен быть указан при взаимодействии с обобщенным ти­
пом или членом. Одно это позволит строить безопасный в отношении типов код; одна­
ко платформа .NET позволяет использовать ключевое слово where для*указания особых
требований к определенному параметру типа.
С помощью ключевого слова t h i s можно добавлять набор ограничений к конкрет­
ному параметру типа, которые компилятор C# проверит во время компиляции. В част­
ности, параметр типа можно ограничить, как описано в табл. 10.6.
Глава 10. Обобщения
383
Таблица 10.6. Возможные ограничения параметров типа для обобщений
Ограничение обобщения
Назначение
where Т : struct
Параметр типа <Т> должен иметь в своей цепочке насле­
дования System.ValueType. Другими словами, <Т>
должен быть структурой
where Т : class
Параметр типа <Т> должен не иметь System.ValueType
в своей цепочке наследования (т.е. <т>
должен быть ссылочным типом)
where Т : new()
Параметр типа <т> должен иметь конструктор по умол­
чанию. Это очень полезно, если обобщенный тип должен
создавать экземпляры параметра типа, поскольку не уда­
ется предположить формат специальных конструкторов.
Обратите внимание, что в типе со многими ограничения­
ми это ограничение должно указываться последним
where T : И м я Б а зо в о го К л а с са
Параметр типа <т> должен быть наследником класса,
указанного в И м я Б а з о в о го К л а с с а
where Т : ИмяИнтерфейса
Параметр типа <т> должен реализовать интерфейс, ука­
занный в ИмяИнтерфейса. Можно задавать несколько
интерфейсов, разделяя их запятыми
Если только не требуется строить какие-то исключительно безопасные к типам
специальные коллекции, возможно, никогда не придется использовать ключевое слово
where в проектах С#. Так или иначе, но в следующих нескольких примерах (частичного)
кода демонстрируется работа с ключевым словом where.
Примеры использования ключевого слова w h e r e
Будем исходить из того, что создан специальный обобщенный класс, и необходимо
гарантировать наличие в параметре типа конструктора по умолчанию. Это может быть
полезно, когда специальный обобщенный класс должен создавать экземпляры Т, потому
что конструктор по умолчанию — это единственный конструктор, потенциально общий
для всех типов. Также подобного рода ограничение Т позволит производить проверку во
время компиляции; если Т — ссылочный тип, то компилятор напомнит программисту о
необходимости переопределения конструктора по умолчанию в объявлении класса (если
помните, конструкторы по умолчанию удаляются из классов, в которых определяются
собственные конструкторы).
// Класс MyGenencClass унаследован от object, примем содержащиеся
/ / в нем элементы должны иметь конструктор по умолчанию,
public class MyGenencClass<T> where T : new()
{
}
Обратите внимание, что конструкция where указывает параметр типа, на который
накладывается ограничение, а за ним следует операция двоеточия. После этой опера­
ции перечисляются все возможные ограничения (в данном случае — конструктор по
умолчанию). Ниже показан еще один пример:
// MyGenencClass унаследован от Object, причем содержащиеся
// в нем элементы должны относиться к классу, реализующему IDrawable.
/ / и поддерживать конструктор по умолчанию.
public class MyGenencClass<T> where T : class, IDrawable, new ()
384
Часть III. Дополнительные конструкции программирования на C#
В данном случае к Т предъявляются три требования. Во-первых, это должен быть
ссылочный тип (не структура), что помечено лексемой c la s s . Во-вторых, Т должен реа­
лизовывать интерфейс ID rawable. В-третьих, он также должен иметь конструктор по
умолчанию. Множество ограничений перечисляются в списке, разделенном запятыми;
однако имейте в виду, что ограничение new() всегда должно идти последним! По этой
причине следующий код не скомпилируется:
// Ошибка! Ограничение new() должно быть последним в списке!
public class MyGenericClass<T> where T : new(), class, IDrawable
{
}
В случае создания обобщенного класса коллекции с несколькими параметрами типа,
можно указывать уникальный набор ограничений для каждого параметра с помощью
отдельной конструкции where:
// <К> должен расширять SomeBaseClass и иметь конструктор по умолчанию, в то время
// как <Т> должен быть структурой и реализовывать обобщенный интерфейс IComparable.
public class MyGenericClasscK, T> where К : SomeBaseClass, new()
where T : IComparable<T>
}
Необходимость построения полностью нового обобщенного класса коллекции возни­
кает редко; однако ключевое слово where также допускается применять и в обобщенных
методах. Например, если необходимо гарантировать, чтобы метод Swap<T>() работал
только со структурами, обновите код следующим образом:
// Этот метод обменяет местами любые структуры, но не классы,
static void Swap<T>(ref Т a, ref Т b) where Т : struct
}
Обратите внимание, что если ограничить метод Swap О подобным образом, обмени­
вать местами объекты s t r i n g (как это делалось в коде примера) уже не получится, по­
скольку s t r in g является ссылочным типом.
Недостаток ограничений операций
В конце этой главы следует упомянуть об одном моменте относительно обобщенных
методов и ограничений. При создании обобщенных методов может оказаться сюрпри­
зом появление ошибок компиляции во время применения любых операций C# (+, -, *,
== и т.д.) к параметрам типа. Например, подумайте о пользе от класса, который может
выполнять Add(), S u b stra ctO , M u ltip ly () и D evideO над обобщенными типами:
// Ошибка на этапе компиляции' Нельзя применять
// арифметические операции к параметрам типа!
public class Bas±cMath<T>
{
public T
{ return
public T
{ return
public T
{ return
public T
{ return
Add(T argl, T arg2)
argl + arg2; }
Subtract (T argl, T arg2)
argl - arg2; }
Multiply(T argl, T arg2)
argl * arg2; }
Divide(T argl, T arg2)
argl / arg2; }
Глава 10. Обобщения
385
К сожалению, приведенный выше класс BasicMath<T> не скомпилируется. Хотя это
может показаться серьезным недостатком, следует снова вспомнить, что обобщения яв­
ляются общими. Естественно, числовые данные работают достаточно хорошо с бинар­
ными операциями С#. Однако если <Т> будет специальным классом или структурой, то
компилятор мог бы предположить, что этот класс поддерживает операции +, -, * и /.
В идеале язык C# должен был бы позволять ограничивать обобщенные типы поддержи­
ваемыми операциями, например:
//
Код т о л ь к о д л я и л л ю с т р а ц и и !
public class BasicMath<T> where T : operator + , operator
operator *, operator /
{
public T
{ return
public T
{ return
public T
{ return
public T
{ return
Add(T argl, T arg2)
argl + arg2; }
Subtract (T argl, T arg2)
argl - arg2; }
Multiply (T argl, T arg2)
argl * arg2; }
Divide (T argl, T arg2)
argl / arg2; }
}
К сожалению, ограничения операций в текущей версии C# не поддерживаются.
Однако можно (хотя это и потребует дополнительной работы) достичь желаемого эф­
фекта, определив интерфейс, поддерживающий эти операции (интерфейсы C# могут оп­
ределять операции) и затем указать ограничение интерфейса для обобщенного класса.
На этом первоначальный обзор построения специальных обобщенных типов завершен.
В следующей главе мы вновь обратимся к теме обобщений, когда будем рассматривать
тип делегата .NET.
Резюме
Настоящая глава начиналась с рассмотрения использования классических контей­
неров, включенных в пространство имен S y s t e m .C o lle c t io n s . Хотя эти типы будут
и далее поддерживаться для обратной совместимости, новые приложения .NET выиг­
рают от применения вместо них новых обобщенных аналогов из пространства имен
S y s te m .C o lle c tio n s .G e n e r ic .
Как вы видели, обобщенный элемент позволяет специфицировать заполнители
(параметры типа), которые указываются во время создания (или вызова — в случае
обобщенных методов). По сути, обобщения предлагают решение проблем упаковки и
обеспечения безопасности типов, которые досаждали при разработке программного
обеспечения для .NET 1.1. Вдобавок обобщенные типы в значительной мере исключают
необходимость в построении специальных типов коллекций.
Хотя чаще всего обобщенные типы, представленные в библиотеках базовых классов
.NET, просто будут использоваться, можно также создавать и собственные обобщенные
типы (и обобщенные методы). При этом есть возможность задавать любое количество
ограничений (с помощью ключевого слова where), чтобы повышать уровень безопасно­
сти в отношении типов и гарантировать выполнение операций над типами в “известном
количестве”; это обеспечивает предоставление определенных базовых возможностей.
ГЛАВА 1 1
Делегаты, события
и лямбда-выражения
плоть до этого момента большинство разработанных приложений добавляли
различные порции кода к методу M a in (), тем или иным способом посылающие
запросы заданному объекту. Однако многие приложения требуют, чтобы объект мог об
ращаться обратно к сущности, которая создала его — посредством механизма обратно­
го вызова. Хотя механизмы обратного вызова могут применяться в любом приложении,
они особенно важны в графических интерфейсах пользователя, где элементы управле­
ния (такие как кнопки) нуждаются в вызове внешних методов при надлежащих условиях
(выполнен щелчок на кнопке, курсор мыши находится на поверхности кнопки и т.п.).
На платформе .NET тип делегата является предпочтительным средством определе­
ния и реагирования на обратные вызовы в приложении. По сути, тип делегата .NET —
это безопасный к типам объект, который “указывает” на метод или список методов,
которые могут быть вызваны позднее. Однако в отличие от традиционного указателя
на функцию C++, делегаты .NET представляют собой классы, обладающие встроенной
поддержкой группового выполнения и асинхронного вызова методов.
В этой главе будет показано, как создавать и управлять типами делегатов, а также
использовать ключевое слово e v e n t в С#, которое облегчает работу с типами делегатов.
По пути вы также изучите несколько языковых средств С#, ориентированных на делега­
ты и события, включая анонимные методы и групповые преобразования методов.
Завершается глава исследованием лямбда-выраженияй (lambda expressions). Исполь­
зуя лямбда-операцию C# (=>), теперь можно указывать блок операторов кода (и парамет­
ры для передачи этим операторам) везде, где требуется строго типизированный делегат.
Как будет показано, лямбда-выражение — это немногим более чем маскировка аноним­
ного метода, и представляет собой упрощенный подход к работе с делегатами.
В
Понятие типа делегата .NET
Прежде чем приступить к формальному определению делегатов .NET, давайте огля­
немся немного назад. В интерфейсе Windows API часто использовались указатели на
функции в стиле С для создания сущностей, именуемых функциями обратного вызова
(callback functions) или просто обратными вызовами (callbacks). С помощью обратных
вызовов программисты могли конфигурировать одну функцию таким образом, чтобы
она осуществляла обратный вызов другой функции в приложении. Применяя такой
подход, разработчики Windows смогли обрабатывать щелчки на кнопках, перемещения
курсора мыши, выбор пунктов меню и общие двусторонние коммуникации между про­
граммными сущностями в памяти.
Глава 11. Делегаты, события и лямбда-выражения
387
Проблема со стандартными функциями обратного вызова в стиле С заключается в
том, что они представляют собой лишь немногим более чем простой адрес в памяти.
В идеале обратные вызовы могли бы конфигурироваться для включения дополнитель­
ной безопасной к типам информации, такой как количество и типы параметров и воз­
вращаемого значения (если оно есть) метода, на который они указывают. К сожалению,
это невозможно с традиционными функциями обратного вызова и это, как следовало
ожидать, является постоянным источником ошибок, аварийных завершений и прочих
неприятностей во время выполнения. Тем не менее, обратные вызовы — удобная вещь.
В .NET Framework обратные вызовы по-прежнему возможны, и их функциональ­
ность обеспечивается в гораздо более безопасной и объектно-ориентированной манере
с использованием делегатов. По сути, делегат — это безопасный в отношении типов
объект, указывающий на другой метод (или, возможно, список методов) приложения,
который может быть вызван позднее. В частности, объект делегата поддерживает три
важных фрагмента информации:
• адрес метода, на котором он вызывается;
• аргументы (если есть) этого метода;
• возвращаемое значение (если есть) этого метода.
На заметку! Делегаты .NET могут указывать как на статические, так и на методы экземпляра.
Как только делегат создан и снабжен необходимой информацией, он может динами­
чески вызывать методы, на которые указывает, во время выполнения. Каждый делегат
в .NET Framework (включая специальные делегаты) автоматически снабжается способ­
ностью вызывать свои методы синхронно или асинхронно. Этот факт значительно уп­
рощает задачи программирования, поскольку позволяет вызывать метод во вторичном
потоке выполнения без рунного создания и управления объектом Thread.
На заметку! Асинхронное поведение типов делегатов будет рассматриваться во время исследо­
вания пространства имен System.Threading в главе 19.
Определение типа делегата в C#
Для определения делегата в C# используется ключевое слово delegate. Имя делега­
та может быть любым. Однако сигнатура определяемого делегата должна соответство­
вать сигнатуре метода, на который он будет указывать. Например, предположим, что
планируется построить тип делегата по имени BinaryOp, который может указывать на
любой метод, возвращающий целое число и принимающий два целых числа в качестве
входных параметров:
/ / Э т о т д е л е г а т м ож ет у к а з ы в а т ь н а лю бой м е т о д , котор ы й
/ / приним ает д в а целы х и в о зв р а щ а е т ц е л о е з н а ч е н и е .
public delegate int BinaryOp(int x, int y) ;
Когда компилятор C# обрабатывает тип делегата, он автоматически генерирует за­
печатанный (sealed) класс, унаследованный от System.MulticastDelegate. Этот класс
(в сочетании с его базовым классом System.Delegate) предоставляет необходимую ин­
фраструктуру для делегата, чтобы хранить список методов, подлежащих вызову в бо­
лее позднее время. Например, если просмотреть делегат BinaryOp с помощью утилиты
ildasm.exe, обнаружится класс, показанный на рис. 11.1.
388
Часть III. Дополнительные конструкции программирования на C#
l> Н \Му Books\C* BookVC# ana ih* NET fUetfornn 5th e<f\First C-'3#t\Chapter_11 Code,: impieD< .
File View
Help
V - H:\My Books\C# Book\C# and the NET Platform 5th ed\First Draft\Chapter_l l\Code\SimpleDelegate\ob]\x86\Debug\Smple
► MANIFEST
6 Щ SimpleDelegate
пВЕИЗЗЕВИ
«
► .class public auto ansi sealed
► extends [mscorllb]5ystem.MulticastDelegate
■ .cto r: void(object, native mt)
■ Begnlnvoke : class [mscorlib]5ystem.IAsyncResult(int 32, г^32, class [mscorlib]5ystem. AsyncCallback object)
■ Endlnvoke : int32(dass [mscorlib]5ystem.IAsyncResult)
■ Invoke : mt32(int32,mt32)
SlmpleDelegate.Program
4
.assembly Sir.ipleDelegate
{
Рис. 11.1. Ключевое слово delegate представляет запечатанный класс,
унаследованный от System.MulticastDelegate
Как видите, сгенерированный компилятором класс BinaryOp определяет три обще­
доступных метода. lnvoke() — возможно, главный из них, поскольку он используется
для синхронного вызова каждого из методов, поддерживаемых объектом делегата; это
означает, что вызывающий код должен ожидать завершения вызова, прежде чем про­
должить свою работу. Может показаться странным, что синхронный метод Invoke () не
должен вызываться явно в коде С#. Как вскоре будет показано. Invoke () вызывается
“за кулисами”, когда применяется соответствующий синтаксис С#.
Методы Begin Invoke () и Endlnvoke () предлагают возможность вызова текущего
метода асинхронным образом, в отдельном потоке выполнения. Имеющим опыт в мно­
гопоточной разработке должно быть известно, что одной из основных причин, вынуж­
дающих разработчиков создавать вторичные потоки выполнения, является необходи­
мость вызова методов, которые требуют определенного времени для завершения. Хотя в
библиотеках базовых классов .NET предусмотрено целое пространство имен, посвящен­
ное многопоточному программированию (System.Threading), делегаты предлагают эту
функциональность в готовом виде.
Каким же образом компилятор знает, как следует определить методы Invoke(),
BeginlnvokeO и Endlnvoke ()? Чтобы разобраться в процессе, ниже приведен код сге­
нерированного компилятором типа класса BinaryOp [полужирным курсивом выделены
элементы, указанные определенным типом делегата):
sealed class
B in a r y O p
:
System.MulticastDelegate
{
public i n t Invoke ( i n t x, i n t y) ;
public IAsyncResult Begmlnvoke (in t x, in t y,
AsyncCallback cb, object state);
public int Endlnvoke(IAsyncResult result);
}
Первым делом, обратите внимание, что параметры и возвращаемый тип для метода
Invoke () в точности соответствуют определению делегата BinaryOp. Начальные пара­
метры для членов BeginlnvokeO (в данном случае — два целых) также основаны на
делегате BinaryOp; однако BeginlnvokeO всегда будет предоставлять два финальных
параметра (типа AsyncCallback и object), используемых для облегчения асинхронно­
го вызова методов. И, наконец, возвращаемый тип Endlnvoke () идентичен исходному
объявлению делегата и всегда принимает единственный параметр — объект, реализую­
щий интерфейс IAsyncResult.
Глава 11. Делегаты, события и лямбда-выражения
389
Давайте рассмотрим другой пример. Предположим, что определен тип делегата, ко­
торый может указывать на любой метод, возвращающий string и принимающий три
входных параметра System.Boolean:
public delegate string MyDelegate(bool a, bool b, bool c);
На этот раз сгенерированный компилятором класс будет выглядеть так:
sealed class M y D e l e g a t e : System.MulticastDelegate
{
public s t r i n g Invoke (bool a, b o o l b, b o o l c) ;
public IAsyncResult Begin Invoke {bool a, b o o l b, b o o l c,
AsyncCallback cb, object state);
public s t r i n g Endlnvoke(IAsyncResult result);
Делегаты также могут “указывать” на методы, содержащие любое количество пара­
метров out и ref (как и массивов параметров, помеченных ключевым словом params).
Например, предположим, что имеется следующий тип делегата:
public delegate string MyOtherDelegate (out bool a, r e f bool b, int c) ;
Сигнатуры методов InvokeO и Begin Invoke () выглядят так, как и следовало ожи­
дать; однако взгляните на метод Endlnvoke (), который теперь включает набор аргумен­
тов out/ref, определенных типом делегата:
sealed class M y O t h e r D e l e g a t e : System.MulticastDelegate
{
public s t r i n g Invoke (out b o o l a, r e f b o o l b, i n t c) ;
public IAsyncResult Begin Invoke (out b o o l a, r e f b o o l b, i nt c,
AsyncCallback cb, object state);
public s t r i n g Endlnvoke (out b o o l a, r e f b o o l b, IAsyncResult result) ;
}
Чтобы подытожить: определение типа делегата C# порождает запечатанный класс с
тремя сгенерированными компилятором методами, типы параметров и возвращаемых
значений которых основаны на объявлении делегата. Базовый шаблон может быть опи­
сан с помощью следующего псевдокода:
// Это только псевдокод!
public sealed class ИмяДелегата : System.MulticastDelegate
{
public возвращаемоеЗначениеДелегата Invoke {всеВходныеЯе£иОи1ПараметрыДелегата) ;
public IAsyncResult Beginlnvoke(в c e B x o д н ы e R e f и O u t П a p a м e т p ы Д e л e г a т a r
AsyncCallback cb, object state);
public возвращаемоеЗначениеДелегата Endlnvoke(всеВходныеЯе£иОи1ПараметрыДелегата,
IAsyncResult result);
}
Базовые классы System.MulticastDelegate
и System.Delegate
При построении типа, использующего ключевое слово delegate, неявно объявля­
ется тип класса, унаследованного от System.MulticastDelegate. Этот класс обеспе­
чивает своих наследников доступом к списку, который содержит адреса методов, под­
держиваемых типом делегата, а также несколько дополнительных методов (и несколько
перегруженных операций), чтобы взаимодействовать со списком вызовов.
Ниже показаны некоторые избранные методы System.MulticastDelegate:
390
Часть III. Дополнительные конструкции программирования на C#
public abstract class MulticastDelegate : Delegate
{
// Возвращает список методов, на которые "указывает" делегат.
public sealed override Delegate[] GetlnvocationList () ;
// Перегруженные операции.
public static bool operator = = (MulticastDelegate dl, MulticastDelegate d2) ;
public static bool operator != (MulticastDelegate dl, MulticastDelegate d2) ;
// Используются внутренне для управления списком методов, поддерживаемых делегатом.
private IntPtr _invocationCount;
private object _invocationList;
Класс System.MulticastDelegate получает дополнительную функциональность от
своего родительского класса System.Delegate. Ниже показан фрагмент определения
класса:
public abstract class Delegate : ICloneable, ISerializable
{
// Методы для взаимодействия со списком функций.
public static Delegate Combine(params Delegate[] delegates);
public static Delegate Combine(Delegate a, Delegate b) ;
public static Delegate Remove(Delegate source, Delegate value);
public static Delegate RemoveAll(Delegate source, Delegate value);
// Перегруженные операции.
public static bool operator ==(Delegate dl, Delegate d2) ;
public static bool operator !=( Delegate dl, Delegate d2);
// Свойства, показывающие цель делегата.
public Methodlnfo Method { get; }
public object Target { get; }
}
Запомните, что вы никогда не сможете напрямую наследовать от этих базовых клас­
сов в коде (при попытке сделать это выдается ошибка компиляции). Тем не менее, при
использовании ключевого слова delegate неявно создается класс, который “являет­
ся” MulticastDelegate. В табл. 11.1 описаны основные члены, общие для всех типов
делегатов.
Таблица 11.1. Основные члены S y s te m .M u ltc a s tD e le g a te / S y s te m .D e le g a te
Член
Назначение
Method
Это свойство возвращает объект System.Ref lection.Method,
который представляет детали статического метода, поддерживаемо­
го делегатом
Target
Если метод, подлежащий вызову, определен на уровне объекта (т.е.
не является статическим), то Target возвращает объект, представ­
ляющий метод, поддерживаемый делегатом. Если возвращенное
Target значение равно null, значит, подлежащий вызову метод
является статическим
Combine ()
Этот статический метод добавляет метод в список, поддерживаемый
делегатом. В C# этот метод вызывается за счет использования пере­
груженной операции += в качестве сокращенной нотации
GetlnvokationListO
Этот метод возвращает массив типов System.Delegate, каждый из
которых представляет определенный метод, доступный для вызова
Remove ()
RemoveAll ()
Эти статические методы удаляют метод (или все методы) из списка
вызовов делегата. В C# метод Remove () может быть вызван неявно,
посредством перегруженной операции -=
Глава 11. Делегаты, события и лямбда-выражения
391
Простейший пример делегата
При первоначальном знакомстве делегаты могут показаться очень слож ны ­
ми. Рассмотрим- для начала очень простое консольное приложение под названием
SimpleDelegate, в котором используется ранее показанный тип делегата BinaryOp.
Ниже приведен полный код.
namespace SimpleDelegate
{
// Этот делегат может указывать на любой метод,
// принимающий два целых и возвращающий целое.
public delegate int BinaryOp(int x, int y) ;
// Этот класс содержит методы, на которые будет указывать BinaryOp.
public class SimpleMath
{
public static int Add(int x, int y)
{ return x + y; }
public static int Subtract(int x, int y)
{ return x - y; }
}
class Program
{
static void Main (string[] args)
{
Console .WnteLine ("***** Simple Delegate Example *****\n");
// Создать объект делегата BinaryOp, "указывающий" на SimpleMath.Add().
BinaryOp b = new BinaryOp(SimpleMath.Add);
// Вызвать метод Add() непрямо с использованием объекта делегата.
Console.WnteLine (" 10 + 10 is {0}", b(10, 10));
Console.ReadLine();
Обратите внимание на формат объявления типа делегата BinaryOp: он определяет,
что объекты делегата BinaryOp могут указывать на любой метод, принимающий два
целых и возвращающий целое (действительное имя метода, на который он указывает,
не существенно). Здесь создан класс по имени SimpleMath, определяющий два статиче­
ских метода, которые соответствуют шаблону, определенному делегатом BinaryOp.
Когда нужно вставить целевой метод в заданный объект делегата, просто передайте
имя этого метода конструктору делегата:
// Создать делегат BinaryOp, который "указывает" на SimpleMath.Add () .
BinaryOp b = new BinaryOp(SimpleMath.Add);
С этого момента указанный метод можно вызывать с использованием синтаксиса,
который выглядит как прямой вызов функции:
// Invoke() действительно вызывается здесь!
Console .WnteLine ("10 + 10 is {0}", b(10, 10));
“За кулисами” исполняющая система на самом деле вызывает сгенерированный ком­
пилятором метод Invoke() на производном классе MulticastDelegate. В этом можно
убедиться, открыв сборку в утилите ildasm.exe и просмотрев код CIL в методе Main():
.method private hidebysig static void Main(string [] args) cil managed
{
callvirt instance int32 SimpleDelegate.BinaryOp: : Invoke(int32, int32)
392
Часть III. Дополнительные конструкции программирования на C#
Хотя C# и не требует явного вызова Invoke () в коде, это вполне можно сделать. То
есть следующий оператор кода является допустимым:
Console.WnteLine ("10 + 10 is {0}", b.Invoke(10, 10));
Вспомните, что делегаты .NET безопасны в отношении типов. Поэтому, попытка
передать делегату метод, не соответствующий шаблону, приводит к ошибке времени
компиляции. Чтобы проиллюстрировать это, предположим, что в классе SimpleMath
теперь определен дополнительный метод по имени SquareNumber (), принимающий
единственный целочисленный аргумент:
public class SimpleMath
public static int SquareNumber(int a)
{ return a * a; }
Учитывая, что делегат Binary Op может указывать только на методы, принимаю­
щие два целых и возвращающие целое, следующий код неверен и компилироваться не
будет:
// Ошибка компиляции! Метод не соответствует шаблону делегата1
BinaryOp Ь2 = new BinaryOp(SimpleMath.SquareNumber);
Исследование объекта делегата
Давайте услож ним текущ ий пример, создав статический метод (по имени
DisplayDelegatelnfoO) в классе Program. Этот метод будет выводить на консоль име­
на методов, поддерживаемых объектом делегата, а также имя класса, определяющего
метод. Для этого будет реализована итерация по массиву System.Delegate, возвра­
щенному GetlnvocationList(), с обращением к свойствам Target и Method каждого
элемента.
static void DisplayDelegatelnfo (Delegate delOb])
{
// Вывести на консоль имена каждого члена списка вызовов делегата.
foreach (Delegate d in delOb].GetlnvocationList ())
{
Console .WnteLine ("Method Name: {0}"f d.Method) ; // имя метода
Console .WnteLine ("Type Name: {0}"f d.Target); // имя типа
}
Исходя из предположения, что в метод Main() добавлен вызов этого вспомогательно­
го метода, вывод приложения будет выглядеть следующим образом:
***** simple Delegate Example
**** *
Method Name: Int32 Add(Int32, Int32)
Type Name:
10 + 10 is 20
Обратите внимание, что имя типа (SimpleMath) в настоящий момент не отображает­
ся при обращении к свойству Target. Причина в том, что делегат BinaryOp указывает
на статический метод, и потому просто нет объекта, на который нужно ссылаться!
Однако если сделать методы Add() и SubstractO нестатическими (удалив в их объяв­
лениях ключевые слова static), можно будет создавать экземпляр типа SimpleMath и
указывать методы для вызова с использованием ссылки на объект:
Глава 11. Делегаты, события и лямбда-выражения
393
static void Main(string [] args)
{
Console.WriteLine("***** Simple Delegate Example *****\n");
// Делегаты .NET могут указывать на методы экземпляра.
SimpleMath.m = new SimpleMath ();
BinaryOp b = new BinaryOp(m.Add);
// Вывести сведения об объекте.
DisplayDelegatelnfо (b);
Console.WriteLine("10 + 10 is {0}"f b(10, 10));
Console.ReadLine();
}
В этом случае вывод будет выглядеть, как показано ниже:
***** Simple Delegate Example *****
Method Name: Int32 Add(Int32, Int32)
Type Name: SimpleDelegate.SimpleMath
10 + 10 is 20
Исходный код. Проект S im p leD e leg a te доступен в подкаталоге C hapter 11.
Отправка уведомлений о состоянии
объекта с использованием делегатов
Ясно, что предыдущий пример S im p le D e le g a te был предназначен только для це­
лей иллюстрации, потому что нет особого смысла создавать делегат только для того,
чтобы просуммировать два числа. Рассмотрим более реалистичный пример, в котором
делегаты используются для определения класса Саг, обладающего способностью инфор­
мировать внешние сущности о текущем состоянии двигателя. Для этого понадобится
предпринять перечисленные ниже действия.
• Определение нового типа делегата, который будет отправлять уведомления вызы­
вающему коду.
• Объявление переменной-члена этого делегата в классе Саг.
• Создание вспомогательной функции в Саг, которая позволяет вызывающему коду
указывать метод для обратного вызова.
• Реализация метода A c c e le r a t e () для обращения к списку вызовов делегата при
нужных условиях.
Для начала создадим проект консольного приложения по имени C arD elegate. Затем
определим новый класс Саг, который изначально выглядит следующим образом:
public class Car
{
// Данные состояния.
public int CurrentSpeed { get; set; }
public int MaxSpeed { get; set; }
public string PetName { get; set; }
// Исправен ли автомобиль?
private bool carlsDead;
// Конструкторы класса.
public Car() { MaxSpeed = 100; }
public Car (string name, int maxSp, int currSp)
1
CurrentSpeed = currSp;
394
Часть III. Дополнительные конструкции программирования на C#
MaxSpeed = maxSp;
PetName = name;
}
Ниже показаны обновления, связанные с реализацией трех первых пунктов:
public class Car
{
// 1. Определить тип делегата.
public delegate void CarEngineHandler(string msgForCaller);
/ / 2 . Определить переменную-член типа этого делегата.
private CarEngineHandler listOfHandlers;
// 3. Добавить регистрационную функцию для вызывающего кода.
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers = methodToCall;
Обратите внимание, что в этом примере типы делегатов определяются непосредст­
венно в контексте класса Саг. Исследование библиотек базовых классов покажет, что
это довольно обычно — определять делегат внутри класса, который будет с ними ра­
ботать. Наш тип делегата — CarEngineHandler — может указывать на любой метод,
принимающий значение string в качестве параметра и имеющий void в качестве типа
возврата.
,
Кроме того, была объявлена приватная переменная-член делегата (по имени
listOfHandlers) и вспомогательная функция (по имени RegisterWithCarEngine()),
которая позволяет вызывающему коду добавлять метод к списку вызовов делегата.
На заметку! Строго говоря, можно было бы определить переменную-член делегата как public, из­
бежав необходимости в добавлении дополнительных методов регистрации. Однако за счет опре­
деления этой переменной-члена делегата как private усиливается инкапсуляция и обеспечива­
ется более безопасное к типам решение. Опасности объявления переменных-членов делегатов как
public еще будут рассматриваться в этой главе, когда речь пойдет о ключевом слове event.
Теперь необходимо создать метод Accelerate(). Вспомните, что здесь стоит задача
позволить объекту Саг отправлять касающиеся двигателя сообщения любому подписав­
шемуся слушателю. Ниже показаны необходимые изменения в коде.
public void Accelerate(int delta)
{
// Если этот автомобиль сломан, отправить сообщение об этом,
if (carlsDead)
{
if (listOfHandlers '= null)
listOfHandlers("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Автомобиль почти сломан?
if (10 == (MaxSpeed - CurrentSpeed) && listOfHandlers != null)
{
listOfHandlers("Careful buddy! Gonna blow!");
Глава 11. Делегаты, события и лямбда-выражения
395
if (CurrentSpeed >= MaxSpeed)
carlsDead = true;
else
Console.WnteLine ("CurrentSpeed = {0}", CurrentSpeed);
}
}
Обратите внимание, что прежде чем вызывать методы, поддерживаемые переменнойчленом listOfHandlers, она проверяется на равенство null. Причина в том, что разме­
щать эти объекты вызовом вспомогательного метода RegisterWithCarEngine() — зада­
ча вызывающего кода. Если вызывающий код не вызовет этот метод, а мы попытаемся
обратиться к списку вызовов делегата, то получим исключение NullReferenceException
и нарушим работу исполняющей системы (что очевидно нехорошо). Теперь, имея всю
инфраструктуру делегатов, рассмотрим обновления класса Program:
class Program
{
static void Main(string [] args)
{
Console .WnteLine (••***** Delegates as event enablers *****\n ");
// Сначала создадим объект Car.
Car cl = new Car ("SlugBug", 100, 10);
// Теперь сообщим ему, какой метод вызывать,
// когда он захочет отправить сообщение.
cl.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
// Ускорим (это инициирует события).
Console .WnteLine ("***** Speeding up *****");
for (int l = 0; l < 6; i++)
cl.Accelerate(20);
Console.ReadLine() ;
}
// Это цель для входящих сообщений.
public static void OnCarEngineEvent(string msg)
{
Console .WnteLine ("\n***** Message From Car Object *****");
Console .WnteLine ("=> {0}", msg);
Console WnteLine ("***********************************\n") ;
Метод Main() начинается с создания нового объекта Car. Поскольку мы заинтере­
сованы в событиях, связанных с двигателем, следующий шаг заключается в вызове
специальной регистрационной функции RegisterWithCarEngine (). Вспомните, что
этот метод ожидает получения экземпляра вложенного делегата CarEngineHandler, и
как с любым делегатом, метод, на который он должен указывать, задается в параметре
конструктора.
Трюк в этом примере состоит в том, что интересующий метод находится в классе
Program! Обратите внимание, что метод OnCarEngineEvent () полностью соответствует
связанному делегату в том, что принимает string и возвращает void. Ниже показан
вывод этого примера:
***** Delegates as event enablers *****
***** Speeding
CurrentSpeed =
CurrentSpeed =
CurrentSpeed =
up *****
30
50
70
396
Часть III. Дополнительные конструкции программирования на C#
***** Message From Car Object *****
=> Careful buddy! Gonna blow!
***********************************
CurrentSpeed = 90
***** Message From Car Object *****
=> Sorry, this car is dead...
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
Включение группового вызова
Вспомните, что делегаты .NET обладают встроенной возможностью группового вы­
зова. Другими словами, объект делегата может поддерживать целый список методов
для вызова, а не просто единственный метод. Для добавления нескольких методов
к объекту делегата используется перегруженная операция +=, а не прямое присваи­
вание. Чтобы включить групповой вызов в типе Саг, можно модифицировать метод
R egisterW ith C a rE n gin eO следующим образом:
public class Car
{
// Добавление поддержки группового вызова.
// Обратите внимание на использование операции +=,
// а не операции присваивания (=) .
public void RegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers += methodToCall;
}
После этого простого изменения вызывающий код теперь может регистрировать
множественные цели для одного и того же обратного вызова. Здесь второй обработчик
выводит входное сообщение в верхнем регистре, просто для примера:
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Delegates as event enablers *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Регистрируем несколько обработчиков событий.
cl.RegisterWithCarEngine (new Car.CarEngineHandler(OnCarEngineEvent));
cl .RegisterWithCarEngine(new Car.CarEngineHandler (OnCarEngineEvent2));
// Ускорим (это инициирует события).
Console.WriteLine ("***** Speeding up *****");
for (int i = 0 ; i < 6 ; i++)
cl.Accelerate(2 0 );
Console.ReadLine();
// Теперь есть ДВА метода, которые будут вызваны Саг
// при отправке уведомлений.
public static void OnCarEngineEvent (string msg)
{
Console.WriteLine ("\n***** Message From Car Object *****");
Console.WriteLine ("=> {0}", msg);
Console ^£j_^0 Lj_j^0 ^,,*^*^*^*^*^*^*'*'*'*'*'*''**'*'*'*'*'*'*'*'*'*'*'*'*'*'*'*'\j"i,,j •
}
Глава 11. Делегаты, события и лямбда-выражения
397
public static void OnCarEngineEvent2(string msg)
{
Console.WriteLine("=> {0}"f msg.ToUpper());
}
В терминах кода CIL операция += разрешает вызывать статический метод Delegate.
Combine () (фактически, его можно вызывать и напрямую, но операция += предлагает бо­
лее простую альтернативу). Ниже показан CIL-код метода RegisterWithCarEngine().
.method public hidebysig instance void RegisterWithCarEngine()
(class CarDelegate.Car/AboutToBlow clientMethod) cil managed
{
// Code size 25 (0x19)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.O
IL_0002: dup
IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.
Car::listOfHandlers
IL_0008: ldarg.l
1 L 0 0 0 9 :c a ll cla ss [m scorlib]System .Delegate [m scorlib]
System.Delegate: : Combine(class [m scorlib]System .Delegate,
class [mscorlib]System .Delegate)
IL_000e: castclass CarDelegate.Car/CarEngineHandler
IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.
Car::listOfHandlers
IL_0018: ret
}// end of method Car::RegisterWithCarEngine
Удаление целей из списка вызовов делегата
В классе Delegate также определен метод Remove (), позволяющий вызывающему
коду динамически удалять отдельные члены из списка вызовов объекта делегата. Это
позволяет вызывающему коду легко “отписываться” от определенного уведомления во
время выполнения. Хотя в коде можно непосредственно вызывать Delegate .Remove (),
разработчики C# могут использовать также перегруженную операцию -= в качестве
сокращения.
Давайте добавим в класс Саг новый метод, который позволяет вызывающему коду
исключать метод из списка вызовов:
public class Car
public void UnRegisterWithCarEngine(CarEngineHandler methodToCall)
{
listOfHandlers -= methodToCall;
}
Опять-таки, синтаксис -= представляет собой просто сокращенную нотацию для
рунного вызова метода Delegate .Remove (), что иллюстрирует следующий код CLI для
метода UnRegisterWithCarEvent () класса Саг:
.method public hidebysig instance void UnRegisterWithCarEvent(class
CarDelegate.Car/AboutToBlow clientMethod) cil managed
{
// Code size 25 (0x19)
.maxstack 8
IL_0000: nop
398
Часть III. Дополнительные конструкции программирования на C#
IL_0001: ldarg.O
IL_0002: dap
IL_0003: ldfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers
IL_0008: ldarg.l
IL_0009: call class [mscorlib]System.Delegate [mscorlib]
System.Delegate::Remove(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_000e: castclass CarDelegate.Car/CarEngineHandler
IL_0013: stfld class CarDelegate.Car/CarEngineHandler CarDelegate.Car::listOfHandlers
IL_0018 : ret
} // end of method Car::UnRegisterWithCarEngine
В текущей версии класса Саг прекратить получение уведомлений от второго обра­
ботчика можно за счет изменения метода Main() следующим образом:
static void Main (string [] args)
{
Console .WnteLine ("***** Delegates as event enablers *****\n");
// Сначала создадим объект Car.
Car cl = new Ca r ("SlugBug", 100, 10);
cl .RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
// Сохраним объект делегата для последупцей отмены регистрации.
Car.CarEngineHandler handler2 = new Car.CarEngineHandler(OnCarEngineEvent2);
cl .RegisterWithCarEngine(handler2);
// Ускорим (это инициирует события).
Console .WnteLine ("***** Speeding up *****");
for (int l = 0; l < 6; i++)
cl.Accelerate(20);
// Отменим регистрацию второго обработчика,
cl .UnRegisterWithCarEngine(handler2) ;
// Сообщения в верхнем регистре больше не выводятся.
Console .WnteLine ("***** Speeding up *****");
for (int l = 0; l < 6; i++)
cl.Accelerate(20);
Console.ReadLine();
}
Одно отличие Main() состоит в том, что на этот раз создается объект Саг.
CarEngineHandler, который сохраняется в локальной переменной, чтобы иметь воз­
можность позднее отменить подписку на получение уведомлений. Тогда при следующем
ускорении Саг уже больше не будет выводиться версия входящего сообщения в верхнем
регистре, поскольку эта цель исключена из списка вызовов делегата.
Исходный код. Проект CarDelegate доступен в подкаталоге Chapter 11.
Синтаксис групповых преобразований методов
В предыдущем примере CarDelegate явно создавались экземпляры объекта делега­
та Car.CarEngineHandler, чтобы регистрировать и отменять регистрацию на получе­
ние уведомлений:
static void Main(string[] args)
{
Console.WnteLine (" ***** Delegates as event enablers *****\n");
Car cl = new Car ("SlugBug", 100, 10);
cl .RegisterWithCarEngine(new C a r.CarEngineHandler(OnCarEngineEvent));
Глава 11. Делегаты, события и лямбда-выражения
399
Саг.CarEngineHandler handler2 =
new Car.CarEngineHandler(OnCarEngineEvent2);
cl.RegisterWithCarEngine(handler2);
}
Если нужно вызывать любые унаследованные члены MulticastDelegate или
Delegate, то наиболее простым способом сделать это будет ручное создание перемен­
ной делегата. Однако в большинстве случаев обращаться к внутренностям объекта де­
легата не понадобится. Объект делегата будет нужен только для того, чтобы передать
имя метода как параметр конструктора.
Для простоты в C# предлагается сокращение, которое называется групповое преоб­
разование методов (method group conversion). Это средство позволяет указывать прямое
имя метода, а не объект делегата, когда вызываются методы, принимающие делегаты
в качестве аргументов.
На заметку! Как будет показано далее в этой главе, синтаксис группового преобразования мето­
дов можно также использовать для упрощения регистрации событий С#.
Для целей иллю страц и и создадим новое консольное прилож ение по имени
CarDelegateMethodGroupConversion и добавим в него файл, содержащий класс
Саг, который был определен в проекте CarDelegate. В показанном ниже коде класса
Program используется групповое преобразование методов для регистрации и отмены
регистрации подписки на уведомления.
class Program
{
static void Main (string[] args)
{
Console .WnteLine ("***** Method Group Conversion *****\n ");
Car cl = new Car();
// Зарегистрировать простое имя метода.
cl.RegisterWithCarEngine(CallMeHere);
Console .WnteLine ("***** Speeding up *****");
for (int i = 0; l < 6; i++)
cl.Accelerate(20);
// Отменить регистрацию простого имени метода.
cl.UnRegisterWithCarEngine(CallMeHere);
// Уведомления больше не поступают!
for (int 1 = 0; i < 6; i++)
cl.Accelerate(20);
Console.ReadLine() ;
}
static void CallMeHere(string msg)
{
Console .WnteLine ("=> Message from Car: {0}", msg);
Обратите внимание, что мы не создаем непосредственно объект делегата, а просто
указываем метод, который соответствует ожидаемой сигнатуре делегата (в данном слу­
чае — метод, возвращающий void и принимающий единственную строку). Имейте в
виду, что компилятор C# по-прежнему обеспечивает безопасность типов. Поэтому, если
метод CallMeHere () не принимает string и не возвращает void, компилятор сообщит
об ошибке.
400
Часть III. Дополнительные конструкции программирования на C#
Исходный код. Проект CarDelegateMethodGroupConversion доступен в подкаталоге
Chapter 11.
Понятие ковариантности делегатов
Как вы могли заметить, каждый из делегатов, созданных до сих пор, указывал на ме­
тоды, возвращающие простые числовые типы данных (или void). Однако предположим,
что имеется новое консольное приложение по имени D eleg a teC o va ria n ce, определяю­
щее тип делегата, который может указывать на методы, возвращающие объект пользо­
вательского класса (не забудьте включить в новый проект определение класса Саг):
// Определение типа делегата, указывающего на методы, которые возвращают объект Саг.
public delegate Car ObtainCarDelegate();
Разумеется, цель для делегата можно определить следующим образом:
namespace DelegateCovariance
{
class Program
{
// Определение типа делегата, указывающего на методы,
// которые возвращают объект Саг.
public delegate Car ObtainCarDelegate();
static void Main(string[] args)
{
Console .WnteLine ("***** Delegate Covariance *****\n");
ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar);
Car c = targetA();
Console. WnteLine ("Obtained a {0}", c) ;
Console.ReadLine ();
}
public static Car GetBasicCar ()
{
return new C a r ("Zippy", 100, 55);
}
}
А теперь пусть необходимо унаследовать новый класс от типа Саг по имени
SportsCar и создать тип делегата, который может указывать на методы, возвращаю­
щие этот тип класса. До появления .NET 2.0 для этого пришлось бы определять полно­
стью новый делегат, учитывая то, что делегаты настолько безопасны к типам, что не
подчиняются базовым законам наследования:
// Определение нового типа делегата, указывающего на методы,
// которые возвращают объект SportsCar.
public delegate SportsCar ObtainSportsCarDelegate();
Поскольку теперь есть два типа делегатов, следует создать экземпляр каждого из
них, чтобы получать типы Саг и SportsCar:
class Program
{
public delegate Car ObtainCarDelegate() ;
public delegate SportsCar ObtainSportsCarDelegate() ;
public static Car GetBasicCar()
{ return new C a r ( ) ; }
public static SportsCar GetSportsCar ()
{ return new SportsCar ( ) ; }
Глава 11. Делегаты, события и лямбда-выражения
401
static void Main(string [] args)
{
Console.WriteLine ("***** Delegate Covariance *****\n");
ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar);
Car c = targetA () ;
Console.WriteLine("Obtained a {0}", c) ;
ObtainSportsCarDelegate targetB =
new ObtainSportsCarDelegate(GetSportsCar);
SportsCar sc = targetB ();
Console.WriteLine("Obtained a {0}", sc);
Console.ReadLine ();
}
}
Учитывая законы классического наследования, было бы идеально построить один
тип делегата, который мог бы указывать на методы, возвращающие объекты Саг и
SportsCar (в конце концов, SportsCar “является” Саг). Ковариантность (которую так­
же называют свободными делегатами (relaxed delegates)) делает это вполне возможным.
По сути, ковариантность позволяет построить единственный делегат, который может
указывать на методы, возвращающие связанные классическим наследованием типы
классов.
На заметку! С другой стороны, контравариантность позволяет создать единственный делегат, кото­
рый может указывать на многочисленные методы, принимающие объекты, связанные классиче­
ским наследованием. Дополнительные сведения ищите в документации .NET Framework 4.0 SDK.
class Program
{
// Определение единственного типа делегата, указывающего на методы,
// которые возвращают объект Саг или SportsCar.
public delegate Car ObtainVehicleDelegate();
public static Car GetBasicCar()
{ return new Car(); }
public static SportsCar GetSportsCar()
{ return new SportsCar(); }
static void Main(string[] args)
{
Console.WriteLine("***** Delegate Covariance *****\n");
ObtainVehicleDelegate targetA = new ObtainVehicleDelegate(GetBasicCar);
Car c = targetA();
Console.WriteLine("Obtained a {0}", c) ;
// Ковариантность позволяет такое присваивание цели.
ObtainVehicleDelegate targetB = new ObtainVehicleDelegate(GetSportsCar);
SportsCar sc = (SportsCar)targetB ();
Console.WriteLine("Obtained a {0}", sc) ;
Console.ReadLine();
}
}
Обратите внимание, что тип делегата ObtainVehicleDelegate определен так, что­
бы указывать на методы, возвращающие только строго типизированные объекты типа
Саг. Однако с помощью ковариантности можно указывать на методы, которые также
возвращают производные типы. Для получения доступа к членам производного типа
просто выполните явное приведение.
Исходный код. Проект DelegateCovariance доступен в подкаталоге Chapter 11.
402
Часть III. Дополнительные конструкции программирования на C#
Понятие обобщенных делегатов
Вспомните из предыдущей главы, что язык C# позволяет определять обобщенные
типы делегатов. Например, предположим, что необходимо определить делегат, который
может вызывать любой метод, возвращающий void и принимающий единственный па­
раметр. Если передаваемый аргумент может изменяться, это моделируется через пара­
метр типа. Для иллюстрации рассмотрим следующий код нового консольного приложе­
ния по имени GenericDelegate:
namespace GenericDelegate
{
// Этот обобщенный делегат может вызывать любой метод, который
// возвращает void и принимает единственный параметр типа.
public delegate void MyGenericDelegate<T> (Т arg);
class Program
{
static void Main(string [] args)
{
Console.WriteLine ("***** Generic Delegates *****\n");
// Регистрация целей.
MyGenencDelegate<string> strTarget =
new MyGenericDelegate<string>(StringTarget);
strTarget("Some string data");
MyGenencDelegate<int> intTarget =
new MyGenericDelegate<int>(IntTarget);
intTarget (9);
Console.ReadLine ();
}
static void StringTarget(string arg)
{
Console.WriteLine("arg in uppercase is: {0}"f arg.ToUpper());
}
static void IntTarget(int arg)
{
Console.WriteLine("++arg is: {0}", ++arg);
}
}
}
Обратите внимание, что в MyGenericDelegate<T> определен единственный пара­
метр, представляющий аргумент для передачи цели делегата. При создании экземпляра
этого типа необходимо указать значение параметра типа вместе с именем метода, кото­
рый может вызывать делегат. Таким образом, если указать тип string, целевому методу
будет отправлено строковое значение:
// Создать экземпляр MyGenencDelegate<T>
// со string в качестве параметра типа.
MyGenericDelegate<stnng> strTarget =
new MyGenericDelegate<string>(StringTarget);
strTarget("Some string data");
Имея формат объекта strTarget, метод StringTarget теперь должен принимать в
качестве параметра единственную строку:
static void StringTarget(s trin g arg)
{
Console.WriteLine("arg in uppercase is: {0}", arg.ToUpper());
}
Глава 11. Делегаты, события и лямбда-выражения
403
Эмуляция обобщенных делегатов без обобщений
Обобщенные делегаты предоставляют более гибкий способ спецификации ме­
тода, подлежащего вызову в безопасной к типам манере. До появления обобщений
(в .NET 2.0) тога же конечного результата можно было достичь с использованием пара­
метра S ystem .O bject:
public delegate void MyDelegate(object arg);
Хотя это позволяет посылать любой аргумент цели делегата, это не обеспечивает
безопасность типов и не избавляет от бремени упаковки/распаковки. Для примера
предположим, что созданы два экземпляра M yDelegate, и оба они указывают на один и
тот же метод MyTarget. Обратите внимание на упаковку/распаковку и отсутствие безо­
пасности типов.
class Program
{
static void Main(string [] args)
// Регистрация с "традиционным" синтаксисом делегатов.
MyDelegate d = new MyDelegate(MyTarget);
d("More string data");
// Синтаксис групповых преобразований методов.
MyDelegate d2 = MyTarget;
d2 (9); // Дополнительные издержки на упаковку.
Console.ReadLine() ;
}
// Из-за отсутствия безопасности типов необходимо
// определить лежащий в основе тип перед приведением.
static void MyTarget(object arg)
{
if(arg is int)
{
int i = (int)arg; // Дополнительные издержки на распаковку.
Console.WriteLine("++arg is: {0}", ++i) ;
}
if(arg is string)
{
string s = (string)arg;
Console.WriteLine("arg in uppercase is: {0}", s .ToUpper());
}
}
}
Когда целевому методу посылается тип значения, это значение упаковывается и сно­
ва распаковывается при получении методом. Также, учитывая, что входящий параметр
может быть чем угодно, перед приведением должна производиться динамическая про­
верка лежащего в основе типа. С помощью обобщенных делегатов необходимую гиб­
кость можно получить без упомянутых проблем.
Исходный код. Проект G e n e ric D e le g a te доступен в подкаталоге C hapter 11.
На этом первоначальный экскурс в тип делегата .NET завершен. Мы еще вернемся
к некоторым дополнительным деталям работы с делегатами в конце этой главы и еще
раз — в главе 19, когда будем рассматриваться многопоточность. А теперь переходим к
связанной теме — ключевому слову ev e n t в С#.
404
Часть III. Дополнительные конструкции программирования на C#
Понятие событий C#
Делегаты — весьма интересные конструкции в том смысле, что позволяют объектам,
находящимся в памяти, участвовать в двустороннем общении. Однако работа с делега­
тами напрямую может порождать довольно однообразный код (определение делегата,
определение необходимых переменных-членов и создание специальных методов реги­
страции и отмены регистрации для предохранения инкапсуляции).
Более того, если делегаты используются в качестве механизма обратного вызова в
приложениях напрямую, существует еще одна проблема: если не определить делегат —
переменную-член класса как приватную, то вызывающий код получит прямой доступ
к объектам делегатов. В этом случае вызывающий код может присвоить переменной
новый объект-делегат (фактически удалив текущий список функций, подлежащих вызо­
ву), и что еще хуже — вызывающий код сможет напрямую обращаться к списку вызовов
делегата. Чтобы проиллюстрировать проблему, рассмотрим следующую переделанную
(и упрощенную) версию предыдущего примера CarDelegate:
public class Car
{
public delegate void CarEngineHandler(string msgForCaller);
// Теперь этот член p u b lic !
public CarEngineHandler listOfHandlers;
// Просто вызвать уведомление Exploded.
public void Accelerate (int delta)
{
if (listOfHandlers '= null)
listOfHandlers("Sorry, this car is dead...");
}
Обратите внимание, что теперь нет делегата — приватной переменной-члена, ин­
капсулированной с помощью специальных методов регистрации. Поскольку эти члены
сделаны общедоступными, вызывающий код может непосредственно обращаться к чле­
ну listOfHandlers и переназначить этот тип на новые объекты CarEngineHandler,
после чего вызывать делегат, когда вздумается:
class Program
{
static void Main(string [] args)
{
Console.WriteLine ("***** A gh1 No Encapsulation1 *****\n");
// Создать Car.
Car myCar = new Car();
// Есть прямой доступ к делегату!
myCar.listOfHandlers = new Car.CarEngineHandler(CallWhenExploded) ;
myCar.Accelerate (10);
// Назначаем ему совершенно новый объект...
/ / В лучшем случае получается путаница.
myCar.listOfHandlers = new Car.CarEngineHandler(CallHereToo);
myCar.Accelerate (10);
// Вызывающий код может также напрямую вызвать делегат!
myCar.listOfHandlers.Invoke ("hee, hee, hee...");
Console.ReadLine ();
}
static void CallWhenExploded(string msg)
{ Console.WriteLine(msg); }
static void CallHereToo(string msg)
{ Console.WriteLine (msg); }
Глава 11. Делегаты, события и лямбда-выражения
405
Общедоступные члены-делегаты нарушают инкапсуляцию, что не только затруднит
сопровождение кода (и отладку), но также сделает приложение уязвимым в смысле безо­
пасности. Ниже показан вывод текущего примера:
***** Agh [
No Encapsulation1 *****
Sorry, this car is dead. ..
Sorry, this car is dead. ..
hee, hee, hee. ..
Очевидно, что вряд ли имеет смысл предоставлять другим приложениям право и з­
менять то, на что указывает делегат, или вызывать его члены напрямую.
Исходный код. Проект PublicDelegateProblem доступен в подкаталоге Chapter 11.
Ключевое слово e v e n t
В качестве сокращения, избавляющего от необходимости строить специальные ме­
тоды для добавления и удаления методов в списке вызовов делегата, в C# предусмотре­
но ключевое слово event. Обработка компилятором ключевого слова event приводит
к автоматическому получению методов регистрации и отмены регистрации наряду со
всеми необходимыми переменными-членами для типов делегатов. Эта переменнаячлен делегата всегда объявляется приватной, и потому не доступна напрямую объекту,
инициировавшему событие. Точнее говоря, ключевое слово event — это не более чем
синтаксическое украшение, позволяющее экономить на наборе кода.
Определение события представляет собой двухэтапный процесс. Во-первых, нужно
определить делегат, который будет хранить список методов, подлежащих вызову при
возникновении события. Во-вторых, необходимо объявить событие (используя ключе­
вое слово event) в терминах связанного типа делегата.
Чтобы проиллюстрировать ключевое слово event, создадим новое консольное при­
ложение по имени CarEvents. В классе Саг будут определены два события под назва­
ниями AboutToBlow и Exploded. Эти события ассоциированы с единственным типом
делегата по имени CarEngineHandler. Ниже показаны начальные изменения в классе
Саг:
public class Car
{
// Этот делегат работает в сочетании с событиями Саг.
public delegate void CarEngineHandler (string msg) ;
// Car может посылать следующие события.
public event CarEngineHandler Exploded;
public event CarEngineHandler AboutToBlow;
}
Отправка события вызывающему коду состоит просто в указании имени события
вместе со всеми необходимыми параметрами, определенными в ассоциированном де­
легате. Чтобы удостовериться, что вызывающий код действительно зарегистрировал
событие, его следует проверить на равенство null перед вызовом набора методов деле­
гата. Ниже приведена новая версия метода Accelerate () класса Саг:
public void Accelerate(int delta)
{
// Если автомобиль сломан, инициировать событие Exploded.
if (carlsDead)
{
if (Exploded '= null)
406
Часть III. Дополнительные конструкции программирования на C#
Exploded("Sorry, this car is dead...");
}
else
{
CurrentSpeed += delta;
// Почти сломан?
if (10 == MaxSpeed - CurrentSpeed
&& AboutToBlow != null)
{
AboutToBlow("Careful buddy! Gonna blow!");
// Все в порядке!
if (CurrentSpeed >= maxSpeed)
carlsDead = true;
else
Console.WriteLine ("CurrentSpeed = {0}", CurrentSpeed);
}
}
Таким образом, объект Car сконфигурирован для отправки двух специальных со­
бытий, без необходимости определения специальных функций регистрации или объ­
явления переменных-членов. Чуть ниже будет продемонстрировано использование
этого нового объекта, но сначала давайте рассмотрим архитектуру событий немного
подробнее.
“За кулисами” событий
Событие C# в действительности развертывается в два скрытых метода, один из
которых имеет префикс add_, а другой — remove . За этим префиксом следует имя
события С#. Например, событие Exploded превращается в два скрытых метода CIL
с именами add_Exploded () и remove_Exploded (). Если заглянуть в CIL-код метода
add_AboutToBlow (), можно обнаружить там вызов метода Delegate.Combine(). Ниже
показан частичный код CIL:
.method public hidebysig specialname instance void
add_AboutToBlow(class CarEvents.Car/CarEngineHandler 'value')
cil managed synchronized
call class [mscorlib]System.Delegate
[m scorlib]System .D elegate: : Combine(
cla ss [m scorlib]System .D elegate, class [m scorlib]System .Delegate)
Как и следовало ожидать, remove AboutToBlow () неявно вызывает Delegate.
Remove ():
.method public hidebysig specialname instance void
remove_AboutToBlow (class CarEvents.Car/CarEngineHandler 'value')
cil managed synchronized
{
call class [mscorlib]System.Delegate
[m scorlib] System. D elegate: : Remove (
cla ss [m scorlib]System .Delegate, cla ss [m scorlib]System .Delegate)
Глава 11. Делегаты, события и лямбда-выражения
407
И, наконец, в CIL-коде, представляющем само событие, используются директи­
вы .addon и .removeon для отображения имен корректных методов add XXX () и
remove_XXX () для вызова:
.event CarEvents.Car/EngineHandler AboutToBlow
{
.addon void CarEvents.Car::add_AboutToBlow
(class CarEvents.Car/CarEngineHandler)
.removeon void CarEvents.Car::remove_AboutToBlow
(class CarEvents.Car/CarEngineHandler)
}
Теперь, когда вы разобрались, как строить класс, способный отправлять события C#
(и уже знаете, что события — это лишь способ сэкономить время на наборе кода), сле­
дующий большой вопрос связан с организацией прослушивания входящих событий на
стороне вызывающего кода.
Прослушивание входящих событий
События C# также упрощают акт регистрации обработчиков событий на стороне
вызывающего кода. Вместо того чтобы специфицировать специальные вспомогатель­
ные методы, вызывающий код просто использует операции += и -= непосредственно
(что приводит к внутренним вызовам методов add XXX () или remove XXX ()). Для реги­
страции события руководствуйтесь следующим шаблоном:
// ИмяОбъекта.ИмяСобытия += new СвязанныйДелегат(функцияДляВызова);
//
Car.EngineHandler d = new Car.CarEngineHandler(CarExplodedEventHandler)
myCar.Exploded += d;
Для отключения от источника событий служит операция -= в соответствии со сле­
дующим шаблоном:
// ИмяОбъекта.ИмяСобытия -= new СвязанныйДелегат(функцияДляВызова);
//
myCar.Exploded -= d;
Следуя этому очень простому шаблону, переделаем метод Main(), применив на этот
раз синтаксис регистрации методов С#:
class Program
{
static void Main(string [] args)
{
Console.WriteLine (''***** Fun with Events *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий.
cl.AboutToBlow += new Car.CarEngineHandler(CarIsAlmostDoomed);
cl.AboutToBlow += new Car.CarEngineHandler(CarAboutToBlow);
Car.CarEngineHandler d = new Car.CarEngineHandler(CarExploded);
cl.Exploded += d;
Console.WriteLine (”***** Speeding up *****");
for (int i = 0; i < 6; i++)
cl.Accelerate(20);
// Удалить метод CarExploded из списка вызовов.
cl.Exploded -= d;
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0; i < 6; i++)
cl.Accelerate (20) ;
Console.ReadLine ();
408
Часть III. Дополнительные конструкции программирования на C#
public static void CarAboutToBlow (string msg)
{ Console.WriteLine(msg); }
public static void CarlsAlmostDoomed (string msg)
{ Console.WriteLine ("=> Critical Message from Car: {0}", msg); }
public static void CarExploded(string msg)
{ Console.WriteLine (msg); }
}
Чтобы еще более упростить регистрацию событий, можно применить групповое пре­
образование методов. Ниже показана очередная модификация Main().
static void Main(string[] args)
{
Console. WriteLine (''***** Fun with Events *****\n");
Car cl = new Car ("SlugBug", 100, 10);
// Регистрация обработчиков событий,
c l .AboutToBlow += CarlsAlmostDoomed;
cl .AboutToBlow += CarAboutToBlow;
cl.Exploded += CarExploded;
Console.WriteLine ("***** Speeding up *****");
for (int i = 0; l < 6; i++)
cl.Accelerate(20) ;
cl.Exploded -= CarExploded;'
Console.WriteLine ("\n***** Speeding up *****"),for (int l = 0; l < 6; i++ )
cl.Accelerate(20);
Console.ReadLine() ;
}
Упрощенная регистрация событий
с использованием Visual Studio 2010
Среда Visual Studio 2010 предоставляет помощь в процессе регистрации обработ­
чиков событий. В случае применения синтаксиса += во время регистрации событий
открывается окно IntelliSense, приглашающее нажать клавишу <ТаЬ> для автоматиче­
ского заполнения экземпляра делегата (рис. 11.2).
^{C arE vents.Program
*1 V H o okln to E ven tiQ
*
p u b lic
S ta tic
v o i d H o o k In t o E v e n ts ( )
{
newCar = new
• ();
n e w C a r.A b o u tT o B lo w + }
| new C a r.C a r E n g in e H a n d le r( n e w C a r _ A b o u tT o B lo w ) ;
iP r e s s TAB t o
in s e r t ) j
)
100 %
-
‘
Рис. 11.2. Выбор делегата с помощью IntelliSense
После нажатия клавиши <ТаЬ> появляется возможность ввести имя обработчика
событий, который нужно сгенерировать (или просто принять имя по умолчанию), как
показано на рис. 11.3.
Снова нажав <ТаЬ>, вы получите заготовку кода цели делегата в корректном форма­
те (обратите внимание, что этот метод объявлен статическим, потому что событие было
зарегистрировано внутри статического метода Main()):
static void newCar_AboutToBlow (string msg)
{
// Add your code1
}
Глава 11. Делегаты, события и лямбда-выражения
409
Рис. 11.3. Формат цели делегата IntelliSense
Средство IntelliSense доступно для всех событий .NET из библиотек базовых клас­
сов. Это средство интегрированной среды разработки замечательно экономит время,
но не избавляет от необходимости поиска в справочной системе .NET правильного де­
легата для использования с определенным событием, а также формата целевого метода
делегата.
Исходный код. Проект CarEvents доступен в подкаталоге Chapter 11.
Создание специальных аргументов событий
По правде говоря, есть еще одно последнее усовершенствование, которое можно вне­
сти в текущую итерацию класса Саг и которое отражает рекомендованный Microsoft
шаблон событий. Если вы начнете исследовать события, посылаемые определенным
типом из библиотек базовых классов, то обнаружите, что первым параметром лежаще­
го в основе делегата будет System.Object, в то время как вторым параметром — тип,
являющийся потомком System.EventArgs.
Аргумент System.Object представляет ссылку на объект, который отправляет со­
бытие (такой как Саг), а второй параметр — информацию, относящуюся к обрабаты­
ваемому событию. Базовый класс System. Event Args представляет событие, которое не
посылает никакой специальной информации:
public class EventArgs
{
public static readonly System.EventArgs Empty;
public EventArgs ();
}
Для простых событий можно передать экземпляр EventArgs непосредственно.
Однако чтобы передавать какие-то специальные данные, потребуется построить под­
ходящий класс, унаследованный от EventArgs. Для примера предположим, что есть
класс по имени CarEventArgs, поддерживающий строковое представление сообщения,
отправленного получателю:
public class CarEventArgs : EventArgs
{
public readonly string msg;
public CarEventArgs(string message)
{
msg = message;
}
}
Теперь понадобится модифицировать делегат CarEngineHandler, как показано ниже
(само событие не изменяется):
410
Часть III. Дополнительные конструкции программирования на C#
public class Car
{
public delegate void CarEngineHandler(object sender, CarEventArgs e) ;
}
Здесь при инициализации события из метода Accelerate () нужно использовать
ссылку на текущий Саг (через ключевое слово this) и экземпляр типа CarEventArgs.
Например, рассмотрим следующее обновление:
public void Accelerate(int delta)
{
// Если этот автомобиль сломан, инициировать событие Exploded.
if (carlsDead)
{
if (Exploded != null)
Exploded(this, new CarEventArgs("Sorry, this car is dead..."));
Все, что потребуется сделать на вызывающей стороне — это обновить обработчики
событий для получения входных параметров и получения сообщения через поле, дос­
тупное только для чтения. Например:
public static void CarAboutToBlow(object sender, CarEventArgs e)
{
Console .WnteLine ("{ 0 } says: {1}", sender, e.msg);
}
Если получатель желает взаимодействовать с объектом, отправившим событие,
можно выполнить явное приведение System.Object. С помощью такой ссылки можно
вызвать любой общедоступный метод объекта, который отправил событие:
public static void CarAboutToBlow (object sender, CarEventArgs e)
{
// Чтобы подстраховаться, произведем проверку во время выполнения перед приведением.
if (sender is Car)
{
Car c = (Car)sender;
Console.WriteLine("Critical Message from {0}: {1}", c.PetName, e.msg);
}
}
Исходный код. Проект PrimAndProperCarEvents доступен в подкаталоге Chapter 11.
Обобщенный делегат EventHandler<T>
Учитывая, что очень много специальных делегатов принимают объект в первом
параметре и наследников EventArgs — во втором, можно еще более упростить преды­
дущий пример, используя обобщенный тип EventHandler<T>, где Т — специальный
тип-наследник EventArgs. Рассмотрим следующую модификацию типа Саг (обратите
внимание, что строить специальный делегат больше не нужно):
public class Car
{
public event EventHandler<CarEventArgs> Exploded;
public event EventHandler<CarEventArgs> AboutToBlow;
Глава 11. Делегаты, события и лямбда-выражения
411
Метод Main () может затем использовать EventHandler<CarEventArgs> везде, где
ранее указывался CarEngineHandler:
static void Main(string [] args)
{
Console.WnteLine ("***** Prim and Proper Events *****\n ");
// Создать Car обычным образом.
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий.
cl.AboutToBlow += CarIsAlmostDoomed;
cl.AboutToBlow += CarAboutToBlow;
EventHandler<CarEventArgs> d = new EventHandler<CarEventArgs>(CarExploded);
cl.Exploded += d;
Итак, вы ознакомились с основными аспектами работы с делегатами и событиями
на языке С#. Хотя этого вполне достаточно для решения практически любых задач, свя­
занных с обратными вызовами, в завершение главы рассмотрим ряд финальных упро­
щений, а именно — анонимные методы и лямбда-выражения.
Исходный код. Проект PrimAndProperCarEvents (Generic) доступен в подкаталоге
Chapter 11.
Понятие анонимных методов C#
Как было показано выше, когда вызывающий код желает прослушивать входящие
события, он должен определить специальный метод в классе (или структуре), соответ­
ствующий сигнатуре ассоциированного делегата. Ниже приведен пример:
class Program
{
static void Main(string[] args)
{
SomeType t = new SomeTypeO ;
// Предположим, что SomeDeletage может указывать на методы,
// которые не принимают аргументов и возвращают void.
t.SomeEvent += new SomeDelegate(MyEventHandler) ;
}
// Обычно вызывается только объектом SomeDelegate.
public static void MyEventHandler()
{
// Что-то делать по возникновении события.
Однако если подумать, то такие методы, как MyEventHandler (), редко предназна­
чены для обращения из любой другой части программы помимо делегата. Если речь
идет о производительности, несложно вручную определить отдельный метод для вызова
объектом делегата.
Чтобы справиться с этим, можно ассоциировать событие непосредственно с бло­
ком операторов кода во время регистрации события. Формально такой код называется
анонимным методом. Для иллюстрации синтаксиса напишем метод Main(), который
обрабатывает события, посланные из типа Саг, с использованием анонимных методов
вместо специальных именованных обработчиков событий:
412
Часть III. Дополнительные конструкции программирования на C#
class Program
{
static void Main(string[] args)
{
Console .WnteLine ("**** * Anonymous Methods *****\n ");
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий в виде анонимных методов.
cl.AboutToBlow += delegate {
Console .WnteLine ("Eek ! Going too fast!");
};
cl .AboutToBlow += delegate(object sender, CarEventArgs e) {
Console .WnteLine ("Message from Car: {0}", e.msg);
cl.Exploded += delegate(object sender, CarEventArgs e) {
Console .WnteLine ("Fatal Message from Car: {0}", e.msg);
// Это в конечном итоге инициирует события,
for (int i = 0; i < 6; i++ )
cl.Accelerate (20);
Console.ReadLine ();
На заметку! Последняя фигурная скобка анонимного метода должна завершаться точкой с запя­
той. Если забыть об этом, во время компиляции возникнет ошибка.
Обратите внимание, что в классе Program теперь не определяются специальные ста­
тические обработчики событий вроде CarAboutToBlowO или CarExplodedO . Вместо
этого с помощью синтаксиса += определяются встроенные неименованные (анонимные)
методы, к которым вызывающий код будет обращаться во время обработки события.
Базовый синтаксис анонимного метода соответствует следующему псевдокоду:
class Program
{
static void Main(string [] args)
{
SomeType t = new SomeType();
t.SomeEvent += delegate (optionallySpecifledDelegateArgs)
{ /* операторы */ };
}
}
Обратите внимание, что при обработке первого события AboutToBlow внутри пре­
дыдущего метода Main() аргументы, передаваемые из делегата, не указываются:
c l .AboutToBlow += delegate {
Console. WnteLine ("Eek! Going too fast!");
};
Строго говоря, вы не обязаны принимать входные аргументы, посланные определен­
ным событием. Однако если планируется использовать эти входные аргументы, нужно
указать параметры, прототипированные типом делегата (как показано во второй обра­
ботке событий AboutToBlow и Exploded). Например:
cl.AboutToBlow += delegate(o bject sender, CarEventArgs e) {
Console.WriteLine ("Critical Message from Car: {0}", e.msg);
};
Глава 11. Делегаты, события и лямбда-выражения
413
Доступ к локальным переменным
Анонимные методы интересны тем, что могут обращаться к локальным переменным
метода, в котором они определены. Формально такие переменные называются внешни­
ми (outer) переменными анонимного метода. Ниже отмечены некоторые важные момен­
ты, касающиеся взаимодействия между контекстом анонимного метода и контекстом
определяющего их метода.
• Анонимный метод не имеет доступа к параметрам ref и out определяющего их
метода.
• Анонимный метод не может иметь локальных переменных, имена которых совпа­
дают с именами локальных переменных объемлющего метода.
• Анонимный метод может обращаться к переменным экземпляра (или статическим
переменным) из контекста объемлющего класса.
• Анонимный метод может объявлять локальные переменные с теми же именами,
что и у членов объемлющего класса (локальные переменные имеют отдельный
контекст и скрывают внешние переменные-члены).
Предположим, что метод Main() определяет локальную переменную по имени
aboutToBlowCounter типа int. Внутри анонимных методов, обрабатывающих событие
AboutToBlow, мы увеличим значение этого счетчика на 1 и выведем результат на кон­
соль перед завершением Main():
static void Main(string[] args)
{
Console.WnteLine ("***** Anonymous Methods *****\n" );
int aboutToBlowCounter = 0;
// Создать Car обычным образом.
Car cl = new Car ("SlugBug", 100, 10);
// Зарегистрировать обработчики событий в виде анонимных методов.
cl .AboutToBlow += delegate
{
aboutToBlowCounter++;
Console.WriteLine("Eek! Going too fast!");
};
cl .AboutToBlow += (object sender, CarEventArgs e)
{
aboutToBlowCounter++;
Console .WnteLine ("Critical Message from Car: {0}", msg) ;
Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter);
Console.ReadLine();
}
Запустив этот модифицированный метод M ain (), вы обнаружите, что финальный
вывод Console.WriteLine() сообщит о двукратном вызове AboutToBlow.
Исходный код. Проект AnonymousMethods доступен в подкаталоге Chapter 11.
Понятие лямбда-выражений
Чтобы завершить знакомство с архитектурой событий .NET, рассмотрим лямбда-вы­
ражения. Как объяснялось ранее в этой главе, C# поддерживает способность обрабаты­
вать события “встроенным образом”, назначая блок операторов кода непосредственно
событию с использованием анонимных методов вместо построения отдельного мето­
414
Часть III. Дополнительные конструкции программирования на C#
да, подлежащего вызову лежащим в основе делегатом. Лямбда-выражения — это всего
лишь лаконичный способ записи анонимных методов, в конечном итоге упрощающий
работу с типами делегатов .NET.
Чтобы подготовить фундамент для изучения лямбда-выражений, создадим новое
консольное приложение по имени SimpleLambdaExpressions. Теперь займемся мето­
дом FindAllO обобщенного типа List<T>. Этот метод может быть вызван, когда нужно
извлечь подмножество элементов из коллекции, и он имеет следующий прототип:
// Метод класса System.Collections.Generic.List<T>.
public List<T> FindAll(Predicate<T> match)
Как видите, этот метод возвращает объект List<T>, представляющий подмножество
данных. Также обратите внимание, что единственный параметр FindAllO — обобщен­
ный делегат типа System. Predicate<T>. Этот делегат может указывать на любой метод,
возвращающий bool и принимающий единственный параметр:
// Этот делегат используется методом FindAllO для извлечения подмножества.
public delegate bool Predicate<T>(Т obj);
Когда вызывается FindAllO, каждый элемент в List<T> передается методу, указанно­
му объектом Predicate<T>. Реализация этого метода будет производить некоторые вычис­
ления для проверки соответствия элемента данных указанному критерию, возвращая true
или false. Если метод вернет true, то текущий элемент будет добавлен в List<T>, пред­
ставляющий искомое подмножество. Прежде чем посмотреть, как лямбда-выражения упро­
щают работу с FindAll (), давайте решим эту задачу в длинной нотации, используя объек­
ты делегатов непосредственно. Добавим метод (по имени TraditionalDelegateSyntaxO)
к типу Program, который взаимодействует с System.Predicate<T> для обнаружения чет­
ных чисел в списке List<T> целочисленных значений:
class Program
{
static void Mai n (string [] args)
{
Console.WriteLine ("***** Fun with Lambdas *****\n");
TraditionalDelegateSyntax ();
Console.ReadLine ();
}
static void TraditionalDelegateSyntaxO
{
// Создать список целых чисел.
List<int> list = new List<int>();
list.AddRange (new int [] { 20, 1, 4, 8, 9 , 44 });
// Вызов F in d A llO с использованием традиционного синтаксиса делегатов.
Predicate<int> callback = new Predicate<int>(IsEvenNumber);
List<int> evenNumbers = list.FindAll(callback);
Console.WriteLine ("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write ("{0}\t", evenNumber);
}
Console.WriteLine ();
)
// Цель для делегата P re d ic a te O .
static bool IsEvenNumber(int i)
{
// Это четное число?
return (i % 2) == 0;
Глава 11. Делегаты, события и лямбда-выражения
415
Здесь имеется метод (IsEvenNumber ()), отвечающий за проверку входного целочис­
ленного параметра на предмет четности или нечетности через операцию C# взятия мо­
дуля % (получения остатка от деления). В результате запуска приложения на консоль
выводятся числа .20, 4, 8 и 44.
Хотя этот традиционный подход к работе с делегатами функционирует ожидаемым
образом, метод IsEvenNumber () вызывается только при очень ограниченных условиях;
в частности, когда вызывается FindAllO, который взваливает на нас полный багаж
определения метода. Если бы вместо этого применялся анонимный метод, код стал бы
существенно яснее. Рассмотрим следующий новый метод в классе Program:
static void AnonymousMethodSyntax()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });
// Теперь использовать анонимный метод.
List<int> evenNumbers = list.FindAll(delegate(int i)
{ return (l d 2) == 0; } );
// Вывод четных чисел.
Console.WnteLine ("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console.WnteLine () ;
}
В этом случае вместо прямого создания типа делегата Predicate<T> с последующим
написанием отдельного метода, метод встраивается как анонимный. Хотя это шаг в пра­
вильном направлении, мы все еще обязаны использовать ключевое слово delegate (или
строго типизированный Predicate<T>), и должны убедиться в соответствии списка па­
раметров. Также, согласитесь, синтаксис, используемый для определения анонимного
метода, выглядит несколько тяжеловесно, что особенно проявляется здесь:
List<int> evenNumbers = list.FindAll (
delegate(int i)
{
return (i % 2) == 0;
}
);
Для дальнейшего упрощения вызова FindAllO можно применять лямбда-выраже­
ния. Используя этот новый синтаксис, вообще не приходится иметь дело с лежащим в
основе объектом делегата. Рассмотрим следующий новый метод в классе Program:
static void LambdaExpressionSyntax()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4, 8, 9 , 44 });
// Теперь используем лямбда-выражение С#.
List<int> evenNumbers = list.FindAll (l => (l % 2) == 0) ;
// Вывод четных чисел.
Console.WriteLine("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write("{0}\t", evenNumber);
}
Console .WnteLine () ;
416
Часть III. Дополнительные конструкции программирования на C#
Здесь обратите внимание на довольно странный оператор кода, передаваемый ме­
тоду FindAllO, который в действительности и является лямбда-выражением. В этой
модификации примера вообще нет никаких следов делегата Predicate<T> (как и клю­
чевого слова delegate). Все, что специфицировано вместо них — это лямбда-выраже­
ние: i ==> (i ь 2) == 0.
Прежде чем разбирать синтаксис дальше, пока просто усвойте, что лямбда-выра­
жения могут применяться везде, где использовался анонимный метод или строго ти­
пизированный делегат (обычно в более лаконичном виде). “За кулисами” компилятор
C# транслирует выражение в стандартный анонимный метод, использующий тип де­
легата Predicate<T> (в чем можно убедиться с помощью утилиты ildasm.exe или
reflector.exe). В частности, следующий оператор кода:
// Это лямбда-выражение...
List<int> evenNumbers = list.FindAll(i => (i " 2) == 0);
компилируется в примерно такой код С#:
// ...превращается в следующий анонимный метод.
List<int> evenNumbers = list.FindAll(delegate (int l)
{
return (l и 2) == 0;
}) ;
Анализ лямбда-выражения
Лямбда-выражение начинается со списка параметров, за которым следует лексема
=> (лексема C# для лямбда-выражения позаимствована из лямбда-вычислений), а за
ней — набор операторов (или единственный оператор), который будет обрабатывать
параметры. На самом высоком уровне лямбда-выражение можно представить следую­
щим образом:
АргументыДляОбработки => ОбрабатывающиеОператоры
То, что находится внутри метода LambdaExpressionSyntaxO, следует понимать так:
// i — список параметров.
// ( i % 2) = 0 - набор операторов для обработки i .
List<int> evenNumbers = list.FindAll(i => (l 1 2) == 0);
Параметры лямбда-выражения могут быть типизированы явно или неявно. В на­
стоящий момент тип данных, представляющий параметр i (целое), определяется неяв­
но. Компилятор способен понять, что i — целое, на основе контекста всего лямбда-вы­
ражения, поместив тип данных и имя переменной в пару скобок, как показано ниже:
// Теперь установим тип параметров явно.
List<int> evenNumbers = list.FindAll ((int i) => (i % 2) == 0) ;
Как было показано, если лямбда-выражение имеет одиночный неявно типизирован­
ный параметр, то скобки в списке параметров могут быть опущены. При желании быть
последовательным в использовании параметров лямбда-выражений, можно всегда за­
ключать список параметров в скобки, чтобы выражение выглядело так:
List<int> evenNumbers = list.FindAll(( i ) => (l
1
2) == 0);
И, наконец, обратите внимание, что сейчас выражение не заключено в скобки (разу­
меется, вычисление остатка от деления помещается в скобки, чтобы гарантировать его
выполнение перед проверкой равенства). Лямбда-выражение с оператором, заключен­
ным в скобки, выглядит следующим образом:
// Теперь заключим в скобки и выражение.
List<int> evenNumbers = list.FindAll ((i) => ((l % 2) == 0));
Глава 11. Делегаты, события и лямбда-выражения
417
Теперь, когда известны разные способы построения лямбда-выражения, как его
представить в понятных человеку терминах? Оставив чистую математику в стороне,
можно привести следующее объяснение:
// Список параметров (в данном случае — единственное целое по имени ±)
// будет обработан выражением ( i % 2) = 0 .
List<int> evenNumbers = list.FindAll((i) => ( ( 1 % 2) == 0));
Обработка аргументов внутри множества операторов
Наше первое лямбда-выражение состояло из единственного оператора, который в
результате вычисления дает булевское значение. Однако, как должно быть хорошо из­
вестно, многие цели делегатов должны выполнять множество операторов кода. По этой
причине C# позволяет строить лямбда-выражения, состоящие из нескольких блоков
операторов. Когда выражение должно обрабатывать параметры в нескольких строках
кода, понадобится выделить контекст этих операторов с помощью фигурных скобок.
Рассмотрим следующую модификацию метода LambdaExpressionSyntax():
static void LambdaExpressionSyntax ()
{
// Создать список целых.
List<int> list = new List<int>();
list.AddRange(new int[] { 20, 1, 4,
8,
9,
44 });
// Обработать каждый аргумент в группе операторов кода.
List<int> evenNumbers = list.FindAll((i) =>
{
Console.WriteLine ("value of l is currently: {0}", i);
bool lsEven = ((i ь 2) == 0) ;
return lsEven;
}) ;
// Вывод четных чисел.
Console.WriteLine ("Here are your even numbers:");
foreach (int evenNumber in evenNumbers)
{
Console.Write ("{0}\t", evenNumber);
}
Console.WriteLine();
}
В этом случае список параметров (опять состоящий из единственного целого i) обра­
батывается набором операторов кода. Помимо вызова Console.WriteLine(), оператор
вычисления остатка от деления разбит на два оператора для повышения читабельно­
сти. Предположим, что каждый из рассмотренных выше методов вызывается в Main():
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Lambdas *****\n");
TraditionalDelegateSyntax ();
AnonymousMethodSyntax();
Console.WriteLine();
LambdaExpressionSyntax();
Console.ReadLine();
}
Запуск этого приложения даст следующий вывод:
***** pun with Lambdas *****
Here are your even numbers:
20
4
8
44
418
Часть III. Дополнительные конструкции программирования на C#
Here are
20
4
value of
value of
value of
value of
value of
value of
Here are
20
4
your even numbers :
8
44
l is currently: 20
l is currently: 1
l is currently: 4
l is currently: 8
l is currently: 9
l is currently: 44
your even numbers:
8
44
Исходный код. Проект SimpleLambdaExpressions доступен в подкаталоге Chapter 11.
Лямбда-выражения с несколькими параметрами и без параметров
Показанные выше лямбда-выражения обрабатывало единственный параметр.
Однако это вовсе не обязательно, поскольку лямбда-выражения могут обрабатывать
множество аргументов или вообще не иметь аргументов. Для иллюстрации первого сце­
нария создадим консольное приложение по имени LambdaExpressionsMultipleParams
со следующей версией класса SimpleMath:
public class SimpleMath
{
public delegate void MathMessage(string msg, int result);
private MathMessage mmDelegate;
public void SetMathHandler (MathMessage target)
{mmDelegate = target; }
public void Add (int x, int y)
{
if (mmDelegate != null)
mmDelegate.Invoke("Adding has completed!", x + y) ;
}
Обратите внимание, что делегат MathMessage принимает два параметра. Чтобы
представить их в виде лямбда-выражения, метод Main() может быть реализован так:
s ta tic void M ain(string [ ] args)
{
// Регистрация делегата как лямбда-выражения.
SimpleMath m = new SimpleMath();
m. SetMathHandler( (msg, re su lt) =>
{C on sole.W riteLin e( "Message: {0 }, Result: {1 }" , msg, r e s u l t ) ; } ) ;
// Это приведет к выполнению лямбда-выражения.
m.Add(10, 10);
Console. ReadLine( ) ;
}
Здесь используется выведение типа компилятором, поскольку для простоты два па­
раметра не типизированы строго. Однако можно было бы вызвать SetMathHandler ()
следующим образом:
m. SetMathHandler( (s trin g msg, in t re su lt) =>
{ Console.W riteL in e( "Message: { 0} , Result: { 1} " , msg, r e s u l t ) ; } ) ;
И, наконец, если лямбда-выражение используется для взаимодействия с делегатом,
вообще не принимающим параметров, то это можно сделать, указав в качестве пара­
метра пару пустых скобок. Таким образом, предполагая, что определен следующий тип
делегата:
Глава 11. Делегаты, события и лямбда-выражения
419
public delegate string VerySimpleDelegate ();
вот как можно обработать результат вызова:
// Вывод на консоль строки "Enjoy your s t r in g !" .
VerySimpleDelegate d =
new VerySimpleDelegate ( () => {return "Enjoy your string!";} );
Console.WriteLine(d.Invoke () );
Исходный код. Проект LambdaExpressionsMultipleParams доступен в подкаталоге
Chapter 11.
Усовершенствование примера P r im A n d P r o p e r C a r E v e n t s
за счет использования лямбда-выражений
Учитывая то, что главное предназначение лямбда-выражений состоит в обеспече­
нии возможности в чистой, сжатой манере определить анонимный метод (и тем самым
упростить работу с делегатами), давайте переделаем проект PrimAndProperCarEvents,
созданный ранее в этой главе. Ниже приведена упрощенная версия класса Program
этого проекта, в которой используется синтаксис лямбда-выражений (вместо простых
делегатов) для перехвата всех событий, поступающих от объекта Саг.
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** More Fun with Lambdas *****\n");
// Создание объекта Car обычным образом.
Car cl = new Car ("SlugBug", 100, 10);
// Использование лямбда-выражений.
cl.AboutToBlow += (sender, e) => { Console.WriteLine(e.msg); };
cl.Exploded += (sender, e) => { Console.WriteLine (e.msg); };
// Ускорим (это инициирует события).
Console.WriteLine("\n***** Speeding up *****");
for (int i = 0 ; i < 6 ; i++ )
cl.Accelerate(20);
Console.ReadLine();
}
}
Теперь общая роль лямбда-выражений должна проясниться, и становится понятно,
что они обеспечивают “функциональную манеру” работы с анонимными методами и ти­
пами делегатов. К новой лямбда-операции (=>) необходимо привыкнуть, однако помни­
те, что любые лямбда-выражения сводятся к следующему простому уравнению:
АргументыДляОбработки => ОбрабатывающиеИхОператоры
Исходный код. Проект CarEventsWithLambdas доступен в подкаталоге Chapter 11.
Резюме
В этой главе вы ознакомились с несколькими способами двустороннего взаимодейст­
вия множества объектов. Во-первых, было рассмотрено ключевое слово delegate, исполь­
зуемое для неявного конструирования класса-наследника System.MulticastDelegate.
420
Часть III. Дополнительные конструкции программирования на C#
Как было показано, объект делегата поддерживает список методов для вызова тогда,
когда ему об этом укажут. Такие вызовы могут выполняться синхронно (с использова­
нием метода Invoke ()) или асинхронно (через методы Be gin Invoke () и Endlnvoke ()).
Асинхронная природа типов делегатов .NET будет рассмотрена в главе 19.
Во-вторых, вы ознакомились с ключевым словом event, которое в сочетании с ти­
пом делегата может упростить процесс отправки уведомлений о событиях ожидающим
объектам. Как видно в результирующем коде CIL, модель событий .NET отображается
на скрытые обращения к типам System.Delegate/System.MulticastDelegate. В этом
свете ключевое слово event является необязательным и просто позволяет сэкономить
на наборе кода.
В главе также рассматривалось средство языка С#, которое называется анонимными
методами. С помощью этой синтаксической конструкции можно явно ассоциировать
блок операторов кода с заданным событием. Как было показано, анонимные методы
могут игнорировать параметры, переданные событием, и имеют доступ к “внешним пе­
ременным” определяющего их метода. Вы также ознакомились с упрощенным способом
регистрации событий с применением групповых преобразований методов.
И, наконец, в завершение главы было дано описание лямбда-операции => в С#. Как
было показано, этот синтаксис значительно сокращает нотацию написания анонимных
методов, когда набор аргументов может быть передан на обработку группе операторов.
ГЛАВА 1 2
Расширенные
средства языка C#
этой главе рассматриваются некоторые более сложные синтаксические конст­
рукции языка программирования С#. Сначала будет показано, как реализуется
и используется метод-индексатор. Этот механизм C# позволяет строить специальные
типы, обеспечивающие доступ к внутренним подтипам с применением синтаксиса, по­
хожего на синтаксис массивов. Затем вы узнаете о том, как перегружать различные
операции (+, -, <, > и т.д.) и как создавать специальные процедуры явного и неявного
преобразования типов (а также причины, по которым это может понадобиться).
Далее рассматриваются три темы, которые особенно полезны при работе с APIинтерфейсами LINQ (хотя это применимо и вне контекста LINQ), а именно: расширяю­
щие методы, частичные методы и анонимные типы.
И в завершение вы узнаете, как создавать контекст “небезопасного” кода, чтобы
напрямую манипулировать неуправляемыми указателями. Хотя использовать указа­
тели в приложениях C# приходится исключительно редко, понимание того, как это
делается, может пригодиться в определенных ситуациях со сложными сценариями
взаимодействия.
В
Понятие методов-индексаторов
Программисты хорошо знакомы с процессом доступа к индивидуальным элементам,
содержащимся в стандартных массивах, через операцию индекса ([ ]). Например:
static void Main(string [] args)
{
// Цикл no аргументам командной строки с использованием операции индекса.
for(int i = 0 ; i < args.Length; i++)
Console.WnteLine ("Args : {0}", args [l] ) ;
// Объявление массива локальных целых.
int [] mylnts = { 10, 9, 100, 432, 9874};
// Использование операции индекса для доступа к элементам.
for (int з = 0 ; j < mylnts.Length; j++)
Console .WnteLine (" Index {0} = {1} ", 3 , mylnts [j]);
Console.ReadLine();
}
Приведенный код не должен быть для вас чем-то новым. В C# имеется возможность
проектировать специальные классы и структуры, которые могут быть индексированы
подобно стандартному массиву, посредством определения метода-индексатора. Это
422
Часть III. Дополнительные конструкции программирования на C#
конкретное языковое средство наиболее полезно при создании специальных типов кол­
лекций (обобщенных и необобщенных).
Прежде чем ознакомиться с реализацией специального индексатора, начнем с рас­
смотрения его в действии. Предположим, что вы добавили поддержку метода-индекса­
тора к пользовательскому типу PeopleCollection, разработанному в главе 10 (в про­
екте CustomNonGenericCollection). Рассмотрим следующее его применение в новом
консольном приложении по имени Simple Indexer:
// Индексаторы позволяют обращаться к элементам в стиле массива.
class Program
{
static void Main(string[] args)
{
Console.WriteLine ("***** Fun with Indexers *****\n");
PeopleCollection myPeople = new PeopleCollection ();
// Добавление
myPeople[0] =
myPeople[1] =
myPeople[2] =
myPeople[3] =
myPeople [4] =
объектов с помощью синтаксиса индексатора.
new Person("Homer", "Simpson", 40);
new Person("Marge", "Simpson", 38);
new Person("Lisa", "Simpson", 9);
new Person("Bart", "Simpson", 7);
new Person("Maggie", "Simpson", 2);
// Получение и вывод на консоль элементов с использованием индексатора.
for (int i = 0; i < myPeople.Count; i++)
{
Console.WriteLine("Person number: {0}", i);
Console.WriteLine("Name: {0} {1}",
myPeople [i] .FirstName, myPeople[i] .LastName);
Console.WriteLine("Age: {0}", myPeople[i].Age);
Console.WriteLine();
}
Как видите, в отношении доступа к подэлементам контейнера индексаторы ведут
себя подобно специальным коллекциям, поддерживающим интерфейсы IEnumerator и
IEnumerable (либо их обобщенные версии). Главное отличие, конечно, в том, что вместо
доступа к содержимому с использованием конструкции fоreach можно манипулировать
внутренней коллекцией подобъектов подобно стандартному массиву.
Но тут возникает серьезный вопрос: как сконфигурировать класс PeopleCollection
(или любой другой класс либо структуру) для поддержки этой функциональности?
Индексатор представляет собой несколько видоизмененное определение свойства. В его
простейшей форме индексатор создается с использованием синтаксиса this [ ]. Ниже
показано необходимое изменение класса PeopleCollection из главы 10:
// Добавим индексатор к существующему определению класса.
public class PeopleCollection : IEnumerable
{
private ArrayList arPeople = new ArrayListO ;
// Специальный индексатор для этого класса.
public Person this[int index]
{
get { return (Person)arPeople[index]; }
set { arPeople.Insert(index, value); }
Глава 12. Расширенные средства языка C#
423
Помимо использования ключевого слова this, индексатор выглядит как объявление
любого другого свойства С#. Например, роль конструкции get состоит в возврате кор­
ректного объекта вызывающему коду. Здесь мы фактически и делаем это, делегируя за­
прос к индексатору объекта ArrayList. В противоположность этому, конструкция set
отвечает за размещение входящего объекта в контейнере по определенному индексу; в
данном примере это достигается вызовом метода Insert () объекта ArrayList.
Как видите, индексаторы — это просто еще одна форма синтаксиса, учитывая, что
та же функциональность может быть обеспечена с использованием “нормальных” обще­
доступных методов вроде AddPerson () или Get Ре г son ( ) . Тем не менее, поддержка методов-индексаторов в специальных типах коллекций позволяет их легко интегрировать
с библиотеками базовых классов .NET.
Хотя создание методов-индексаторов — обычное дело при построении специальных
коллекций, следует помнить, что обобщенные типы предлагают эту функциональность
в готовом виде. В следующем методе используется обобщенный список List<T> объ­
ектов Person. Обратите внимание, что индексатор List<T> можно просто применять
непосредственно.
static void UseGenencListOf People ()
{
List<Person> myPeople = new List<Person>();
myPeople.Add(new Person("Lisa", "Simpson", 9));
myPeople.Add(new Person ("Bart", "Simpson", 7)) ;
/ / З а м е н и м п е р в у ю п е р с о н у с пом ощ ью и н д е к с а т о р а .
myPeople[0] = new Person("Maggie", "Simpson", 2);
/ / Т е п е р ь п о л у ч и м и о т о б р а з и м каж ды й э л е м е н т ч е р е з и н д е к с а т о р .
for (int i = 0; i < myPeople.Count; i++)
{
Console.WriteLine ("Person number: {0}", l);
Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName,
myPeople[i].LastName);
Console.WriteLine("Age: {0}", myPeople[l].Age);
Console.WriteLine();
}
Исходный код. Проект Simplelndexer доступен в подкаталоге Chapter 12.
Индексация данных с использованием строковых значений
В текущем классе PeopleCollection определен индексатор, позволяющий вызываю­
щему коду идентифицировать подэлементы с применением числовых значений. Однако
надо понимать, что это не обязательное требование метода-индексатора. Предположим,
что решено хранить объекты Person, используя System.Collections. Generic.
Dictionary<TKey, TValue> вместо ArrayList. Учитывая, что типы ListDictionary
позволяют производить доступ к содержащимся в них типам с использованием стро­
кового маркера (такого как фамилия персоны), индексатор можно было бы определить
следующим образом:
public class PeopleCollection : IEnumerable
{
private Dictionary<string, Person> listPeople =
new Dictionary<string, Person>();
424
Часть III. Дополнительные конструкции программирования на C#
// Этот индексатор возвращает персону по строковому индексу,
public Person this[string name]
{
get { return (Person)listPeople[name]; }
set { listPeople[name] = value; }
}
public void ClearPeople ()
{ listPeople.Clear (); }
public int Count
{ get { return listPeople.Count; } }
IEnumerator IEnumerable.GetEnumerator()
{ return listPeople.GetEnumerator(); }
}
Теперь вызывающий код может взаимодействовать с содержащимися внутри объек­
тами Person, как показано ниже:
static void Main(string [] args)
{
Console .WnteLine (''***** Fun with Indexers *****\n");
PeopleCollection myPeople = new PeopleCollection();
myPeople["Homer"] = new Person("Homer", "Simpson", 40);
myPeople["Marge"] = new Person("Marge", "Simpson", 38);
// Получит "Homer" и вывести данные на консоль.
Person homer = myPeople["Homer" ];
Console.WriteLine(homer.ToString() ) ;
Console.ReadLine();
}
Опять-таки, если использовать обобщенный тип Dictionary<TKey, TValue> напря­
мую, получится функциональность метода-индексатора в готовом виде, без построения
специального необобщенного класса, поддерживающего строковый индексатор.
Исходный код. Проект Stringlndexer доступен в подкаталоге Chapter 12.
Перегрузка методов-индексаторов
Имейте в виду, что методы-индексаторы могут быть перегружены в отдельном клас­
се или структуре. То есть если имеет смысл позволить вызывающему коду обращаться к
подэлементам с использованием числового индекса или строкового значения, в одном и
том же типе можно определить несколько индексаторов. Например, если вы когда-либо
программировали с применением ADO.NET (встроенный API-интерфейс .NET для дос­
тупа к базам данных), то вспомните, что тип DataSet поддерживает свойство по имени
Tables, которое возвращает строго типизированную коллекцию DataTableCollection.
В свою очередь, в DataTableCollection определены три индексатора для получения
объектов DataTable — по порядковому номеру, по дружественным строковым именам
и необязательному пространству имен:
public sealed class DataTableCollection : InternalDataCollectionBase
{
// Перегруженные
public DataTable
public DataTable
public DataTable
индексаторы.
this[string name] { get; }
this[string name, string tableNamespace] { get; }
this[int index] { get; }
Глава 12. Расширенные средства языка C#
425
Следует отметить, что множество типов из библиотек базовых классов поддержи­
вают методы-индексаторы. Поэтому даже если текущий проект не требует построения
специальных индексаторов для классов и структур, помните, что многие типы уже под­
держивают этот, синтаксис.
Многомерные индексаторы
Можно также создавать метод-индексатор, принимающий несколько параметров.
Предположим, что имеется специальная коллекция, хранящая подэлементы двумер­
ного массива. В этом случае метод-индексатор можно сконфигурировать следующим
образом:
public class SomeContainer
{
private int[,] my2DintArray = new int [10, 10];
public int this[int row, int column]
{ /* установить или получить значение из двумерного массива */ ]
Если только не строится очень специализированный класс коллекций, то вряд ли по­
надобится создавать многомерные индексаторы. Здесь снова пример ADO.NET показы­
вает, насколько полезной может быть эта конструкция. Класс DataTable в ADO.NET —
это, по сути, коллекция строк и столбцов, похожая на распечатанную таблицу или
электронную таблицу Microsoft Excel.
Хотя объекты DataTable обычно наполняются без вашего участия, посредством
связанных с ними “адаптеров данных”, в приведенном ниже коде показано, как вруч­
ную создать находящийся в памяти объект DataTable, содержащий три столбца (для
имени, фамилии и возраста). Обратите внимание, что после добавления одной строки
в DataTable с помощью многомерного индексатора производится обращение к всем
столбцам первой (и единственной) строки. (Чтобы реализовать это, в файл кода понадо­
бится импортировать пространство имен System.Data.)
static void MultilndexerWithDataTable ()
{
// Создать простой объект DataTable с тремя столбцами.
DataTable myTable = new DataTable ();
myTable.Columns.Add(new DataColumn("FirstName"));
myTable.Columns.Add(new DataColumn("LastName"));
myTable.Columns.Add(new DataColumn("Age"));
// Добавить строку к таблице.
myTable.Rows.Ad d ("Mel", "Appleby", 60);
// Использовать многомерный индексатор для вывода деталей первой строки.
Console.WriteLine("First Name: {0]", myTable.Rows[0][0]);
Console .WnteLine ("Last Name : {0]", myTable.Rows[0][1]);
Console.WriteLine("Age : {0]", myTable.Rows[0][2]);
}
Начиная с главы 21, мы продолжим рассмотрение ADO.NET, так что не пугайтесь,
если что что-то в приведенном выше коде покажется незнакомым. Этот пример просто
иллюстрирует, что методы-индексаторы могут поддерживать множество измерений, а
при правильном использовании могут упростить взаимодействие с подобъектами, со­
держащимися в специальных коллекциях.
Определения индексаторов в интерфейсных типах
Индексаторы могут определяться в типе интерфейса, позволяя поддерживающим
типам предоставлять их специальные реализации.
426
Часть III. Дополнительные конструкции программирования на C#
Ниже показан пример такого интерфейса, который определяет протокол для получе­
ния строковых объектов с использованием числового индексатора:
public interface IStringContainer
{
// Этот интерфейс определяет индексатор, возвращающий
// строки по числовому индексу,
string this[int index] { get; set; }
}
При таком определении интерфейса любой класс или структура, реализующие его,
должны поддерживать индексатор чтения/записи, манипулирующий подэлементами
через числовое значение.
На этом первая главная тема настоящей главы завершена. Хотя понимание синтак­
сиса индексаторов C# важно, как объяснялось в главе 10, обычно единственным слу­
чаем, когда программисту нужно строить специальный обобщенный класс коллекции,
является ситуация, когда необходимо добавить ограничения к параметрам-типам. Если
придется строить такой класс, добавление специального индексатора может заставить
класс коллекции выглядеть и вести себя подобно стандартному классу коллекции из
библиотеки базовых классов .NET.
А теперь давайте рассмотрим языковое средство, позволяющее строить специальные
классы и структуры, которые уникальным образом реагируют на встроенные операции
C# — перегрузку операций.
Понятие перегрузки операций
В С#, подобно любому языку программирования, имеется готовый набор лексем, ис­
пользуемых для выполнения базовых операций над встроенными типами. Например,
известно, что операция + может применяться к двум целым, чтобы дать их сумму:
// Операция + с целыми.
int а = 10 0 ;
int Ь = 240;
int с = а + b;
/ / с теперь равно 340
Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и
та же операция + может применяться к большинству встроенных типов данных С#?
Например, рассмотрим такой код:
// Операция
string si =
string s2 =
string s3 =
+ со строками.
"Hello";
" world!";
si + s2;
// s3 теперь содержит "Hello world!"
По сути, функциональность операции + уникальным образом базируются на пред­
ставленных типах данных (строках или целых в данном случае). Когда операция + при­
меняется к числовым типам, мы получаем арифметическую сумму операндов. Однако
когда та же операция применяется к строковым типам, получается конкатенация
строк.
Язык C# предоставляет возможность строить специальные классы и структуры, ко­
торые также уникально реагируют на один и тот же набор базовых лексем (вроде опе­
рации +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать
нельзя. В табл. 12.1 описаны возможности перегрузки основных операций.
Глава 12. Расширенные средства языка C#
427
Таблица 12.1. Возможности перегрузки операций C#
Возможность перегрузки
Операция C#
+,
, ~,
!
++,
Этот набор унарных операций может быть перегружен
— , tru e,
fa ls e
+,
* , /, %, &, I , A , « ,
>>
Эти бинарные операции могут быть перегружены
и
A
if
V
a"
if
v"
if
II
Эти операции сравнения могут быть перегружены. C# требует
совместной перегрузки “подобных” операций (т.е. < и >, < = и > = ,
= = и != )
[]
Операция [ ] не может быть перегружена. Как было показано ра­
нее в этой главе, однако, аналогичную функциональность пред­
лагают индексаторы
0
Операция ( ) не может быть перегружена. Однако, как будет по­
казано далее в этой главе, ту же функциональность предоставля­
ют специальные методы преобразования
+ = , - = , * = , / = , %=, &= ,
A=, « = ,
>>=
1= ,
Сокращенные операции присваивания не могут перегружаться;
однако вы получаете их автоматически, перегружая соответст­
вующую бинарную операцию
Перегрузка бинарных операций
Чтобы проиллюстрировать процесс перегрузки бинарных операций, представим сле­
дующий простой класс Point, определенный в новом консольном приложении по имени
OverloadedOps:
// Простой класс C# для повседневного пользования,
public class Point
{
public int X {get; set; }
public int Y {get; set; }
public Point (int xPos, int yPos)
{
X = xPos;
Y = yPos;
}
public override string ToStringO
{
return string.Format("[{0}, {1}]", this.X, this.Y);
Теперь, логически рассуждая, имеет смысл складывать экземпляры P o in t вместе.
Например, если сложить вместе две переменных Poin t, получится новая P o in t с суммар­
ными значениями х и у. Кстати, также может быть полезно иметь возможность вычитать
одну P o in t из другой. В идеале пригодилась бы возможность написать такой код:
// Сложение и вычитание двух точек?
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Overloaded Operators *****\n");
// Создать две точки.
Point ptOne = new Point (100, 100);
Point ptTwo = new Point (40, 40);
Console.WriteLine("ptOne = {0}", ptOne);
Console.WriteLine("ptTwo = {0}", ptTwo);
428
Часть III. Дополнительные конструкции программирования на C#
// Сложить две точки, чтобы получить большую?
Console.WriteLine("ptOne + ptTwo: {0} ", ptOne + ptTwo);
// Вычесть одну точку из другой, чтобы получить меньшую?
Console.WriteLine("ptOne - ptTwo: {0} ", ptOne - ptTwo);
Console.ReadLine();
}
Однако в том виде, как он есть, класс P o in t приведет к ошибкам этапа компиляции,
поскольку типу P o in t не известно, как реагировать на операции + и
Чтобы оснастить специальный тип возможностью уникально реагировать на встро­
енные операции, в C# служит ключевое слово o p e ra to r, которое может использовать­
ся только в сочетании со статическими методами. При перегрузке бинарной операции
(вроде + и -) чаще всего передаются два аргумента того же типа, что и определяющий
их класс (в данном примере — P oin t); это иллюстрируется в следующей модификации
кода:
// Более интеллектуальный тип Point.
public class Point
// Перегруженная операция +
public static Point operator + (Point pi, Point p2)
{ return new Point(pi.X + p2.X, pi.Y + p2.Y); }
// Перегруженная операция -
public static Point operator - (Point pi, Point p2)
{ return new Point(pl.X - p2.X, pl.Y - p2.Y); }
}
Логика, положенная в основу операции +, состоит просто в возврате нового экзем­
пляра P o in t на основе сложения соответствующих полей входных параметров P o in t.
Поэтому, когда вы напишете p t l + pt2, “за кулисами” произойдет следующий скрытый
вызов статического метода o p era to r+ :
// Псевдокод: Point рЗ = Point.operator+ (pi, р2)
Point рЗ = pi + р2;
Аналогично, p i - р2 отображается на следующее:
// Псевдокод: Point р4 = Point.operator- (pi, р2)
Point р4 = pi - р2;
После этого дополнения программа скомпилируется, и мы получим возможность
складывать и вычитать объекты P o in t:
ptOne
ptTwo
ptOne
ptOne
=
=
+
-
[100, 100]
[40, 40]
ptTwo: [140, 140]
ptTwo: [60, 60]
При перегрузке бинарной операции вы не обязаны передавать ей два параметра оди­
накового типа. Если это имеет смысл, один из аргументов может отличаться. Например,
ниже показана перегруженная операция +, которая позволяет вызывающему коду полу­
чить новый объект P o in t на основе числового смещения:
public class Point
public static Point operator + (Point pi, int change)
{
return new Point(pi.X + change, pl.Y + change);
}
Глава 12. Расширенные средства языка C#
429
public static Point operator + (int change, Point pi)
{
return new Point(pl.X + change, pl.Y + change);
}
}
Обратите внимание, что если нужно передавать аргументы в любом порядке, по­
требуются обе версии метода (т.е. нельзя просто определить один из методов и рассчи­
тывать, что компилятор автоматически будет поддерживать другой). Теперь можно ис­
пользовать эти новые версии операции + следующим образом:
// Выводит [110, 110]
Point biggerPoint = ptOne + 10;
Console .WnteLine ("ptOne + 10 = {0}", biggerPoint);
// Выводит [120, 120]
Console. WnteLine ("10 + biggerPoint = {0]", 10 + biggerPoint);
Console.WriteLine ();
А как насчет операций += и -= ?
Перешедших на C# с языка C++ может удивить отсутствие возможности перегрузки
операций сокращенного присваивания (+=, -+ и т.д.). Не беспокойтесь. В терминах C#
операции сокращенного присваивания автоматически эмулируются при перегрузке со­
ответствующих бинарных операций. Таким образом, если в классе P o in t уже перегру­
жены операции + и -, можно написать следующий код:
// Перегрузка бинарных операций автоматически обеспечивает
// перегрузку сокращенных операций.
static void Main (string[] args)
// Операция += автоматически перегружена
Point ptThree = new Point (90, 5) ;
Console .WnteLine ("ptThree = {0}", ptThree);
Console.WriteLine("ptThree += ptTwo: {0}", ptThree += ptTwo);
// Операция -= автоматически перегружена
Point ptFour = new Point (0, 500);
Console.WriteLine("ptFour = {0}", ptFour);
Console.WriteLine ("ptFour -= ptThree: {0}", ptFour -= ptThree);
Console.ReadLine();
}
Перегрузка унарных операций
В C# также допускается перегружать и унарные операции, такие как ++ и — . При
перегрузке унарной операции также определяется статический метод через ключевое
слово o p e ra to r, однако в этом случае просто передается единственный параметр того
же типа, что и определяющий его класс/структура. Например, дополните P o in t сле­
дующими перегруженными операциями:
public struct Point
// Прибавить 1 к значениям X/Y входного объекта Point,
public static Point operator + + (Point pi)
{ return new Point(pi.X+l, pl.Y+1); }
// Вычесть 1 из значений X/Y входного объекта Point,
public static Point operator — (Point pi)
{ return new Point(pi.X-l, pl.Y-1); }
430
Часть III. Дополнительные конструкции программирования на C#
В результате появляется возможность выполнять инкремент и декремент значений
X и Y класса Point, как показано ниже:
static void Main(string [] args)
// Применение унарных операций ++ и — к Point.
Point ptFive = new Point (1, 1);
Console.WriteLine("++ptFive = {0}", ++ptFive);
// [2, 2]
Console.WriteLine("--ptFive = {0}", — ptFive);
// [1, 1]
// Применение тех же операций для постфиксного инкремента/декремента.
Point ptSix = new Point (20, 20);
Console.WriteLine("ptSix++ = {0}", ptSix++);
// [20, 20]
Console .WriteLine ("ptSix— = {0}", ptSix— );
// [21, 21]
Console.ReadLine();
}
В предыдущем примере кода обратите внимание, что специальные операции ++ и
— применяются двумя разными способами. В C++ допускается перегружать операции
префиксного и постфиксного инкремента/декремента по отдельности. В C# это невоз­
можно; тем не менее, возвращаемое значение инкремента/декремента автоматически
обрабатывается правильно (т.е., для перегруженной операции ++ выражение pt++ имеет
значение ^модифицированного объекта, в то время как ++pt имеет новое значение,
примененное перед использованием выражения).
Перегрузка операций эквивалентности
Как вы должны помнить из главы 6, метод System.Object .Equals () может быть
перегружен для выполнения сравнений объектов на основе значений (а не ссы­
лок). Если вы решите переопределить Equals () (часто вместе со связанным методом
System. Object.GetHashCode ()), это позволит легко переопределить и операции про­
верки эквивалентности (== и ! =). Для иллюстрации рассмотрим модифицированное оп­
ределение типа Point:
// Этот вариант Point также перегружает операции == и '=.
public class Point
public override bool Equals(object o)
{
return o . T o S t n n g O == this.ToString();
}
public override int GetHashCode ()
{ return this.ToString().GetHashCode(); }
// Теперь перегрузим операции == и !=.
public static bool operator == (Point pi, Point p2)
{ return p i .Equals(p2 ); }
public static bool operator != (Point pi, Point p2)
{ return 1p i .Equals(p2 ); }
}
Обратите внимание, что для выполнения нужной работы реализации операций ==
и ! = просто вызывают перегруженный метод Equals () . Теперь класс Point можно ис­
пользовать следующим образом:
// Использование перегруженных операций эквивалентности,
static void Main(string [] args)
Console.WriteLine("ptOne == ptTwo : {0}", ptOne == ptTwo);
Глава 12. Расширенные средства языка C#
431
Console.WriteLine("ptOne != ptTwo : {0}", ptOne != ptTwo);
Console.ReadLine();
}
Как видите, сравнение двух объектов с применением хорошо знакомых операций ==
и != выглядит намного интуитивно понятней, чем вызов Object .Equals ( ) . При пере­
грузке операций эквивалентности для определенного класса помните, что C# требует,
чтобы в случае перегрузки операции == обязательно перегружалась также и операция
! = (компилятор напомнит, если вы забудете это сделать).
Перегрузка операций сравнения
В главе 9 было показано, как реализовать интерфейс I Comparable для выполнения
сравнений двух сходных объектов. Вдобавок для того же класса можно перегрузить опе­
рации сравнения (<, >, <= и >=). Подобно операциям эквивалентности, C# и здесь требует,
чтобы в случае перегрузки операции < обязательно перегружалась также и операция >.
После перегрузки в классе P o in t этих операций сравнения пользователь объекта смо­
жет сравнивать объекты P o in t следующим образом:
// Использование перегруженных операций < и >.
static void Main(string [] args)
Console.WriteLine ("ptOne < ptTwo : {0}", ptOne < ptTwo);
Console.WriteLine ("ptOne > ptTwo : {0}", ptOne > ptTwo);
Console.ReadLine();
}
Предполагая, что интерфейс IComp a rable уже реализован, перегрузка операций
сравнения становится тривиальной. Ниже показано модифицированное определение
класса.
// Объекты Point также можно сравнивать с помощью операций сравнения.
public class Point : IComparable
public int CompareTo(object obj)
{
if (obj is Point)
{
Point p = (Point)obj;
if (this.X > p.X && this.Y > p.Y)
return 1;
if (this .X < p.X && this.Y < p.Y)
return -1;
else
return 0;
}
else
throw new ArgumentException ();
}
public static bool operator < (Point pi, Point p2)
{ return (pi.CompareTo (p2) < 0) ; }
public static bool operator > (Point pi, Point p2)
{ return (pi.CompareTo(p2) >0); }
public static bool operator <=(Point pi, Point p2)
{ return (pi.CompareTo(p2) <= 0); }
public static bool operator >=(Point pi, Point p2)
{ return (pi.CompareTo (p2) >= 0) ; }
}
432
Часть III. Дополнительные конструкции программирования на C#
Внутреннее представление перегруженных операций
Подобно любому программному элементу С#, перегруженные операции имеют спе­
циальное представление в синтаксисе CIL. Чтобы начать исследование того, что проис­
ходит “за кулисами”, откройте сборку OverloadedOps .ехе в утилите ildasm.exe. Как
видно на рис. 12.1, перегруженные операции внутренне представлены в виде скрытых
методов (op_Addition (), op_Subtraction (), op_Equality () и т.д.).
Рис. 12.1. В терминах CIL перегруженные операции отображаются на скрытые методы
Если посмотреть на инструкции CIl[ для метода op Addition (того, что принимает
два параметра Point), легко заметить, что компилятор также вставил модификатор ме­
тода specialname:
.method public hidebysig specialname static
class OverloadedOps.Point
op_Addition(class OverloadedsOps.Point pi,
class OverloadedOps.Point p2) cil managed
В действительности любая операция, которую можно перегрузить, превращается в
специально именованный метод в коде CIL. В табл. 12.2 приведены отображения на CIL
для наиболее распространенных операций С#.
Таблица 12.2. Отображение операций C# на специально именованные методы СИ
Встроенная операция C#
Представление CIL
—
op Decrement()
++
op Increment()
+
op Addition()
*
op Subtraction()
/
==
op Division()
>
op GreaterThan()
<
op LessThan()
op Multiply()
op Equality()
Глава 12. Расширенные средства языка C#
433
Окончание табл. 12.2
Встроенная операция C#
Представление CIL
i—
op Inequality()
>=
op GreaterThanOrEqual()
<=
op LessThanOrEqual()
-=
op SubtractionAssignment()
+=
op AdditionAssignment()
Финальные соображения относительно перегрузки операций
Как уже было показано, C# предлагает возможность строить типы, которые могут
уникальным образом реагировать на различные встроенные, хорошо известные опера­
ции. Теперь перед добавлением поддержки этого поведения в классы необходимо убе­
диться в том, что операции, которые вы собираетесь перегружать, имеют хоть какой-то
смысл в реальном мире.
Например, предположим, что перегружена операция умножения для класса MiniVan
(минивэн). Что вообще должно означать перемножение двух объектов MiniVan? Не
слишком много. Фактически, если коллеги по команде увидят следующее использова­
ние объектов MiniVan, то будут весьма озадачены:
// Это не слишком понятно. ..
MiniVan newVan = myVan * yourVan;
Перегрузка операций обычно полезна только при построении служебных типов.
Строки, точки, прямоугольники, функции и шестиугольники — подходящие кандидаты
на перегрузку операций. Люди, менеджеры, автомобили, подключения к базе данных
и веб-страницы — нет. В качестве эмпирического правила: если перегруженная опера­
ция затрудняет пользователю понимание функциональности типа, не делайте этого.
Используйте это средство с умом.
Также имейте в виду, что даже если вы не хотите перегружать операции в специаль­
ных классах, это уже сделано в многочисленных типах из библиотек базовых классов.
Например, сборка System.Drawing.dll предлагает определение Point, применяемое
в Windows Forms, в котором перегружено множество операций. Обратите внимание на
значок операции в браузере объектов Visual Studio 2010 (рис. 12.2).
B row s e
.NET Fram ew ork 4
;<Search>
■
= 3 ® '
,,
Im a g e G e tT h u m b n aillm ag eA b o rl •
Im a g e A m m a to r
K now nC olor
+
Г X Derived Types
R ectangle
==(System Draw ing.Poin t, S ystem .D raw ing.P ointj
public static S y s te m -D ra w in g ,P o in t o p e r a to r
(S ystem . D ra w in g ,P o in t p r, S y s te m ,D ra w in g ,S iz e sz)
Member o f S ys te m -D ra w in g .P o in t
Point
’[^|Base Types
P om tC onverter
[~j
k E m p ty
Pens
Pom tF
ш
im p lic it operator(System D raw ing Point)
Г 5 o p erato r
Pen
^
!>
m
o p erato r -(S ystem Draw ing Point, S yste m D ra w in g S iz e )
Im a g e F o rm atC o n verter
>
m
,
exp Ik rt oper ator(Systcm . D raw in g Point)
o p erato r l=(S ys tem .D raw in g .P o in t System D raw in g .P o in t)
Im ageC orv. erter
Л
- ‘s e t . ..
Truncate(S ys tem .D raw ing Pom tF)
^
1-
*
.1
1
S u m m a ry :
Translates a System Drawing.Point by a given System.Drawing.Size
Рис. 12.2. Множество типов в библиотеках базовых классов включают
уже перегруженные операции
434
Часть III. Дополнительные конструкции программирования на C#
Исходный код. Проект O verloadedO ps доступен в подкаталоге Chapter 12.
Понятие преобразований пользовательских типов
Теперь обратимся к теме, близкой к перегрузке операций — преобразованию поль­
зовательских типов. Чтобы заложить фундамент для последующей дискуссии, давайте
кратко опишем нотацию явного и неявного преобразования между числовыми данными
и связанными с ними типами классов.
Числовые преобразования
В терминах встроенных числовых типов (s b y te , in t , f l o a t и т.п.) явное преобра­
зование требуется при попытке сохранить большее значение в меньшем контейнере,
поскольку это может привести к потере данных. По сути, это означает, что вы говорите
компилятору: “Я знаю, что делаю”. В противоположность этому неявное преобразование
происходит автоматически, когда вы пытаетесь поместить меньший тип в целевой тип,
и в результате этой операции не происходит потеря данных:
static void Main()
{
int a = 123;
long b = a;
int c = (int) b;
// Неявное преобразование int в long
// Явное преобразование long в int
Преобразования между связанными типами классов
Как было показано в главе 6, типы классов могут быть связаны классическим на­
следованием (отношение “является” (“is а”)). В этом случае процесс преобразования C#
позволяет выполнять приведение вверх и вниз по иерархии классов. Например, класснаследник всегда может быть неявно приведен к базовому типу. Однако если необхо­
димо хранить тип базового класса в переменной типа класса-наследника, понадобится
явное приведение:
// Два связанных типа классов,
class Base{}
class Derived : Base{}
class Program
{
static void Main(string [] args)
{
// Неявное приведение наследника к предку.
Base myBaseType;
myBaseType = new Derived ();
// Для хранения базовой ссылке в ссылке
//на наследника нужно юное преобразование.
Derived myDerivedType = (Derived)myBaseType;
}
}
Это явное приведение работает благодаря тому факту, что классы Base и D erived
связаны отношением классического наследования. Однако что если есть два типа клас­
сов из разных иерархий без общего предка (кроме S ystem .O b ject), которые требуют
преобразования друг в друга? Если они не связаны классическим наследованием, явное
приведение здесь не поможет.
Глава 12. Расширенные средства языка C#
435
Рассмотрим типы значений, такие как структуры. Предположим, что определены
две структуры .NET с именами Square и Rectangle. Учитывая, что они не могут пола­
гаться на классическое наследование (поскольку всегда запечатаны), нет естественного
способа выполнить приведение между этими, на первый взгляд, связанными типами.
Наряду с возможностью создания в структурах вспомогательных методов (вроде
Rectangle.ToSquare О ), язык C# позволяет строить специальные процедуры преоб­
разования, которые позволят типам реагировать на операцию приведения ( ) . Таким
образом, если корректно сконфигурировать эти структуры, можно будет использовать
следующий синтаксис для явного преобразования между ними:
// Преобразовать Rectangle в Square!
Rectangle rect;
rect.Width = 3;
rect.Height = 1 0 ;
Square sq = (Square)rect;
Создание специальных процедур преобразования
Начнем с создания нового консольного приложения по имени CustomC on vers ions.
В C# предусмотрены два ключевых слова — explicit и implicit, которые можно при­
менять для управления реакцией на попытки выполнить преобразования. Предположим,
что имеются следующие определения классов:
public class Rectangle
{
public int Width {get; set;}
public int Height {get; set;}
public Rectangle(int w, int h)
{
Width = w; Height = h;
}
public Rectangle (){}
public void Draw()
{
for (int i = 0; l < Height; i++)
{
for (int j = 0; j < Width; j++)
{
Console.Write("*");
}
Console.WriteLine();
}
public override string ToStringO
{
return string.Format("[Width = {0}; Height = {1}]",
Width, Height);
public class Square
{
public int Length {get; set; }
public Square (int 1)
{
Length = 1 ;
}
public Square () {}
436
Часть III. Дополнительные конструкции программирования на C#
public void Draw()
{
for (int i = 0; l < Length; i++)
{
for (int j = 0; j < Length;
J++)
{
Console.Write ("*");
}
Console.WriteLine();
}
}
public override string ToStringO
{ return string.Format (" [Length = {0}]", Length); }
// Rectangle можно явно преобразовать в Square,
public static explicit operator Square(Rectangle r)
{
Square s = new Square ();
s.Length = r.Height;
return s;
}
Обратите внимание, что эта итерация типа Squire определяет явную операцию
преобразования. Подобно процессу перегрузки операций, процедуры преобразования
используют ключевое слово operator в сочетании с ключевым словом explicit или
implicit и должны быть статическими. Входным параметром является сущность, из
которой выполняется преобразование, в то время как тип операции — сущность, к ко­
торой оно производится.
В этом случае предполагается, что квадрат (геометрическая фигура с четырьмя рав­
ными сторонами) может быть получен из высоты прямоугольника. Таким образом, пре­
образовать Rectangle в Square можно следующим образом:
static void Main(string [] args)
{
Console.WriteLine ("***** Fun with Conversions *****\n");
// Создать Rectangle.
Rectangle r = new Rectangle(15, 4) ;
Console.WriteLine (r.ToString());
r . Draw();
Console.WriteLine();
// Преобразовать г в Square на основе высоты Rectangle.
Square s = (Square)r;
Console.WriteLine(s.ToString());
s . Draw();
Console.ReadLine();
}
Вывод этой программы показан на рис. 12.3.
Хотя, может быть, не слишком полезно преобразовывать Rectangle в Square в пре­
делах одного контекста, предположим, что есть функция, спроектированная так, чтобы
принимать параметров Square:
// Этот метод требует параметр типа Square.
static void DrawSquare(Square sq)
{
Console.WriteLine(sq.ToString ());
sq.Draw();
Глава 12. Расширенные средства языка C#
437
Имея операцию явного преобразования в тип Square, можно передавать типы
Rectangle для обработки этому методу, используя явное приведение:
static void Main(string[] args)
{
// Преобразовать Rectangle в Square для вызова метода.
Rectangle rect = new Rectangle(10, 5);
DrawSquare((Square)rect);
Console.ReadLine();
}
Рис. 12.3. Преобразование Rectangle в Square
Дополнительные явные преобразования типа S q u a r e
Теперь, когда можно явно преобразовывать объекты Rectangle в объекты Square,
давайте рассмотрим несколько дополнительных явных преобразований. Учитывая, что
квадрат симметричен по всем сторонам, может быть полезно предусмотреть процедуру
преобразования, которая позволит вызывающему коду привести целочисленный тип к
типу Square (который, разумеется, будет иметь длину стороны, равную переданному
целому). Аналогично, что если вы захотите модифицировать Square так, чтобы вызы­
вающий код мог выполнять приведение из Square в System. Int32? Логика вызова вы­
глядит следующим образом.
static void Main(string[] args)
{
// Преобразование int в Square.
Square sq2 = (Square)90;
Console.WriteLine("sq2 = {0}", sq2) ;
// Преобразование Square в int.
int side = (int)sq2 ;
Console.WriteLine("Side length of sq2 = {0}", side);
Console.ReadLine();
}
Ниже показаны необходимые изменения в классе Square:
public class Square
{
public static explicit operator Square(int sideLength)
{
438
Часть III. Дополнительные конструкции программирования на C#
Square newSq = new Square();
newSq.Length = sideLength;
return newSq;
}
public static explicit operator int (Square s)
{return s.Length;}
}
По правде говоря, преобразование Square в int может показаться не слишком ин­
туитивно понятной (или полезной) операцией. Однако это указывает на один очень
важный факт, касающийся процедур пользовательских преобразований: компилятор не
волнует, что и куда преобразуется, до тех пор, пока пишется синтаксически коррект­
ный код. Таким образом, как и с перегруженными операциями, возможность создания
операций явного приведения еще не означает, что вы обязаны их создавать. Обычно
эта техника наиболее полезна при создании типов структур .NET, учитывая, что они не
могут участвовать в отношениях классического наследования (где приведение достает­
ся бесплатно).
Определение процедур неявного преобразования
До сих пор вы создавали различные пользовательские операции явного преобразова­
ния. Однако что, если понадобится неявное преобразование?
static void Main(string [] args)
// Попытка выполнить неявное приведение?
Square s3 = new Square ();
s3.Length = 83 ;
Rectangle rect2 = s3;
Console.ReadLine();
Этот код не скомпилируется, если для типа Rectangle не будет предусмотрена про­
цедура неявного преобразования. Ловушка здесь вот в чем: не допускается иметь одно­
временно функции явного и неявного преобразования, если они не отличаются по типу
возвращаемого значения или списку параметров. Это может показаться ограничением,
однако вторая ловушка состоит в том, что когда тип определяет процедуру неявного
преобразования, никто не запретит вызывающему коду использовать синтаксис явного
приведения!
Запутались? Для того чтобы прояснить ситуацию, давайте добавим к классу
Rectangle процедуру неявного преобразования, используя для этого ключевое слово
implicit (обратите внимание, что в следующем коде предполагается, что ширина ре­
зультирующего Rectangle вычисляется умножением стороны Square на 2):
public class Rectangle
public static implicit operator Rectangle(Square s)
{
Rectangle r = new Rectangle ();
r.Height = s.Length;
// Предположим, что длина нового Rectangle
// будет равна (Length х 2)
г.Width = s .Length * 2 ;
return г;
Глава 12. Расширенные средства языка C#
439
После такой модификации можно будет выполнять преобразование между типами:
static void Main(string[] args)
{
// Неявное преобразование работает!
Square s3 = new Square ();
s3.Length = 7;
Rectangle rect2 = s3;
Console.WriteLine("rect2 = {0}", rect2);
DrawSquare(s3);
// Синтаксис явного преобразования также работает!
Square s4 = new Square ();
s4.Length = 3;
Rectangle rect3 = (Rectangle)s4;
Console.WriteLine("rect3 = {0}", rect3);
Console.ReadLine();
•
}
Внутреннее представление процедур
пользовательских преобразований
Подобно перегруженным операциям, методы, квалифицированные ключевыми сло­
вами i m p l i c i t или e x p l i c i t , имеют специальные имена в терминах CIL: op l m p l i c i t
и op E x p l i c i t , соответственно (рис. 12.4).
Рис. 12.4. Представление CIL определяемых пользователем процедур преобразования
На заметку! В браузере объектов Visual Studio 2010 операции пользовательских преобразований
отображаются с использованием значков “ явная операция" и “ неявная операция".
На этом рассмотрение определений операций пользовательского преобразования
завершено. Как и с перегруженными операциями, здесь следует помнить, что данный
фрагмент синтаксиса представляет собой просто сокращенное обозначение “нормаль­
ных” функций-членов, и в этом смысле является необязательным. Однако в случае пра­
вильного применения пользовательские структуры могут использоваться более естест­
венно, поскольку трактуются как настоящие типы классов, связанные наследованием.
Исходный код. Проект Custom Conversions доступен в подкаталоге C h apter 12.
440
Часть III. Дополнительные конструкции программирования на C#
Понятие расширяющих методов
В .NET 3.5 появилась концепция расширяющих методов (extension method), которая
позволила добавлять новую функциональность к предварительно скомпилированным
типам “на лету”. Известно, что как только тип определен и скомпилирован в сборку
.NET, его определение становится более-менее окончательным. Единственный способ
добавления новых членов, обновления или удаления членов состоит в перекодирова­
нии и перекомпиляции кодовой базы в обновленную сборку (или же можно прибегнуть
к более радикальным мерам, таким как использование пространства имен System.
Ref lection.Emit для динамического изменения скомпилированного типа в памяти).
Теперь в C# можно определять расширяющие методы. Суть расширяющих методов
в том, что они позволяют существующим скомпилированным типам (а именно — клас­
сам, структурам или реализациям интерфейсов), а также типам, которые в данный мо­
мент компилируются (такие как типы в проекте, содержащем расширяющие методы),
получать новую функциональность без необходимости в непосредственном изменении
расширяемого типа.
Эта техника может оказаться полезной, когда нужно внедрить новую функциональ­
ность в типы, исходный код которых не доступен. Также она может пригодиться, когда
необходимо заставить тип поддерживать набор членов (в интересах полиморфизма),
но вы не можете модифицировать его исходное объявление. Механизм расширяющих
методов позволяет добавлять функциональность к предварительно скомпилированным
типам, создавая иллюзию, что она была у него всегда.
На заметку! Имейте в виду, что расширяющие методы на самом деле не изменяют скомпили­
рованную кодовую базу! Эта техника лишь добавляет члены к типу в контексте текущего
приложения.
При определении расширяющих методов первое ограничение состоит в том, что
они должны быть определены внутри статического класса (см. главу 5), и потому каж­
дый расширяющий метод должен быть объявлен с ключевым словом static. Второй
момент состоит в том, что все расширяющие методы помечаются таковыми посредст­
вом ключевого слова this в виде модификатора первого (и только первого) параметра
данного метода. Третий момент — каждый расширяющий метод может быть вызван
либо от текущего экземпляра в памяти, либо статически, через определенный стати­
ческий класс! Звучит странно? Давайте рассмотрим полный пример, чтобы прояснить
картину.
О пределение расш иряю щ их методов
Создадим новое консольное приложение по имени ExtensionMethods. Теперь пред­
положим, что строится новый служебный класс по имени MyExtensions, в котором оп­
ределены два расширяющих метода. Первый позволяет любому объекту из библиотек
базовых классов .NET получить новый метод по имени DisplayDef iningAssembly (),
который использует типы из пространства имен System. Ref lection для отображения
сборки указанного типа.
На заметку! API-интерфейс рефлексии формально рассматривается в главе 15. Если эта тема яв­
ляется новой, просто знайте, что рефлексия позволяет исследовать структуру сборок, типов и
членов типов во время выполнения.
Второй расширяющий метод по имени ReverseDigits () позволяет любому экземп­
ляру System. Int32 получить новую версию себя, но с обратным порядком следования
Глава 12. Расширенные средства языка C#
441
цифр. Например, если на целом значении 1234 вызвать ReverseDigits () , возвращен­
ное целое значение будет равно 4321. Взгляните на следующую реализацию класса (не
забудьте импортировать пространство имен System.Reflection):
static class MyExtensions
{
// Этот метод позволяет любому объекту отобразить
// сборку, в которой он определен.
public static void DisplayDefiningAssembly(this object obj)
{
Console.WnteLine ("{ 0 } lives here: => {l}\n", obj.GetType().Name,
Assembly.GetAssembly(obj.GetType()).GetName().Name);
// Этот метод позволяет любому целому изменить порядок следования
// десятичных цифр на обратный. Например, 56 превратится в 65.
public static int ReverseDigits(this int i)
{
// Транслировать int в string и затем получить все его символы.
char[] digits = i .ToStnng () .ToCharArray () ;
// Изменить порядок элементов массива.
Array.Reverse(digits);
// Вставить обратно в строку.
string newDigits = new string(digits);
// Вернуть модифицированную строку как int.
return int.Parse(newDigits);
Обратите внимание, что первый параметр каждого расширяющего метода квали­
фицирован ключевым словом this, перед определением типа параметра. Первый па­
раметр расширяющего метода всегда представляет расширяемый тип. Учитывая, что
DisplayDef iningAssembly () прототипирован расширять System. Object, любой тип в
любой сборке теперь получает этот новый член. Однако ReverseDigits () прототипиро­
ван только для расширения целочисленных типов, и потому если что-то другое попыта­
ется вызвать этот метод, возникнет ошибка времени компиляции.
Знайте, что каждый расширяющий метод может иметь множество параметров, но
только первый параметр может быть квалифицирован как this. Например, вот как вы­
глядит перегруженный расширяющий метод, определенный в другом служебном классе
по имени TestUtilClass:
static class TesterUtilClass
{
// Каждый Int32 теперь имеет метод Foo()...
public static void Foo(this int i)
{ Console.WriteLine ("{0 } called the Foo() method.", l); }
// ...который перегружен для приема параметра string!
public static void Foo(this int i, string msg)
{ Console.WriteLine ("{0 } called Foo() and told me: {1}", l, msg); }
}
Вызов расш иряю щ их методов на уровне экземпляра
После определения этих расширяющих методов теперь все объекты (в том числе,
конечно же, все содержимое библиотек базовых классов .NET) имеют метод по имени
DisplayDef iningAssembly () , в то время как типы System. Int 32 (и только целые) —
методы ReverseDigits () и Foo ():
442
Часть III. Дополнительные конструкции программирования на C#
static void Main(string [] args)
Console .WriteLine (''***** Fun with Extension Methods *****\n");
// В int появилась новая идентичность1
int mylnt = 12345678;
mylnt.DisplayDefiningAssembly();
// To же и у DataSet!
System.Data.DataSet d = new System.Data.DataSet();
d.DisplayDefiningAssembly();
// И у SoundPlayerl
System.Media.SoundPlayer sp = new System.Media.SoundPlayer();
sp.DisplayDefiningAssembly();
// Использовать новую функциональность int.
Console.WriteLine("Value of mylnt: {0}", mylnt);
Console.WriteLine("Reversed digits of mylnt: {0}", mylnt.ReverseDigits()) ;
mylnt.Foo();
mylnt.Foo("Ints that Foo? Who would have thought it!");
bool b2 = true;
// Ошибка! Booleans не имеет метода Foo()!
// Ь2 .Foo ();
Console.ReadLine();
Ниже показан вывод этой программы:
***** Fun with Extension Methods
*****
Int32 lives here: => mscorlib
DataSet lives here: => System.Data
SoundPlayer lives here: => System
Value of
Reversed
12345678
12345678
mylnt:
digits
called
called
12345678
of mylnt: 87654321
the Foo () method.
Foo () and told me: Ints that Foo? Who would have thought it!
Вызов расш иряю щ их методов статически
Вспомните, что первый параметр расширяющего метода помечен ключевым словом
th is , а за ним следует тип элемента, к которому метод применяется. Если вы посмотри­
те, что происходит “за кулисами” (с помощью инструмента вроде ild a sm .ex e), то обна­
ружите, что компилятор просто вызывает “нормальный” статический метод, передавая
переменную, на которой вызывается метод, в первом параметре (т.е. в качестве значе­
ния th is ). Ниже показаны примерные подстановки кода.
private static void Main(string [] args)
{
Console .WriteLine (''***** Fun with Extension Methods *****\n");
int mylnt = 12345678;
MyExtensions.DisplayDefiningAssembly(mylnt);
System. Data.DataSet d = new DataSetO;
MyExtensions.DisplayDefiningAssembly(d);
System.Media.SoundPlayer sp = new SoundPlayer();
MyExtensions.DisplayDefiningAssembly(sp);
Console.WriteLine("Value of mylnt: {0}", mylnt);
Console.WriteLine("Reversed digits of mylnt: {0}",
MyExtensions.ReverseDigits(mylnt));
TesterUtilClass.Foo(mylnt);
TesterUtilClass.Foo(mylnt, "Ints that Foo?
Who would have thought it!");
Console.ReadLine();
Глава 12. Расширенные средства языка C#
443
Учитывая, что вызов расширяющего метода из объекта (что похоже на вызов метода
уровня экземпляра) — это просто эффект “дымовой завесы”, создаваемый компилято­
ром, расширяющие методы всегда можно вызвать как нормальные статические методы,
используя привычный синтаксис C# (как показано выше).
Контекст расширяющ его метода
Как только что объяснялось, расширяющие методы — это, по сути, статические
методы, которые могут быть вызваны от экземпляра расширяемого типа. Поскольку
это — разновидность синтаксического “украшения”, важно понимать, что в отличие от
“нормального” метода, расширяющий метод не имеет прямого доступа к членам типа,
который он расширяет. Иначе говоря, расширение — это не наследование. Взгляните на
следующий простой тип Саг:
public class Car
{
public int Speed;
public int SpeedUp ()
{
return ++Speed;
Построив расширяющий метод для типа Саг по имени SlowDown (), вы не получи­
те прямого доступа к членам Саг внутри контекста расширяющего метода, поскольку
это не является классическим наследованием. Таким образом, следующий код вызовет
ошибку компиляции:
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// Ошибка! Этот метод не унаследован от Саг!
return --Speed;
}
}
Проблема в том, что расширяющий метод SlowDown () пытается обратиться к
полю Speed типа Саг. Однако поскольку SlowDown () — статический член класса
CarExtension, в его контексте отсутствует Speed!
Тем не менее, допустимо использовать параметр, квалифицированный словом this,
для обращения к общедоступным (и только общедоступным) членам расширяемого типа.
Таким образом, следующий код успешно скомпилируется, как и следовало ожидать:
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// Скомпилируется успешно!
return --c.Speed;
Теперь можно создать объект Саг и вызывать методы SpeedUp () и SlowDown (), как
показано ниже:
static void UseCarO
{
Car с = new Car () ;
Console.WnteLine ("Speed: {0}", c .SpeedUp ()) ;
Console.WriteLine("Speed: {0}", c .SlowDown());
444
Часть III. Дополнительные конструкции программирования на C#
Импорт типов, которые определяют расш иряю щ ие методы
В случае выделения набора статических классов, содержащих расширяющие мето­
ды, в уникальное пространство имен другие пространства имен в этой сборке использу­
ют стандартное ключевое слово using для импорта не только самих статических клас­
сов, но также и каждого из поддерживаемых расширяющих методов. Об этом следует
помнить, поскольку если не импортировать явно корректное пространство имен, то рас­
ширяющие методы будут недоступны в таком файле кода С#. Хотя на первый взгляд мо­
жет показаться, что расширяющие методы глобальны по своей природе, на самом деле
они ограничены пространствами имен, в которых они определены, или пространствами
имен, которые их импортируют Таким образом, если поместить определения рассмат­
риваемых статических классов (MyExtensions, TesterUtilClass и CarExtensions) в
пространство имен MyExtensionMethods, как показано ниже:
namespace MyExtensionMethods
{
static class MyExtensions
static class TesterUtilClass
{
static class CarExtensions
то другие пространства имен в проекте должны явно импортировать пространство
MyExtensionMethods для получения расширяющих методов, определенных этими ти­
пами. Поэтому следующий код вызовет ошибку во время компиляции:
// Единственная директива using,
using System;
namespace MyNewApp
{
class JustATest
{
void SomeMethod()
{
// Ошибка! Для расширения int методом Foo() необходимо
// импортировать пространство имен MyExtensionMethods!
int i = 0 ;
i .Foo () ;
}
}
}
Поддержка расш иряю щ их методов средством IntelliSense
Учитывая тот факт, что расширяющие методы не определены буквально на расши­
ряемом типе, при чтении кода есть шансы запутаться. Например, предположим, что
имеется импортированное пространство имен, в котором определено несколько рас­
ширяющих методов, написанных кем-то из команды разработчиков. При написании
своего кода вы создаете переменную расширенного типа, применяете операцию точки
и обнаруживаете десятки новых методов, которые не являются членами исходного оп­
ределения класса!
Глава 12. Расширенные средства языка C#
445
К счастью, средство IntelliSense в Visual Studio маркирует все расширяющие методы
уникальным значком с изображением синей стрелки вниз (рис. 12.5).
Рис. 12 .5. Отображение расширяющих методов в IntelliSense
Если метод помечен этим значком, это означает, что он определен вне исходного
определения класса, через механизм расширяющих методов.
Исходный код. Проект ExtensionMethods доступен в подкаталоге Chapter 12.
Построение и использование библиотек расш ирений
В предыдущем примере производилось расширение функциональности различных
типов (таких как System. Int32) для использования в текущем консольном приложе­
нии. Представьте, насколько было бы полезно построить библиотеку кода .NET, опреде­
ляющую расширения, на которые могли бы ссылаться многие приложения. К счастью,
сделать это очень легко.
Подробности создания и конфшурирования специальных библиотек будут рассмат­
риваться в главе 14; а пока, если хотите реализовать самостоятельно приведенный здесь
пример, создайте проект библиотеки классов по имени MyExtensionLibrary. Затем пе­
реименуйте начальный файл кода C# на MyExtensions .cs и скопируйте определение
класса MyExtensions в новое пространство имен:
namespace MyExtensionsLibrary
{
// Не забудьте импортировать System.Reflection!
public static class MyExtensions
{
// Та же реализация, что и раньше.
public static void DisplayDefiningAssembly(this object obj)
{...}
// Та же реализация, что и раньше.
public static int ReverseDigits(this int l)
{ ...}
}
}
На заметку! Чтобы можно было экспортировать расширяющие методы из библиотеки кода .NET,
определяющий их тип должен быть объявлен с ключевым словом public (вспомните, что по
умолчанию действует модификатор доступа internal).
446
Часть III. Дополнительные конструкции программирования на C#
П осле этого
можно
ском п и ли ровать б и бли отек у и ссы латься на сборку
MyExtensionsLibrary.dll внутри новых проектов .NET. Это позволит использовать
новую функциональность System.Object и System. Int32 в любом приложении, кото­
рое ссылается на библиотеку.
Ч тобы проверить сказанное, создадим новый проект консольного прилож е­
ния (по имени MyExtensionsLibraryClient) и добавим к нему ссылку на сборку
MyExtensionsLibrary.dll. В начальном файле кода укажем на использование про­
странства имен MyExtensionsLibrary и напишем простой код, который вызывает эти
новые методы на локальном значении int:
using System;
// Импортируем наше специальное пространство имен,
using MyExtensionsLibrary;
namespace MyExtensionsLibraryClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine (''***** Using Library with Extensions *****\n");
// Теперь эти расширяющие методы определены внутри внешней
// библиотеки классов .NET.
int mylnt = 987;
myInt.DisplayDefiningAssembly() ;
Console.WriteLine("{0} is reversed to {1}",
mylnt, mylnt.ReverseDigits() );
Console.ReadLine();
}
}
В Microsoft рекомендуют размещать типы, которые имеют расширяющие методы, в
отдельной сборке (внутри выделенного пространства имен). Причина проста — сокра­
щение сложности программной среды. Например, если вы напишете базовую библиоте­
ку для внутреннего использования в компании, причем в корневом пространстве имен
этой библиотеки определено 30 расширяющих методов, то в конечном итоге все прило­
жения будут видеть эти методы в списках IntelliSense (даже если они и не нужны).
Исходный код. Проекты MyExtensionsLibrary и MyExtensionsLibraryClient доступны
в подкаталоге Chapter 12.
Расш ирение интерфейсных типов через расш иряю щ ие методы
Итак, было показано, каким образом расширять классы (а также структуры, которые
следуют тому же синтаксису) новой функциональностью через расширяющие методы.
Чтобы завершить исследование расширяющих методов С#, следует отметить, что новы­
ми методами можно также расширять и интерфейсные типы; однако семантика такого
действия определенно несколько отличается от того, что можно было бы ожидать.
Создадим новое консольное приложение по имени InterfaceExtensions, а в нем —
простой интерфейсный тип (IBasicMath), включающий единственный метод по име­
ни Add ( ) . Затем подходящим образом реализуем этот интерфейс в каком-нибудь типе
класса (Муса 1с). Например:
// Определение обычного интерфейса на С#.
interface IBasicMath
Глава 12. Расширенные средства языка C#
447
int Add(int x f int у) ;
}
// Реализация IBasicMath.
class MyCalc : IBasicMath
{
public int'Add (int x, int y)
{
return x + y;
Теперь предположим, что доступ к коду с определением IBasicMath отсутствует, но
к нему нужно добавить новый член (например, метод вычитания), чтобы расширить его
поведение. Можно попробовать написать следующий расширяющий класс:
static class MathExtensions
{
// Расширить IBasicMath методом вычитания?
public static int Subtract(this IBasicMath itf,
int x, int y) ;
Однако такой код вызовет ошибку во время компиляции. В случае расширения ин­
терфейса новыми членами должна также предоставляться реализация этих членов!
Это кажется противоречащим самой идее интерфейсных типов, поскольку интерфейсы
не включают реализации, а только определения. Тем не менее, класс MathExtensions
должен быть определен следующим образом:
static class MathExtensions
{
// Расширить IBasicMath этим методом с этой реализацией,
public static int Subtract (this IBasicMath itf, int x, int y)
{
return x - y;
}
Теперь может показаться, что допустимо создать переменную типу IBasicMath и не­
посредственно вызвать Substract ( ) . Опять-таки, если бы такое было возможно (а на
самом деле нет), то это нарушило бы природу интерфейсных типов .NET. На самом деле
приведенный код говорит вот что: “Любой класс в моем проекте, реализующий интер­
фейс IBasicMath, теперь имеет метод Substract ( ) , реализованный представленным
образом”. Как и раньше, все базовые правила соблюдаются, а потому пространство
имен, определяющее MyCalc, должно иметь доступ к пространству имен, определяюще­
му MathExtensions. Рассмотрим следующий метод Main ():
static void Main(string[] args)
{
Console.WnteLine ("***** Extending an interface *****\n ");
// Вызов членов IBasicMath из объекта MyCalc.
MyCalc c = new MyCalc ();
Console.WnteLine ("1 + 2 = {0}", c.Add(l, 2));
Console.WriteLine("1 - 2 = {0}", c .Subtract(1, 2));
// Для вызова расширения можно выполнить приведение к IBasicMath.
Console. WnteLine ("30 - 9 = {0}", ((IBasicMath) с ). Subtract (30, 9));
// Это не будет работать!
// IBasicMath ltfBM = new IBasicMath ();
// itfBM.Subtract (10, 10);
Console.ReadLine ();
}
448
Часть III. Дополнительные конструкции программирования на C#
На этом исследование расширяющих методов C# завершено. Помните, что это кон­
кретное языковое средство может быть очень полезным, когда нужно расширить функ­
циональность типа, даже если нет доступа к первоначальному исходному коду (или
если тип запечатан), в целях поддержания полиморфизма. Во многом подобно неявно
типизированным локальным переменным, расширяющие методы являются ключевым
элементом работы с A PI-интерфейсом LINQ. Как будет показано в следующей главе,
множество существующих типов в библиотеках базовых классов расширены новой
функциональностью через расширяющие методы, что позволяет им интегрироваться в
программную модель LINQ.
Исходный код. Проект InterfaceExtension доступен в подкаталоге Chapter 12.
Понятие частичных методов
Начиная с версии .NET 2.0, строить определения частичных классов стало возмож­
но с использованием ключевого слова p a r t i a l (см. главу 5). Вспомните, что эта деталь
синтаксиса позволяет разбивать полную реализацию типа на несколько файлов кода
(или других мест, таких как память). До тех пор, пока каждый аспект частичного типа
имеет одно полностью квалифицированное имя, конечным результатом будет “нормаль­
ный” скомпилированный класс, находящийся в созданной компилятором сборке.
Язык C# расширяет роль ключевого слова p a r t i a l , позволяя его применять на уров­
не метода. По сути, это дает возможность прототипировать метод в одном файле, а реа­
лизовать в другом. При наличии опыта работы в C++, это может напомнить отношения
между файлами заголовков и реализаций C++. Тем не менее, частичные методы C# об­
ладают рядом важных ограничений.
• Частичные методы могут определяться только внутри частичного класса.
• Частичные методы должны возвращать void.
• Частичные методы могут быть статическими или методами экземпляра.
• Частичные метода могут иметь аргументы (включая параметры с модификатора­
ми this, ref или params, но не с out).
• Частичные метода всегда неявно приватные (private).
Еще более странным является тот факт, что частичный метод может быть как поме­
щен, так и не помещен в скомпилированную сборку! Для прояснения картины давайте
рассмотрим пример.
Первый взгляд на частичные методы
Для оценки влияния определения частичного метода создадим проект консольно­
го приложения по имени PartialMethods. Затем определим новый класс CarLocator
внутри файла C# по имени CarLocator.cs:
// CarLocator.cs
partial class CarLocator
{
// Этот член всегда будет частью класса CarLocator.
public bool CarAvailablelnZipCode (string zipCode)
{
// Этот вызов *может* быть частью реализации данного метода.
VenfyDuplicates (zipCode) ;
// Некоторая интересная логика взаимодействия с базой данных...
return true;
}
Глава 12. Расширенные средства языка C#
449
// Этот член *может* быть частью класса CarLocator!
partial void V e n f yDuplicates (string make);
}
Обратите внимание, что метод Verif yDuplicates () определен с модифика­
тором partial и не имеет определения тела внутри этого файла. Кроме того, ме­
тод CarAvailablelnZipCode () содержит вызов VerifyDuplicates () внутри своей
реализации.
Скомпилировав это приложение в таком, как оно есть виде, и открыв скомпилиро­
ванную сборку в утилите ildasm.exe или refatcor.exe, вы не обнаружите там ни­
каких следов Verif yDuplicates () в классе CarLocator, равно как никаких вызовов
Verif yDuplicates () внутри CarAvailablelnZipCode ( ) ! С точки зрения компилятора
в данном проекте класс CarLocator определен в следующем виде:
internal class CarLocator
{
public bool CarAvailablelnZipCode(string zipCode)
{
return true;
}
}
Причина столь странного усечения кода связана с тем, что частичный метод
Verif yDuplicates () не имеет реальной реализации. Добавив в проект новый файл (на­
пример, CarLocatorlmpl .cs) с определением остальной порции частичного метода:
// CarLocatorlmpl.cs
partial class CarLocator
1
partial void VerifyDuplicates(string make)
{
// Assume some expensive data validation
// takes place here...
вы обнаружите, что во время компиляции будет принят во внимание полный комплект
класса CarLocator, как показано в следующем примерном коде С#:
internal class CarLocator
{
public bool CarAvailablelnZipCode (string zipCode)
{
this.VerifyDuplicates(zipCode);
return true;
}
private void VerifyDuplicates(string make)
{
}
}
Как видите, когда метод определен с ключевым словом partial, то компилятор
принимает решение о том, нужно ли его включить в сборку, в зависимости от нали­
чия у этого метода тела или же просто пустой сигнатуры. Если у метода нет тела, все
его упоминания (вызовы, описания метаданных, прототипы) на этапе компиляции
отбрасываются.
В некоторых отношениях частичные методы C# — это строго типизированная вер­
сия условной компиляции кода (через директивы препроцессора #if, #elif и #endif).
Однако основное отличие состоит в том, что частичный метод будет полностью проиг­
450
Часть III. Дополнительные конструкции программирования на C#
норирован во время компиляции (независимо от настроек сборки), если отсутствует его
соответствующая реализация.
Использование частичных методов
Учитывая ограничения, присущие частичным методам, самое важное из которых
связано с тем, что они должны быть неявно private и всегда возвращать void, сразу
представить множество полезных применений этого средства языка может быть труд­
но. По правде говоря, из всех языковых средств C# частичные методы кажутся наиме­
нее востребованными.
В текущем примере метод VerifyDuplicates () помечен как частичный в демонст­
рационных целях. Однако предположим, что этот метод, будучи реализованным, выпол­
няет некоторые очень интенсивные вычисления.
Снабжение этого метода модификатором partial дает возможность другим разра­
ботчикам классов создавать детали реализации по своему усмотрению. В данном случае
частичные методы предоставляют более ясное решение, чем применение директив пре­
процессора, поддерживая “фиктивные” реализации виртуальных методов либо генери­
руя объекты исключений NotlmplementedException.
Наиболее распространенным применением этого синтаксиса является определение
так называемых легковесных событий. Эта техника позволяет проектировщикам клас­
сов представлять привязки для методов, подобно обработчикам событий, относительно
которых разработчики могут самостоятельно решать — реализовывать их или нет. В со­
ответствии с принятым соглашением, имена таких методов-обработчиков легковесных
событий имеют префикс On. Например:
// CarLocator.EventHandler.cs
partial class CarLocator
{
public bool CarAvailablelnZipCode(string zipCode)
OnZipCodeLookup(zipCode);
return true;
>
// Обработчик "легковесного" события,
partial void OnZipCodeLookup(string make);
}
Если разработчик класса пож елает получать уведом ления о вызове метода
CarAvailablelnZipCode (), он может предоставить реализацию метода OnZipCodeLookup ().
В противном случае ничего делать не потребуется.
Исходный код. Проект PartialMethods доступен в подкаталоге Chapter 12.
Понятие анонимных типов
Как объектно-ориентированный программист, вы знаете преимущества классов
в отношении представления состояния и функциональности заданной программной
сущности. То есть, когда нужно определить класс, который предполагает многократное
использование и предоставляет обширную функциональность через набор методов, со­
бытий, свойств и специальных конструкто
Download