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());
Conso